Truyền block vào trong Ruby mà không thông qua cách &block
Ruby
106
White

Lơi Rệ viết ngày 23/05/2015

Chú thích: Bản dịch tiếng Việt của bài viết Passing Blocks in Ruby Without &block của Paul Mucur

Có 2 cách để nhận vào block trong một hàm của Ruby. Cách đầu tiên là sử dụng từ khoá yield như sau:

def speak
  puts yield
end

speak { "Hello" }
# Hello
#  => nil

Cách khác là 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 hay ở trang

).

Chúng 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)

Điều đó chứng ta là chúng ta nên chọn yield thay vi &block, nhưng nếu chúng ta cần truyền một block qua một hàm khác thì sao?

Ví dụ, ở đây là một class với một hàm tell_ape giao việc cho một hàm khác có tên tell. Kiểu pattern thường được xử lý bằng method_missing nhưng tôi sẽ giữ và khai báo toàn bộ các hàm để dễ dàng 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

Đấy là một điều không thể làm với từ khoá 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ũng không thể chạy với cách &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

Nhưng có một cách để tạo một đối tượng 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 với không có argument nào của chính
nó, nó sẽ trả về một Proc có kèm block được đưa cho hàm ở ngoài.

def speak
  puts Proc.new.call
end

speak { "Hello" }
# Hello
#  => nil

Điều này có nghĩa là có thể truyền vào một block giữa các hàm với nhau mà không cần phải sử dụng &block nữa:

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 bạn dùng Proc.new, bạn sẽ bị mất tốc độ của cách 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 bạn không cần đến chúng. Tôi có thể chứng thực điều vừa nói thông qua benchmark 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

Kết quả là khác biệt rất lớn về tốc độ:

$ 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 đối tượng Proc mới, nay cả khi chúng ta không cần dùng đến. Bằng cách sử dụng Proc.new khi chúng ta cần đến, chúng ta có thể tránh trả giá về tốc độ của việc khởi tạo toàn bộ các đối tượng.

Tuy thế, có thể bạn sẽ gặp một số vấn đề về phần code dễ đọc vs tốc độ, điều đó được thấy rõ từ hàm somtimes_block chỉ nhận block và do đó được mặc định hiểu sẽ phải làm cái gì đó với cái block đó, nhưng không thể đưa ra cùng nhận định vào hàm tôi ưu sometimes_proc_new.

Kết luận là nó tuỳ thuộc vào yêu cầu, nhưng cũng khá thú vị khi biết được chiêu hữu dụng này của Ruby.

Nguồn tham khảo

  1. Data Structures as Functions (or, Implementing Set#to_proc and Hash#to_proc in Ruby)
  2. A Summit for Scaling
  3. Implementing if in Ruby
Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Lơi Rệ

43 bài viết.
206 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
65 12
Sự sống còn của các công ty kỹ thuật phụ thuộc vào nguồn nhân lực chất xám của họ. Thế nên rất thiết yếu cho việc đầu tư xây dựng team có khả năng ...
Lơi Rệ viết gần 2 năm trước
65 12
White
38 7
Trời se se lạnh, Melbourne chuyển mùa, ngồi trong quán cafe bắt đầu một ngày làm việc mới với suy nghĩ tại sao các bạn Việt Nam không muốn tham gia...
Lơi Rệ viết gần 3 năm trước
38 7
White
36 15
Thế nào là làm việc từ xa? Internet, một trong những phát minh vĩ đại nhất của con người thế kỷ 20. Công nghệ này xoả bỏ rào cản vật lý giữa các n...
Lơi Rệ viết 2 năm trước
36 15
Bài viết liên quan
White
8 6
Chưa xem phần 2? Xem (Link) Trong bài viết này tôi giới thiệu cho các bạn về khái niệm function arity, một cách gọi mĩ miều của số lượng argument ...
Lơi Rệ viết hơn 2 năm trước
8 6
White
8 1
Tiếp theo (Link) Mình sẽ hướng dẫn cách test căn bản cho API mình tạo. Thật ra mà nói thì mình phải viết test trước khi làm nhưng mà để tránh việc...
My Mai viết hơn 2 năm trước
8 1
White
4 2
__Chú thích__: Đây là bản dịch tiếng Việt của bài viết gốc của tôi. Nếu bạn muốn xem bản tiếng Anh, xin hãy trỏ tới URL (Link) Lời mở (Link) là ...
Lơi Rệ viết gần 3 năm trước
4 2
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
43 bài viết.
206 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!