跳至主要内容

[Ruby] block, Proc 和 Lambda

keywords: block, yield, Proc, lambda

在 Ruby 裡,幾乎什麼東西都是物件,但其實還是有少數的例外,Block 就不是物件。也因此 Block 沒有辦法單獨的存在,也沒辦法把它指定給某個變數,像這樣的寫法都會造成語法錯誤(Syntax Error):

{ puts "Hello, Ruby" }            # 這樣會產生語法錯誤
action = { puts "Hello, Ruby" } # 這樣也會產生語法錯誤

Block

建立 Block

# 第一種方式,用 do...end,通常用在多行的 block
do |token|
# ...blockCode...
end

# 第二種方式,直接用 { |token| } 表示,通常用在單行的 block
{ |token| ... }

在 methods 中執行 block

如果想要讓附掛在 methods 後的 Block 執行,可使用 yield 方法,暫時把控制權交棒給 Block,等 Block 執行結束後再把控制權交回來:

# 透過 yield 掛上 block

def say_hello
puts "開始"
yield # 把控制權暫時讓給附掛在 say_hello 後的 Block
puts "結束"
end

say_hello {
puts "這裡是 Block"
}

=begin
"開始"
"這裡是 Block"
"結束"
=end

使用 pipe 在 block 中代入參數(token)

Block 本質上是匿名函式,我們也可以在 Block 中代入參數,而 Block 中的 │ (pipe)就是這個匿名函式的參數,稱做 token,token 是在這個 Block 裡專屬的區域變數,Block 執行結束後就會失效了:

# pipe 中的變數是區域變數,離開 block 就找不到

5.times do |i|
puts i # 這個變數 i 只有在 Block 裡有效
end

puts i # NameError,離開 Block 之後就失效

其他使用 pipe 代入 token 的例子:

# 使用 pipe 的例子 1
def say_hello
puts "開始"
yield 123 # 把控制權暫時讓給 Block, 並且傳數字 123 給 Block
puts "結束"
end

say_hello do | x | # 這個 x 是來自 yield 方法,把 123 代入 |x|
puts "這裡是 Block,我收到了 #{x}"
end
# 使用 pipe 的例子 2
def this_method_takes_a_block
yield(5)
end

this_method_takes_a_block do |num| # 把 5 代入 |num|
puts num # 回傳 5
end
# 使用 pipe 的例子 3
def this_silly_method_too(num)
yield(num + 5)
end

this_silly_method_too(3) do |wtf| # 把 3 + 5 代入 |wtf|
puts wtf + 1 # 回傳 9
end

Block 的回傳值

yield 方法除了把控制權暫時的讓給後面的 Block 之外,Block 最後一行的執行結果也會自動變成 Block 的回傳值,所以可把 Block 當做判斷內容。

# 利用 Block 判斷元素內容(類似 Array.select{|i|})

def pick(list)
result = []

list.each do |i|
result << i if yield(i) # 如果 yield 的回傳值是 true 的話,則將 block 的回傳值推入 result
end

result
end

# 這裡會把 pick() 中 i 的值代入 yield(i) 中,進而代入 pick 所接的 block |x| 中,
# 而 block 最後一行執行的結果,作為 yield(i) 的回傳值。

p pick([ *1. .10]) { | x | x % 2 == 0 } # => [2, 4, 6, 8, 10]
p pick([ *1. .10]) { | x | x < 5 } # => [1, 2, 3, 4]
# 利用 Block 寫出 times(類似 Number.times{|i|})
def times_pj(time)
i = 0
while i < time
yield(i)
i += 1
end
end

times_pj(5){|x| puts "This is my timer #{x}" }
信息

簡單來說,method 中的代入 yield 的參數,會被代入 block 的 token ,然後 block 最後一行執行的結果,會再回傳給 yield。

Proc

Block 本身並不是物件,它沒辦法單獨的存在 Ruby 的世界裡,需要依附在方法或物件後面。但是透過 Proc 我們可以把 block 物件化,讓我們可以使用任何物件可以使用的方法;另外,我們也可以避免重複撰寫功能類似的 Block。

透過 Proc 將 block 物件化

我們可以使用 Proc.new <BLOCK> 將 block 保存成物件:

# variable = Proc.new <BLOCK>
greeting = Proc.new { puts "哈囉,世界" } # 使用 Proc 類別可把 Block 物件化
hello_world = Proc.new do
puts "Hello, World!"
end
# Proc 中也可以代入 token
# varibale = Proc.new { |token| }
say_hello_to = Proc.new { |name| puts "你好,#{name}"}

say_hello_to = Proc.new do |name|
puts "Hello, #{name}"
end

在函式中代入 Proc

透過 Proc.new 可以將 block 物件化,但當我們要附掛到 method 後時,要再轉回 block 使用,這時使用關鍵字 &

# 把 Proc 變回 block 代入方法中(使用&)

arr = [1, 2, 3, 4, 5, 6]
multiple_by_2 = Proc.new {|i| i * 2}
arr_mulitple_by_2 = arr.collect(&multiple_by_2) # [2, 4, 6, 8, 10, 12]

直接執行 Proc

我們也可以直接執行透過 Proc 物件化後的 block:

# 直接呼叫 Proc 的方式
say_hello_to.call("Aaron") # 使用 call 方法
say_hello_to.("Aaron") # 使用小括號(注意,有多一個小數點)
say_hello_to["Aaron"] # 使用中括號
say_hello_to === "Aaron" # 使用三個等號
say_hello_to.yield "Aaron" # 使用 yield 方法

lambda

除了 Proc 之外,我們也可以透過 lambda 來將 block 物件化,用法和 Proc 幾乎一模一樣:

建立 lambda

使用關鍵字 lambda

succ_lambda = lambda {|x| x + 2}
succ_lambda.call(3) # 5

使用 ->

succ_arrow = ->(x){ x + 2 }
succ_arrow.call(3) # 5

lambda 和 Proc 的差異

lambdaProc 幾乎一模一樣,除了:

  1. lambda 會檢查代入的參數數目,Proc 不會。因此當丟入的參數數目不對時,lambda 會丟出錯誤,而 proc 只會忽略未預期的參數並代入 nil。
say_hello_lambda = lambda { |first_name, last_name| puts "Hello #{first_name}, #{last_name}."}
say_hello_proc = Proc.new { |first_name, last_name| puts "Hello #{first_name}, #{last_name}."}

say_hello_lambda.call('Aaron') # wrong number of arguments (given 1, expected 2)
say_hello_proc.call('Aaron') # Hello Aaron, .
  1. 當 lambda 內執行到 return 後,它會將控制權交回呼叫它的方法,繼續執行該方法後的片段;但當 Proc 內使用到 return 時,不會回到呼叫它的方法,而是立即跳出該方法:
def batman_ironman_proc
victor = Proc.new { return "Batman will win!" }
victor.call
"Iron Man will win!"
end

puts batman_ironman_proc # Batman will win!

def batman_ironman_lambda
victor = lambda { return "Batman will win!" }
victor.call
"Iron Man will win!"
end

puts batman_ironman_lambda # Iron Man will win!

其他補充

Block 不是參數

Block 通常得像寄生蟲一樣依附或寄生在其它的方法或物件(或是使用某些類別把它物件化),但它不是參數。在下面這段範例中,name 才是參數,但 Block 不是。上面這段程式碼執行之後不會有任何錯誤,但 Block 裡要執行的動作也不會執行:

def say_hello_to(name)
# do something here
end

say_hello_to("悟空") { puts "這裡是 Block" }

參考資料