method / block / yield / Proc / lambda 全面解釋


#1

hmm…這篇大概是這邊偏難但最重要的一篇文章,也就是 method / block / Proc ( Procedures ) / lambda 的解釋,這邊不會說 class methods & instance methods 的分別,單就定義與使用而言,& 這篇文章不會預設你已經上過課的立場,不過希望有一些些Ruby的底子,&這邊有點苦悶,不過請耐心看完,&不要只看,至少複製貼上 demo 玩看看,文內所有#開頭的全為註解

我之前也已經有寫一篇關於 block & yield 的文章,可以參考這邊

okay,首先,類型定義,大概先歸類一下

( block.call | yield ) << ( block = ( Proc | lambda ) ~= method )

hmm,一項一項來,首先,基本的 methods 的定義,還有 Proc & lambda 的定義

def temp1( a , b = 123 , *argv , &block )
  #...a = 必要值,b = 預設值,*argv = 多出來的傳值,&block 送一個 block 進來
end

temp2 = Proc.new do | a , b = 123 , *argv , &block|
  #...
end
#=> #<Proc:0x000001021b9ab0@(irb):28>

temp3 = lambda do | a , b = 123 , *argv , &block|
  #...
end
#=> #<Proc:0x00000102aae260@(irb):29 (lambda)>

首先,這三段語法基本上等價,也就是 method / Proc / lambda 的定義,然後,後兩者有縮寫可以寫,類似寫成以下

temp2 = Proc.new { | a , b = 123 , *argv , &block|
  #...
}

temp3 = lambda { | a , b = 123 , *argv , &block|
  #...
}

okay,這邊注意一下,temp1是method name,temp2 & 3是變數名稱,你不能修改method name,不然它通常會直接執行掉,然後把結果值吐給你,而變數就有趣了,你可以把該匿名函數丟給其他人,而以下將略過傳入值,上面只是表現出,method / Proc / lambda 可以定義相同的傳入值,而以下是使用的 demo 類似

def temp1
   puts "im_temp1"
end
temp2 = Proc.new { puts "im_temp2" }
temp3 = lambda { puts "im_temp3" }

temp1 #=> "im_temp1"
temp2.call #=> "im_temp2"
temp3.call #=> "im_temp3"

tempA1 = temp1 #=> nil , temp1執行結束,印出 , #=> "im_temp1"
tempA2 = temp2 #=> #<Proc:0x000001022b40a0@(irb):42>
tempA3 = temp3 #=> #<Proc:0x00000102289d28@(irb):43 (lambda)>

tempA1.call #=> ERROR undefined method `call' for nil:NilClass
tempA2.call #=> "im_temp2"
tempA3.call #=> "im_temp3"

你會發覺 method 被定義後,基本上沒辦法丟給另外一個變數( 只可以 alias 成另外一個名稱[此處無demo] ),用了它就會被執行,而temp2 & 3真正的名稱叫做匿名函數,就是一段尚未被執行的暫存的程式碼,你可以丟到任何地方被執行就是,包括丟給別的 method,接下來做一個簡單的 callblock 的範例

def temp_b1( &block )
  block.call("im_tempB1")
end
def temp_b2
  yield("im_tempB2") #小刮號可省略
end

block1 = lambda{|x| puts "block1 #{x}"}
block2 = Proc.new{|x| puts "block2 #{x}"}

temp_b1 do |x|
  puts "block0 #{x}"
end
#=> "block0 im_tempB1"

temp_b1(&block1)
#=> "block1 im_tempB1"

temp_b2(&block2)
#=> "block2 im_tempB2"

這邊要慢慢解釋 … temp_b1 可以接受一個 block ,也就是接受一個 Proc 或 lambda 的程式進來,然後執行它,temp_b2 是 temp_b1 的縮寫,直接用 yield 去執行 block,兩段語法完全等價

後面宣告的兩個 block_x,然後再下去有三段語法,第一個是最正規的 callblock 的方式,最後面兩個語法是把我們宣告過的 block_x 丟到 method 內去,而temp_b1 do |x|這段其實等同於temp_b2(&block2)這段的"縮寫",單純不用多個變數名稱 block_x 然後直接傳進去而已

然而為何說是縮寫,測試一下就知道

def temp_c(&block)
  puts block
end
temp_c do ; end #=> #<Proc:0x000001028d4750@(irb):100>

temp_c( &Proc.new do ; end ) #=> #<Proc:0x000001028acbd8@(irb):102>

#以下不一樣,&省略小刮號
temp_c &lambda{} #=> #<Proc:0x00000102838710@(irb):104 (lambda)>

okay可以看得出來原形是Proc而非lambda,上面已經有 demo 就是&的用法,&這個補綴字元很簡單,lambda | Proc展開,丟到 method 定義中的 &block 或是 yield 中去而已,所以做個總結

  1. method = 最基本且定義後不能變更的程式
  2. &block & .call & yield = 傳入與執行的動作
  3. yield = 方便執行block的方式,語句同等於省略&block輸入 & block.call
  4. block = 兩種 Proc | lambda
  5. Proc ~= lambda = 一段程式,可以丟給別人再執行

anyway 上面的東西請多看幾次多玩幾次,&請先把上面的概念搞懂再繼續往下看,接下來示範 Proc 和 lambda 有何不同(上面的說明應該把所有的東西過濾到剩下兩種東西了,method 除外)

Proc 和 lambda 最大的不同點應該就兩項,一個是傳入值,一個是return

首先,傳入值:

block_s1 = lambda{|x,y| puts [x.class,y.class]}
block_s2 = Proc.new{|x,y| puts [x.class,y.class]}

block_s1.call(1,2,3,4,5) #=> ERROR : ArgumentError: wrong number of arguments (5 for 2)
block_s1.call(1,2) #=> OK : Fixnum \n Fixnum
block_s1.call() #=> ERROR : ArgumentError: wrong number of arguments (0 for 2)

block_s2.call(1,2,3,4,5) #=> OK : Fixnum \n Fixnum
block_s2.call(1,2) #=> OK : Fixnum \n Fixnum
block_s2.call() #=> OK : NilClass \n NilClass

okay,你會發覺 Proc 整個亂丟亂傳都會過,但 lambda 就必須是該數量才會過,而另外一個是return

  1. Proc = 寫作地的 method 的 return
  2. lambda = 執行地的 return

以下是demo code

def temp_d1(&block)
  puts "temp_d1===start"
  puts "temp_d1===got : #{block.call("yoo_temp_d1")}"
  puts "temp_d1===end"
end
def temp_d2
  puts "temp_d2[[GOGOGO!!]]"

  puts "temp_d2[[lamb no return]]"
  lam_1 = lambda do |x|
    "**lamb no return (#{x})**"
  end
  temp_d1 &lam_1

  puts "temp_d2[[proc no return]]"
  proc_1 = Proc.new do |x|
    "**proc no return (#{x})**"
  end
  temp_d1 &proc_1

  puts "temp_d2[[lamb has return]]"
  lam_2 = lambda do |x|
    puts "<<lamb return : start (#{x})>>"
    return "**lamb has return**"
  end
  temp_d1 &lam_2

  puts "temp_d2[[proc has return]]"
  proc_1 = Proc.new do |x|
    puts "<<proc return : start (#{x})>>"
    return "**proc has return**"
  end
  temp_d1 &proc_1

  puts "temp_d2[[BYEBYE!!]]"
end

#執行
temp_d2

執行結果

temp_d2[[GOGOGO!!]]
temp_d2[[lamb no return]]
temp_d1===start
temp_d1===got : **lamb no return (yoo_temp_d1)**
temp_d1===end
temp_d2[[proc no return]]
temp_d1===start
temp_d1===got : **proc no return (yoo_temp_d1)**
temp_d1===end
temp_d2[[lamb has return]]
temp_d1===start
<<lamb return : start (yoo_temp_d1)>>
temp_d1===got : **lamb has return**
temp_d1===end
temp_d2[[proc has return]]
temp_d1===start
<<proc return : start (yoo_temp_d1)>>
 => "**proc has return**"

okay,這邊很長請自己對照,簡單的來說如果沒有 return 這個關鍵字,lambda 和 Proc 的表現一樣,而如果有return,lambda 會回傳到當初 call 的地方,而 Proc 是從寫作 return 的地方直接 return 掉(temp_d1 來不及說temp_d1===end,而 temp_d2 連說 BYEBYE 的機會都沒有 ),且不會回到當初 call 的地方

最後,你應該看得懂,為何這個會成功了 & 為何那麼簡單

def temp_final
  10.times do |i|
    return i if i == 5
  end
end
#=> 5

所以這個會生一個 Proc 去 times 這個 method 內執行,然後 return 時直接從temp_final return 回去

&這就是 Ruby 被人詬病慢的地方…太多的 Proc 包裝有的沒的(看起來像是 for / while 迴圈,但其實是生出一個 Proc 的物件,並執行了 N 次)…雖然很好用,但…效能高不起來就是,而高效的 Ruby 程式設計這類的方式勢必要避免,不過這就是易用與效能的取決點就是了


對yield的疑問
Ruby / Rails meta programming 的實現
請教幾個ruby/rails/js的小問題
該怎麼判斷Form Tag 是否為空白?
多值傳入與取得( Array的傳值展開與包裝 , *argv )
Grape 如何設定共用的 headers?
Rails extend engine routes
對yield的疑問
#2

Dear JC,

哇又來了~~!!

最近又試了這段程式碼~~

再請教一下…

這個 return 是什意思?..

是指一般像 function (PHP)的回傳值嗎?

那這 Proc, lambda 的 return 怎運用呢~~

謝大魔王~


#3

簡單舉例

def a
    func = Proc.new { return "proc" }
    func.call
    return "def"
end

def b
    func = lambda { return "lambda" }
    func.call
    return "def"
end

a # "proc"
b # "def"

不管 lambda 怎麼 return 都不會影響後續,但是 Proc 會搶過來 return 整個 function


#4

另外寫個遞迴類的demo給你看,東西同david的範例 & 感謝david

temp_proc = Proc.new do |x = 0|
  if x < 3
    puts "call start #{x}"
    y = temp_proc.call(x + 1)
    puts "call end #{x}"
    return y
  else
    puts "return from #{x}"
    return x
  end
end
temp_proc.call

回傳

call start 0
call start 1
call start 2
return from 3
#LocalJumpError: unexpected return #多層跳脫錯誤......
# => 3

主因是Proc於Proc的block內call return時,會不知道該往上return多少層才可以…所以會噴錯,這時可以有兩種作法,如前述所言,要就不加return或是改用lambda

所以修改成Proc版

temp_proc = Proc.new do |x = 0|
  if x < 3
    puts "call start #{x}"
    y = temp_proc.call(x + 1) # y暫存結果
    puts "call end #{x}"
    y #當return用,&不這樣會寫return puts的回傳結果 : nil
  else
    puts "return from #{x}"
    x #當return用
  end
end
temp_proc.call

與lambda版(與最上面原程式相比,只有第一行換成lambda)

temp_proc = lambda do |x = 0|
  if x < 3
    puts "call start #{x}"
    y = temp_proc.call(x + 1)
    puts "call end #{x}"
    return y
  else
    puts "return from #{x}"
    return x
  end
end
temp_proc.call

執行結果都相同

call start 0
call start 1
call start 2
return from 3
call end 2
call end 1
call end 0
# => 3

anyway這是 ( Proc & lambda ) + return 的概念上的問題,弄懂了應該就很好操控之類的 :slight_smile:


#5

@JokerCatz 你這裡沒改……

與lambda版(與最上面原程式相比,只有第一行換成lambda)
程式碼恕刪


#6

fixed


#7

續此範例,如果我把Proc拿出來

a = Proc.new {return i if i == 5}
def temp_final
  10.times &a
end

會出現

NameError: undefined local variable or method xxx

我發現如果在外部定義變數,進到函數中也一樣看不見

這跟一般的程式語言一樣,但是一旦這樣,若我在某函數中想用Proc or lambda,不就都得把他們的實例寫進函數中,不能從函數外部引用了?這樣代碼不能重複使用,寫成Proc有什麼好處?


#8

你 … 寫錯了 … 你沒把 a 傳進 method 的啊

a是區域變數(local variable),不是 @a (instance variable) 或是 @@a (class variable) 也不是 $a (global variable),所以不會無中生有跑到你的 temp_final 這個 method 內,因為根本就沒傳進去,寫執行一個 Proc 是要用類似 a.call 而不是用外部傳到內部的 &a 的方式 ( 必須寫在 def method name 後面 )

hmmm~ 我從頭寫一個等效 each 的寫法好了,來比較 Proc 和 lambda 的差異,類似

class Array
  def fake_each &block
    puts block.inspect
    for i in self do
      block.call(i)
    end
    return -99
  end
end

def test_proc
  ans = [1,3,2,4].fake_each do |i|  #do ... end 預設是 Proc
    return i if i == 2
  end
  return ans
end
test_proc()
"#<Proc:0x007fdbf9f4a6f0>" ; #=> 2

def test_lambda
  ans = [1,3,2,4].fake_each(&lambda{|i|  #改換用lambda
    return i if i == 2
  })
  return ans
end
test_lambda()
"#<Proc:0x007fdbf9db9868 (lambda)>" #=> -99

anyway 有點長,Ruby 預設是 Proc 而非 lambda,好處是隨時 return,所以 Ruby 下大約 99% 全部都是 Proc.new 的方是就是,而非 lambda,& 可以的話先練好你的變數傳遞的方式,這邊算是高級議題,請不要在這打轉,基本功先練好,這裡的東西就大概放在心底,繼續玩下去總有一天你會開悟的


#9

了解了~感謝JC!