Toán hạng ampersand trong Ruby
Kí hiệu: ampersand: kí tự '&' operator: toán tử operand: toán hạng unary operator: toán tử đơn 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à ...
Kí hiệu: ampersand: kí tự '&' operator: toán tử operand: toán hạng unary operator: toán tử đơn
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ành Proc
- Nếu object là Proc, & converts nó thành block
- Nếu object không phải block hay Proc, 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 ...