Truyền block vào method không sử dụng &block
Bài đăng này đã không được cập nhật trong 8 năm
Có 2 cách để truyền vào một block cho một hàm trong Ruby.
Cách 1
Sử dụng từ khoá yield
:
def speak
puts yield
end
speak { "Hello" }
# Hello
# => nil
Cách 2
Chèn vào trước argument cuối của một hàm với một dấu &
(ampersand) để sau đó tạo ra một đối tượng Proc
từ bất kể block
nào được truyền vào. Đối tượng này có thể được thực thi với hàm call
như sau:
def speak(&block)
puts block.call
end
speak { "Hello" }
# Hello
# => nil
Vấn đề của cách thứ 2 là khi khởi tạo đối tượng Proc mới sẽ làm ảnh hưởng đến tốc độ, anh Aaron Patterson có giải thích chi tiết trong bài nói “ZOMG WHY IS THIS CODE SO SLOW?" tại RubyConf X, (đoạn 30 phút ở trang 181).
Ta có thể kiểm tra thông qua benchmark
, block_benchmark.rb
:
require "benchmark"
def speak_with_block(&block)
block.call
end
def speak_with_yield
yield
end
n = 1_000_000
Benchmark.bmbm do |x|
x.report("&block") do
n.times { speak_with_block { "ook" } }
end
x.report("yield") do
n.times { speak_with_yield { "ook" } }
end
end
Kết quả cho thấy sự khác biệt rõ rệt giữa 2 cách:
$ ruby block_benchmark.rb
Rehearsal ------------------------------------------
&block 1.410000 0.020000 1.430000 ( 1.430050)
yield 0.290000 0.000000 0.290000 ( 0.291750)
--------------------------------- total: 1.720000sec
user system total real
&block 1.420000 0.030000 1.450000 ( 1.452686)
yield 0.290000 0.000000 0.290000 ( 0.292179)
Như vậy ta có thể nhận thấy việc sử dụng yield
sẽ nhanh hơn &block
. Nhưng khi ta cần truyền 1 block
cho 1 method
thì sao?
Ví dụ, ta có 1 class với hàm tell_ape
gọi đến hàm tell
. Kiểu pattern thường được xử lý bằng method_missing nhưng ta sẽ giữ và khai báo toàn bộ các hàm để tiện giải thích:
class Monkey
# Monkey.tell_ape { "ook!" }
# ape: ook!
# => nil
def self.tell_ape(&block)
tell("ape", &block)
end
def self.tell(name, &block)
puts "#{name}: #{block.call}"
end
end
Cách làm trên là không thể nếu sử dụng yield
class Monkey
# Monkey.tell_ape { "ook!" }
# ArgumentError: wrong number of arguments (2 for 1)
def self.tell_ape
tell("ape", yield)
end
def self.tell(name)
puts "#{name}: #{yield}"
end
end
Và cách trên cũng không thể chạy nếu sử dụng &block
:
class Monkey
# Monkey.tell_ape { "ook!" }
# TypeError: wrong argument type String (expected Proc)
def self.tell_ape
tell("ape", &yield)
end
def self.tell(name)
puts "#{name}: #{yield}"
end
end
Tuy nhiên có 1 cách để chỉ tạo ra 1 object Proc
khi cần thiết, đó là cách sử dụng một đặc tính ít được biết đến của hàm Proc.new
, anh Aaron có giải thích trong bài nói được nhắc ở trên
Nếu Proc.new
được gọi từ bên trong một hàm mà không có bất kỳ argument nào thì nó sẽ trả về một Proc
có kèm block
được đưa cho hàm ở ngoài.
Nếu Proc.new
được gọi từ bên trong một hàm với không có argument nào của chính nó, nó sẽ trả về một Proc
có chứa block
cho method
chứa nó.
def speak
puts Proc.new.call
end
speak { "Hello" }
# Hello
Điều này có nghĩa là ta có thể truyền vào một block
giữa các methods
mà không cần phải sử dụng &block
class Monkey
# Monkey.tell_ape { "ook!" }
# ape: ook!
# => nil
def self.tell_ape
tell("ape", &Proc.new)
end
def self.tell(name)
puts "#{name}: #{yield}"
end
end
Dĩ nhiên là nếu dùng Proc.new
, thì sẽ mất thời gian hơn so với yield
(khi các đối tượng Proc được khởi tạo với &block) nhưng nó sẽ tránh được các khởi tạo không cần thiết của các đối tượng Proc
khi ta không cần đến chúng. Ta có thể sử dụng benmark để làm rõ điều trên: proc_new_benchmark.rb:
require "benchmark"
def sometimes_block(flag, &block)
if flag && block
block.call
end
end
def sometimes_proc_new(flag)
if flag && block_given?
Proc.new.call
end
end
n = 1_000_000
Benchmark.bmbm do |x|
x.report("&block") do
n.times do
sometimes_block(false) { "won't get used" }
end
end
x.report("Proc.new") do
n.times do
sometimes_proc_new(false) { "won't get used" }
end
end
end
Ta có thể thấy sự khác biệt khá lớn:
$ ruby code/proc_new_benchmark.rb
Rehearsal --------------------------------------------
&block 1.080000 0.160000 1.240000 ( 1.237644)
Proc.new 0.160000 0.000000 0.160000 ( 0.156077)
----------------------------------- total: 1.400000sec
user system total real
&block 1.090000 0.080000 1.170000 ( 1.178771)
Proc.new 0.160000 0.000000 0.160000 ( 0.155053)
Mấu chốt ở đây là khi sử dụng &block
thì sẽ luôn tạo ra object Proc
mới, ngay cả khi ta không cần dùng đến. Bằng cách sử dụng Proc.new
khi cần, ta có thể tránh việc khởi tạo toàn bộ các object.
Tuy nhiên, có thể bạn sẽ phải cân nhắc về code dễ đọc hay tốc độ. Như với method somtimes_block
cần truyền vào block
. Và nghiễm nhiên ta sẽ hiểu rằng block
đó được sử dụng để làm gì trong method đó. Nhưng với method sometimes_proc_new
thì rất dễ ngây hiểu nhầm
Kết luận
Vậy dùng cách nào tuỳ thuộc vào yêu cầu và mục đích của chúng ta. Đó cũng là 1 tính năng khá hữu ích.
Nguồn tham khảo http://mudge.name/2011/01/26/passing-blocks-in-ruby-without-block.html
All rights reserved