close

From: http://railsfun.tw/t/method-block-yield-proc-lambda/110

-------------------------------------------------------

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 程式設計這類的方式勢必要避免,不過這就是易用與效能的取決點就是了

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 Foxbrush 的頭像
    Foxbrush

    Foxbrush

    Foxbrush 發表在 痞客邦 留言(0) 人氣()