avatar

大兜

右手寫程式,左手寫音樂

Ruby Block, Proc and Lambda

Ruby 可將程式碼當參數傳遞,被參數化的程式碼稱為 Block。也就是呼叫方法時後面的 {|| } 符號,其中的 || 之間放置參數列宣告,若無參數則可省略。

Ruby 的 Proc 類似 ECMAScript 的 function。在 ECMAScript 中使用關鍵字 function 即可配置一個 Function 物件。Ruby 則使用 Kernel::proc、Kernel::lambda 方法(但兩者有些微差異),或是直接建構一個 Proc 物件(Proc.new)。

Block and Proc

Ruby 會主動將 Block 參數化成 Proc,Block 無法單獨存在,只能作為 Ruby 指令或呼叫方法時的引數。僅需利用流程指令 yield 即可將流程轉移到被參數化的 Block 中運行。我們可以用 Kernel::block_given? 判斷使用者有無傳遞 Block。

#  Compare with Block way and Proc way
# Block way
def f1(n)
  if block_given?
      yield n # yield to black
  else
      puts 'no block'
  end
end

# Proc way
def f2(n, &p)
  if block_given?
    p[n] # call proc p
    # 'p[n]' can be alternated with 'p.call(n)'
    # 'yield n' also works
  else
    puts 'no block'
  end
end

# Usage
f1('Hello'){|s| puts s}
f2(10){|n| n.times{puts 'a'}}

More about Proc

Block Way 無法得知被參數化的 Block,其 Proc 的指標。如果要取得該 Proc 的指標,需要在最後一個參數前面加上 ’&’,這東西只能有一個,且必須放在最後面,否則都會跳出 syntax error。

# Don't do this!
def f3(&p, n) #syntax error
  # ...
end

def f4(n, &p1, &p2) #syntax error
  # ...
end

Blockless

如果在呼叫方法時不想加上 Block,卻又想傳入一段程式碼區塊。

def f3(n, p)
  p[n] # call proc p
  # 'p[n]' is equivalent to 'p.call(n)'
  # 'yield n' will not work unless a block was given, but notice that the block has nothing to do with parameter 'p'
end
f3('Tony', Proc.new{|name| puts name}) # 'Proc.new' is equivalent to 'Kernel::proc'

建立一個 Proc 實體,並當參數傳入即可,但還是得在建立同時寫 Block 給 Proc.new 方法。這種寫法對於熟悉 ECMAScript 的人應該不陌生。

function f(n, p) { return p(n); }
f(10, function(n){print(n)});

上面介紹的方法乍看下很冗長又不好看,的確,如果只希望傳遞僅一個程式碼區塊作為參數,上述方法稍嫌麻煩,用 Proc Way 可以簡化很多,如果對取得 Proc 的指標不感興趣,用 Block Way 更加簡潔。

Passing More Than One Proc

但這種將 Proc 實體當參數傳遞的方法也不盡然無用武之地,因為 Ruby 在呼叫方法時止允許傳入一個Block,當想要傳入多的程式碼區段作為參數時,適用此技。

def f4(n, p1, p2)
  p1[n]
  p2.call n
end
f4('Hi', Proc.new{|s| puts "p1: #{s}"}, proc{|s| puts "p2: #{s}"})

The Ampersand

剛才介紹 & 的其中一個用法,那就是在方法宣告同時,指定從 Block 轉成 Proc 的變數名,除此 & 隨著使用地點不同,還可以把 Proc 轉成 Block:

f1("Hahaha", &proc{|s| puts s})

proc 會回傳一個 Proc 實體,當 Proc 碰到 & 之後,會轉換成 Block,所以以上的示範意義與下相同:

f1("Hahaha"){|s| puts s}

另外還有一個妙用,如果我們想把一串單字轉換成大寫,如下:

words = %w(Jane, aara, multiko)
upcase_words = words.map {|x| x.upcase}
p upcase_words

這看起來相當簡潔,但其實可以更簡潔:

words = %w(Jane, aara, multiko)
upcase_words = words.map(&:upcase) # alternate this line
p upcase_words

原理是因為 Ruby 可以用物件的方法名去參考到該方法(反射),舉個例子:

# those two lines are equivalent
"tonytonyjan".upcase
"tonytonyjan".send(:upcase)

所以當我們寫出 map(&:upcase) 這樣的語法時,他會將傳出的物件的方法化為 Proc 並執行,所以與 map{|x| x.upcase} 意義相同。

如果想調用的方法需要參數的話,則在宣告方法的同時要動點手腳,舉個例子:

def f5(n, m)
  yield n,m # yield to black
end

class A
  def f7 s
    puts "A.f7 says #{s}"
  end
end

f5(A.new,"The World!", &:f7)

&:f7 會去找第一個接到的參數,並呼叫 f7,及 f5 中的 n,而在那之後所有擲出的參數,都被當成 f7 的參數。所以 f5(A.new,"The World!", &:f7) 的真正意思是「呼叫物件 A 的 f7 方法,並傳入 "The World!" 當參數」。

Proc and Lambda

在 Wiki 中找到 Closure 的資料,其中有的一段 Ruby 程式碼清楚闡述了 Proc 和 Lambda的差別:

# Compare Proc with Lambda
def foo
  f = Proc.new { return "return from foo from inside proc" }
  f.call # control leaves foo here
  return "return from foo"
end

def bar
  f = lambda { return "return from lambda" }
  f.call # control does not leave bar here
  return "return from bar"
end

puts foo # prints "return from foo from inside proc"
puts bar # prints "return from bar"

除此之外,他們一樣。

Reference