Ruby callable object
This post hasn't been updated for 4 years
Sử dụng một block là quy trình gồm 2 bước: Một là viết code vào đó, hai là thực thi code đó. Định nghĩa "Đóng gói code trước, thực thi nó sau" không phải là duy nhất với block. Trong Ruby có ít nhất 3 nơi bạn có thể đóng gói code:
proc
, là một block objectlambda
, tương tự như procmethod
Proc objects
Hầu hết mọi thứ trong Ruby đều là object, nhưng block thì không phải. Nhưng tại sao bạn phải quan tâm về điều đó? Hãy tưởng tượng bạn muốn lưu trữ một block và thực thi chúng sau, để làm được điều này bạn phải cần 1 object.
Ruby cung cấp một class là Proc
, cho phép một block được trở thành một object.
Khai báo một proc với Proc.new
và thực thi nó với Proc#call
inc = Proc.new {|x| x + 1}
inc.class # => Proc
inc.call(2) # => 3
Ruby cung cấp 2 method để convert block thành Proc là proc
và lambda
# use proc
inc = proc {|x| x + 1}
# use lambda
inc = lambda {|x| x + 1}
# hoặc 1 cách viết khác của lambda
inc = ->(x) {x + 1}
Toán tử &
Một block giống như là một argument vô danh của method. Bạn có thể thực thi block trong method bằng cách sử dụng yield
. Tuy nhiên, trong 2 trường hợp sau thì yield
không thể:
- Bạn muốn pass một block của method này vào một method khác (thậm chí là vào block khác)
- Bạn muốn convert block thành một object Proc
Để làm được điều này, đầu tiên bạn cần 1 cái tên cho block. Để đính kèm một bindings
với block, bạn có thể thêm một argument đặc biệt vào method, nó nằm cuối cùng của list arguments và có thêm tiền tố &
def math(a, b)
yield(a, b)
end
def do_math(a, b, &op)
math(a, b, &op)
end
do_math(2, 3) {|x, y| x * y} # => 6
Ở ví dụ trên, toán tử &
cũng đã đồng thời convert block thành một object Proc.
def my_method &pr
pr
end
a = my_method {"Hello"}
a.class # => Proc
a.call # => "Hello"
Bạn có thể convert block thành Proc bằng toán tử &
, vậy nếu bạn muốn làm ngược lại convert Proc thành block thì làm thế nào? Vẫn là toán tử &
:
def my_method
"Hello #{yield}"
end
p = Proc.new {"Canh"}
my_method(&p)
# => "Hello Canh"
procs vs lambdas
Chúng ta có nhiều cách để tạo ra một Proc: Proc.new, lambda, toán tử &
. Tất cả object tạo ra đều là Proc. Tuy nhiên, có một chút khác biệt giữa lambda với phần còn lại - gọi chung là procs
. Khác biệt tuy ít nhưng khá quan trọng để chúng ta định nghĩa thành 2 loại của Proc là procs
và lambdas
(bạn cũng có thể dùng method Proc#lambda?
để kiểm tra xem Proc object đó có phải là lambda hay không).
Hai điểm khác biệt giữa procs và lambdas là từ khóa return
và việc check arguments
procs, lambdas with return keyword
Trong 1 lambda, return
sẽ returns từ lambda đó
def double(callable_obj)
callable_obj.call * 2
end
l = lambda {return 10}
double(l) # => 20
Trong 1 proc, return
sẽ returns từ scope mà proc đó được định nghĩa:
def another_double
p = Proc.new {return 10}
result = p.call
# Method sẽ được return tại đây, những dòng code sau không thực thi
puts "Hello"
return result * 2
end
another_double # => 10
Trở lại với hàm double
ở trên, nếu thay lambda bằng proc
p = Proc.new {return 10}
double(p) # => LocalJumpError
Chương trình sẽ return từ scope mà proc được định nghĩa, chính là Top-Level. Nhưng bạn không thể return từ scope Top-Level. Để fix lỗi trên bạn có thể remove từ khóa return
trong proc
procs, lambdas with arguments
Điểm khác nhau thứ 2 giữa procs và lambdas chính là cách nhận arguments truyền vào.
procs sẽ không quan tâm tới việc block muốn nhận bao nhiêu arguments
p = Proc.new {|a, b| [a, b]}
p.call(1, 2, 3) # => [1, 2]
p.call(1) # => [1, nil]
Trong khi lambdas sẽ raise error khi bạn truyền vào không đúng tham số mà block mong muốn
l = lambda {|a, b| [a, b]}
l.call(1, 2, 3) # => ArgumentError
l.call(1) # => ArgumentError
l.call(1, 2) # => [1, 2]
Một cách thông thường chúng ta có thể thấy lambdas khá giống với method, nghiêm ngặt với tham số truyền vào, exists khi return. Chính vì vậy nếu không có lí do đặc biệt, chúng ta sẽ sử dụng lambdas thay vì procs.
Method objects
Thành viên cuối cùng trong gia đình callable objects là method objects.
class MyClass
def initialize x
@x = x
end
def my_method
@x
end
end
obj = MyClass.new(1)
m = obj.method :my_method
m.call # => 1
Bằng cách gọi hàm Kenel#method
bạn có thể biến method đó thành một object của class Method, và thực thi sau với Method#call
Một Method object tương tự như một block hoặc một lambda. Bạn có thể convert một Method thành một proc bằng Method#to_proc
. Hoặc bạn cũng có thể convert 1 block thành 1 method bằng define_method
.
Tuy nhiên, có một sự khác biệt quan trọng giữa lambdas và methods đó là: lambdas thì được đánh giá bên trong scope nó định nghĩa (nó là một bao đóng), còn methods thì được đánh giá bên trong object gọi nó.
Bài viết tham khảo từ cuốn sách Metaprogramming Ruby 2
All Rights Reserved