Trong Ruby Magic chúng ta yêu thích đi sâu vào phép màu đằng sau những thứ dùng hàng ngày để hiểu cách chúng hoạt động. Phần này, chúng ta sẽ khám phá sự khác biệt giữa blocks, procs và lambdas.
Trong ngôn ngữ lập trình có first-class functions, function có thể lưu trong biến và pass như argument cho function khác. Function thậm chí có thể dùng function khác làm return value.
Closure là first-class function có environment. Environment là mapping đến biến tồn tại khi closure được tạo. Closure giữ quyền truy cập các biến này, ngay cả khi chúng định nghĩa ở scope khác.
Ruby không có first-class functions, nhưng có closures dạng blocks, procs và lambdas. Blocks dùng pass code blocks cho method, procs và lambdas cho phép lưu code blocks trong biến.
Blocks
Trong Ruby, blocks là đoạn code snippet tạo ra để thực thi sau. Blocks pass cho method yield chúng trong từ khóa do và end. Ví dụ điển hình là #each, lặp qua enumerable object.
[1,2,3].each do |n|
puts "#{n}!"
end
[1,2,3].each { |n| puts "#{n}!" } # one-line equivalent.
Trong ví dụ, block pass cho Array#each, chạy block cho mỗi item trong array và in ra console.
def each
i = 0
while i < size
yield at(i)
i += 1
end
end
Trong ví dụ đơn giản Array#each, trong vòng while, yield gọi để execute block pass cho mỗi item. Lưu ý method này không có argument, vì block pass implicit.
Implicit Blocks và Từ khóa yield
Trong Ruby, method có thể nhận block implicit và explicit. Implicit block passing hoạt động bằng cách gọi từ khóa yield trong method. yield đặc biệt. Nó tìm và gọi block pass, nên không cần thêm block vào danh sách argument method.
Vì Ruby cho phép implicit block passing, bạn có thể gọi mọi method với block. Nếu nó không gọi yield, block bị ignore.
irb> "foo bar baz".split { p "block!" }
=> ["foo", "bar", "baz"]
Nếu method gọi có yield, block pass được tìm và gọi với argument pass cho yield.
def each
return to_enum(:each) unless block_given?
i = 0
while i < size
yield at(i)
i += 1
end
end
Ví dụ này trả instance Enumerator trừ khi block given.
yield và block_given? tìm block trong scope hiện tại. Cho phép pass block implicit, nhưng ngăn code truy cập block trực tiếp vì không lưu trong biến.
Explicitly Passing Blocks (Pass block tường minh)
Ta có thể explicit accept block trong method bằng cách thêm argument dùng ampersand parameter (thường gọi &block). Vì block giờ explicit, ta dùng method #call trực tiếp trên object kết quả thay vì rely vào yield.
Argument &block không phải argument proper, nên gọi method với bất cứ thứ gì ngoài block sẽ gây ArgumentError.
def each_explicit(&block)
return to_enum(:each) unless block
i = 0
while i < size
block.call at(i)
i += 1
end
end
Khi block pass như này và lưu trong biến, nó tự động convert thành proc.
Procs
"Proc" là instance của class Proc, giữ code block để execute, và có thể lưu trong biến. Để tạo proc, gọi Proc.new pass block.
proc = Proc.new { |n| puts "#{n}!" }
Vì proc lưu trong biến, nó cũng pass cho method như argument bình thường. Lúc này không dùng ampersand, vì proc pass explicit.
def run_proc_with_random_number(proc)
proc.call(random)
end
proc = Proc.new { |n| puts "#{n}!" }
run_proc_with_random_number(proc)
Thay vì tạo proc pass cho method, ta dùng ampersand parameter syntax Ruby như trước và dùng block thay thế.
def run_proc_with_random_number(&proc)
proc.call(random)
end
run_proc_with_random_number { |n| puts "#{n}!" }
Lưu ý ampersand thêm vào argument method. Sẽ convert block pass thành proc object và lưu trong biến trong scope method.
Tip: Mặc dù có proc trong method hữu ích một số trường hợp, convert block sang proc gây performance hit. Khi có thể, dùng implicit blocks thay thế.
#to_proc
Symbol, hash và method có thể convert sang proc dùng #to_proc. Use case thường thấy là pass proc tạo từ symbol cho method.
[1,2,3].map(&:to_s)
[1,2,3].map {|i| i.to_s }
[1,2,3].map {|i| i.send(:to_s) }
Ví dụ này show 3 cách equivalent gọi #to_s trên mỗi element array. Cái đầu, symbol prefix ampersand pass, tự convert thành proc bằng #to_proc. Hai sau show proc đó trông như nào.
class Symbol
def to_proc
Proc.new { |i| i.send(self) }
end
end
Mặc dù ví dụ đơn giản, implementation Symbol#to_proc show điều xảy ra under the hood. Method trả proc nhận 1 argument và send self cho nó. Vì self là symbol, nó gọi Integer#to_s.
Lambdas
Lambdas về cơ bản là procs với vài yếu tố phân biệt. Chúng giống "regular" methods hơn 2 cách: enforce số argument pass khi gọi và dùng "normal" returns.
Khi gọi lambda expect argument mà không pass, hoặc pass argument cho lambda không expect, Ruby raise ArgumentError.
irb> lambda (a) { a }.call
ArgumentError: wrong number of arguments (given 0, expected 1)
from (irb):8:in `block in irb_binding'
from (irb):8
from /Users/jeff/.asdf/installs/ruby/2.3.0/bin/irb:11:in `<main>'
Cũng, lambda treat return keyword như method. Khi gọi proc, chương trình yield control cho code block trong proc. Vì vậy, nếu proc return, scope hiện tại return. Nếu proc gọi trong function và gọi return, function return ngay lập tức.
def return_from_proc
a = Proc.new { return 10 }.call
puts "This will never be printed."
end
Function này yield control cho proc, nên khi proc return, function return. Gọi function ví dụ sẽ không bao giờ print output và return 10.
def return_from_lambda
a = lambda { return 10 }.call
puts "The lambda returned #{a}, and this will be printed."
end
Khi dùng lambda, nó sẽ print. Gọi return trong lambda behave như gọi return trong method, nên biến a populated với 10 và dòng in ra console.
Blocks, procs và lambdas
Đã đi sâu vào cả blocks, procs, lambdas, hãy zoom out tóm tắt so sánh.
- Blocks dùng nhiều trong Ruby pass code bits cho function. Dùng
yield, block pass implicit không cần convert sang proc. - Dùng parameter prefix ampersand, pass block cho method tạo proc trong context method. Procs behave như blocks, nhưng lưu trong biến.
- Lambdas là procs behave như methods, nghĩa là enforce arity và return như method thay vì parent scope.
Kết thúc look vào closures trong Ruby. Còn nhiều về closures như lexical scopes và bindings, nhưng giữ cho episode tương lai. Meantime, cho chúng tôi biết bạn muốn đọc gì trong Ruby Magic tương lai, closures hay khác @AppSignal.