程式區塊與 Proc


迭代器與程式區塊 中談過,每次呼叫方法時,其實會涉及四個部份:
  • 訊息接收者
  • . 運算
  • 訊息名稱
  • 程式區塊

如果你在方法最後一個參數設定&開頭的參數,則會使用區塊建立Proc物件傳入,Proc有個call方法,用以執行Proc物件內含的程序。例如:
>> def foreach(arr, &block)
>>     arr.each do |element|
?>         block.call(element)
>>     end
>> end
=> nil
>> foreach([1, 2, 3]) do |element|
?>     puts element
>> end
1
2
3
=> [1, 2, 3]
>>


你可以使用Proc.new建立程序物件。例如:
>> p = Proc.new { |param| puts param }
=> #<Proc:0x263bf80@(irb):11>
>> p.call(123)
123
=> nil
>>


Proc.new會使用指定的程式區塊建立物件,呼叫call時,就是執行程式區塊指定的程式流程。要注意的是,Proc物件是Proc物件,程式區塊是程式區塊,兩者根本上不同,程式區塊是定義方法時語法的一部份,呼叫方法時指定程式區塊,如果方法最後有個&參數,Ruby直譯器會使用方法上指定的程式區塊來建立Proc物件。例如:
foreach([1, 2, 3]) { |element| puts element }

要用程式流程來示意的話,Ruby會使用以下建立Proc物件:
p = Proc.new { |element| puts element }

再呼叫方法:
foreach([1, 2, 3], &p)

注意最後那個&,這表示將p傳給方法最後一個&參數,少了那個&,那麼p就只會是一般的方法呼叫引數。例如:

>> p = Proc.new { puts "XD" }
=> #<Proc:0x25c5e88@(irb):1>
>> def some(param, &p)
>>     puts param
>>     p.call
>> end
=> nil
>> def other(param, p)
>>     puts param
>>     p.call
>> end
=> nil
>> some(10, &p)
10
XD
=> nil
>> other(20, p)

20
XD
=> nil
>> some(10, p)

ArgumentError: wrong number of arguments (2 for 1)
        from (irb):2:in `some'
        from (irb):12
        from C:/Winware/Ruby192/bin/irb:12:in `<main>'
>> other(20, &p)
ArgumentError: wrong number of arguments (1 for 2)
        from (irb):6:in `other'
        from (irb):13
        from C:/Winware/Ruby192/bin/irb:12:in `<main>'
>>


所以,任何可以接受程式區塊的方法,如果想要自行建立Proc物件傳入,都要加上個&。例如你有個想重用的程序,則可以使用Proc而不是程式區塊:
>> puts_proc = Proc.new { |element| puts element }
=> #<Proc:0x2666388@(irb):14>
>> [1, 2, 3].each(&puts_proc)
1
2
3
=> [1, 2, 3]
>> "abc".each_char(&puts_proc)
a
b
c
=> "abc"
>>


不過如下指定就錯了,因為方法最後一個參數不知道該使用傳入的Proc,還是捕捉程式區塊而建立的Proc:
>> [1, 2, 3].each(&puts_proc) { |element| print element }
SyntaxError: (irb):17: both block arg and actual block given
        from C:/Winware/Ruby192/bin/irb:12:in `<main>'
>>


實際上,&會觸發物件的to_proc方法,並嘗試指定給&變數,你可以在任何物件上定義to_proc方法,然後使用&來觸發to_proc方法。例如:
class Ball
attr_reader :radius

def initialize(radius)
@radius = radius
end

def self.to_proc
Proc.new { |ball| ball.radius }
end
end

# 收集球的半徑
print [Ball.new(10), Ball.new(20), Ball.new(30)].collect(&Ball) # [10, 20, 30]

例如Symbol上就定義有to_proc方法,若有個程式是如下:
>> ["justin", "monica"].each { |name| name.capitalize! }
=> ["Justin", "Monica"]
>>


則可以改用以下:
>> :capitalize.to_proc.call("orz")
=> "Orz"
>> ["justin", "monica"].each(&:capitalize!)
=> ["Justin", "Monica"]
>>


有些方法可以直接傳入Symbol的,也是類似的道理。例如陣列的reduce方法,為了方便,甚至設計為可省略&:
>> [1, 2, 3].reduce { |sum, element| sum += element }
=> 6
>> [1, 2, 3].reduce(&:+)
=> 6
>> [1, 2, 3].reduce(:+)
=> 6
>>


實際上,Symbol的設計大致是:
class Symbol
    def to_proc
        Proc.new { |o| o.send(self) }
    end
end

因此總能找出正確的回應方法來執行。

Proc的call方法可以接受任意引數,不過實際上你可以取得幾個引數,在於你定義了幾個區塊參數。例如:
>> p = Proc.new { |a, b| puts a, b }
=> #<Proc:0x29e4790@(irb):16>
>> p.call(1)
1

=> nil
>> p.call(1, 2)
1
2
=> nil
>> p.call(1, 2, 3)
1
2
=> nil
>>


Proc正如其名,是一小段程序,一小段流程,要注意若在建立Proc時的程式區塊return時的狀況。例如:
>> def some
>>     puts "some 1"
>>     p = Proc.new { puts "執行 Proc"; return 1 }
>>     p.call
>>     puts "some 2"
>> end
=> nil
>> some
some 1
執行 Proc
=> nil
>>


注意到並沒有顯示"some 2",因為上例相當於:
def some
    puts "some 1"
    puts "執行 Proc"
    return
    puts "some 2"
end

上例中,Proc是在some的作用範圍中建立,如果Proc沒有在作用範圍中建立,建立Proc時的程式區塊中若有return,則會引發LocalJumpError
>> p = Proc.new { puts "執行 Proc"; return 1 }
=> #<Proc:0x6245e8@(irb):20>
>> def some(proc)
>>     puts "some 1"
>>     proc.call
>>     puts "some 2"
>> end
=> nil
>> some(p)

some 1
執行 Proc
LocalJumpError: unexpected return
        from (irb):20:in `block in irb_binding'
        from (irb):23:in `call'
        from (irb):23:in `some'
        from (irb):26
        from C:/Winware/Ruby192/bin/irb:12:in `<main>'
>>


因為設計API時,並不希望有return中斷了原本API的執行流程,因此Ruby執行時如果看到return,就會視為錯誤,即使return的目的是正常結束並傳回值,如果你確實是想傳回值,可以不撰寫return,因為Ruby執行流程中最後一個物件就會被當作傳回值。例如:
>> def some(proc)
>>     puts "some 1"
>>     proc.call
>>     puts "some 2"
>> end
=> nil
>> some(Proc.new { puts "執行 Proc"; 1 })
some 1
執行 Proc
some 2
=> nil
>>


因為Proc像是個執行流程而不是方法,除了要注意return之外,迭代器與程式區塊 中也提到,要注意程式區塊中撰寫了break、next或redo的結果。