Toán hạng ampersand trong Ruby
Bài đăng này đã không được cập nhật trong 7 năm
Kí hiệu: ampersand: kí tự '&' operator: toán tử operand: toán hạng unary operator: toán tử đơn
Ruby's '&' operator
Mục tiêu của bài viết
Mục tiêu của mình khi viết bài này đó là tìm hiểu & làm rõ 1 vài khía cạnh của toán tử '&' trong Ruby. Nếu bạn đã quen với các khái niệm block
, Proc
, lambda
và phân biệt được các khái niệm này, just go ahead,
còn nếu chưa thì mình khuyên bạn nên dành 1 vài phút Google về nó thì sẽ tốt hơn.
Khi sử dụng Ruby chắc các bạn không còn lạ lẫm gì với kiểu khai báo method như thế này:
class TellMeAboutBlock
def accept_block(&block)
end
end
hoặc kiểu này có khi còn hay gặp hơn
[1,2].map(&:to_s)
# => ["1", "2"]
Hai đoạn code trên có điểm chung là đều sử dụng &
, thế nhưng điểm khác nhau là gì, điều gì diễn ra đằng sau kí tự &
.
Trong ví dụ đầu tiên, &
converts block
thành một Proc
object, còn ở ví dụ thứ hai &
converts :to_s
thành một block
mà hàm map
có thể tiếp nhận như là một đối số.
Cơ bản thì xử lý theo 3 kiểu, toán tử &
converts blocks
và non-Proc
objects trở thành Proc
objects, hoặc từ Proc
objects trở thành blocks
. Blog Post này summarizes đầy đủ và chi tiết về 3 cách đó:
- Nếu object là
block
,&
converts nó thànhProc
- Nếu object là
Proc
,&
converts nó thànhblock
- Nếu object không phải
block
hayProc
, trước hết&
gọi hàm object.to_proc để chuyển nó thành một Proc, sau đó lại convert về block
&block -> Proc
Trường hợp đầu tiên, hãy xem khi object là block
thì sao nhé:
class TellMeAboutBlock
def tell_me_class(&block)
p block.class
end
end
n = TellMeAboutBlock.new
n.tell_me_class {p "Random Block"}
# => Proc
Ở đoạn code trên, hàm #tell_me_class(&block) in ra tên class của đối số &block
mà nó nhận vào. Khi dòng n.tell_me_class {"Random Block"} chạy thì, "Proc" được returned.
Nếu tinh ý một chút thì bạn sẽ nhận ra ngay là những block
ví dụ như {|n| n+1}
hay {"Random Block"}
bản chất nó không phải là Ruby objects; nó không thuộc về một class nào cả. Để chứng minh điều vừa nói, ví dụ sau cho kết quả là SyntaxError:
{|n| n+1}.class
# =>
SyntaxError: (irb):4: syntax error, unexpected '|', expecting '}'
Điều này có nghĩa là hàm #tell_me_class đã thực hiện một điều gì đó với parameter &block
để rồi cuối cùng kết quả được in ra là Proc
class. Vậy chắc bạn cũng đã đoán được vai trò của toán tử &
ở đây rồi.
Khi một hàm được định nghĩa, bất cứ khi nào parameter cuối cùng được prefix bởi toán tử &
, điều đó cũng tương đương mọi block sẽ được convert thành Proc
. Ta chỉ cần duy nhật 1 parameter với &
và parameter đó phải được liệt kê cuối cùng.
Nếu bạn đang thắc mắc là nếu trong block có yield
thì nó có hoạt động không thì câu trả lời là CÓ.
class TellMeAboutBlock
def tell_me_class(&block)
puts block.class
yield
end
end
n = TellMeAboutBlock.new
n.tell_me_class {p "Random Block"}
# =>
Proc
"Random Block"
Nhưng điều gì xảy ra nếu ta không truyền block
, mà thay vào đó, ta truyền trực tiếp một Proc
object vào hàm #tell_me_class, như thế này:
class TellMeAboutBlock
def tell_me_class(&block)
puts block.class
end
end
n = TellMeAboutBlock.new
my_proc = Proc.new {p "Random Block"}
n.tell_me_class(my_proc)
# => wrong number of arguments (1 for 0) (ArgumentError)
ArgumentError là bởi vì #tell_me_class expect 1 block, không phải 1 đối số "thông thường".
&Proc -> block
n.tell_me_class(&my_proc)
# => Proc
Đúng như những gì chúng ta expect.
Có một chú ý nhỏ là &
bảo toàn status lambda
của Proc, ví dụ sau là minh chứng:
my_lam = ->(n) { n.to_s}
n.tell_me_class(&my_lam)
# => #<Proc:0x007fe07502e3f8@proc_post.rb:15 (lambda)>
Có một câu hỏi đặt ra ở đây là: Vì sao ở ví dụ trên, khi ta truyền trực tiếp Proc vào hàm tell_me_class
lại báo lỗi wrong number of arguments (1 for 0) (ArgumentError)
Chẳng phải hàm này expect số lượng arguments là 1 đó sao?
Hóa ra là dù signature của hàm này là: tell_me_class(&block) nhưng khi kiểm tra arity
thì không tính đối số &block
Minh chứng đây:
class TellMeAboutBlock
def tell_me_class(&block)
puts block.class
end
def two_params_one_is_block(param, &block)
end
end
n = TellMeAboutBlock.new
n.method(:tell_me_class).arity
# => 0
n.method(:two_params_one_is_block).arity
# => 1
Điều này dẫn đến 1 kết luận đó là: Nếu 1 hàm chỉ nhận &block mà ta lại truyền vào Proc objects, FixNum objects, String objects, v.v. thì sẽ bị lỗi ArgumentError
class TellMeAboutBlock
def tell_me_class(&block)
puts block.class
end
end
n = TellMeAboutBlock.new
my_proc = Proc.new {p "Random Block"}
n.tell_me_class(my_proc)
# => wrong number of arguments (1 for 0) (ArgumentError)
n.tell_me_class(1)
# => wrong number of arguments (1 for 0) (ArgumentError)
n.tell_me_class("Hello")
# => wrong number of arguments (1 for 0) (ArgumentError)
Nhưng bạn có thể KHÔNG truyền vào 1 đối số nào và sẽ KHÔNG bị một lỗi nào cả !
n.tell_me_class()
# => NilClass
# Vì chỗ này ta không đưa &block vào nên nó bằng nil thôi
n.tell_me_class(nil)
# => wrong number of arguments (1 for 0) (ArgumentError)
Small Note On Performance
Ưu tiên sử dụng yield khi muốn trigger block nhé. Để thực thi block, làm thế này:
class TellMeAboutBlock
def tell_me_class(&block)
yield
end
end
Thay vì:
class TellMeAboutBlock
def tell_me_class(&block)
block.call
end
end
&non-Proc -> block
Và chúng ta sẽ xem xét đến trường hợp thứ 3, để xem &object với object không phải Proc thì sao.
Nếu object không phải block hoặc Proc, trước tiên convert về Proc bằng cách gọi object.to_proc
sau đó convert tiếp Proc về block như trường hợp số 2.
[1,2].map(&:to_s)
# => ["1", "2"]
Đầu tiên chúng ta biết method map
nhận 1 block. Như document thì:
map { |obj| block } → array
Returns a new array with the results of running block once for every element in enum.
map
chỉ nhận block mà thôi, nếu mà ta định truyền 1 Proc vào thì sẽ lỗi ngay:
my_proc = Proc.new {|n| n.to_s}
[1,2].map(my_proc)
# => wrong number of arguments (1 for 0) (ArgumentError)
Như đã nói ở các ví dụ trên ta có thể dùng &
để chuyển Proc về block và pass nó vào map
my_proc = Proc.new {|n| n.to_s}
[1,2].map(&my_proc)
# => ["1", "2"]
... To be continued ...
All rights reserved