Rubyのブロックはメソッドに対するメソッドのMix-inだ!

Yugui著「初めてのRuby」を読んでいる
7章メソッドにはブロック付きメソッドのことが
丁寧に記述されていて大変参考になる


Rubyのブロックは分かったようで分からない代物だ
スーパーマリオブラザーズのように
間口が広くて奥が深い
少し真剣に向き合ってみた
完全理解にはほど遠いけれども
頭を整理するために
今の理解を書いてみることにする

引数付きメソッド

Ruby空間における操作対象はオブジェクトである
オブジェクトは外からのメッセージを受け取ると
その中の対応するメソッドを起動して
そこに書かれている手続きを実行する

  "Charlie".length  # => 7が返る

文字列オブジェクト"Charlie"に
メッセージlengthが送られると
対応するlengthメソッドが起動され
7が返される


メソッドは引数を取ることができる
メッセージのカッコ内に収められたオブジェクトは
メソッドに渡されその中で他のオブジェクトと協同することになる


数字、文字列、配列、ハッシュ、範囲などはオブジェクトだから
それを受ける用意があるのなら
当然メソッドに渡すことができる

  class String
    def speak(word)
      case word
      when Integer
        word.times { print self, " " }
      else
        word.each { |item| print item, "-#{self} " }
      end
    end
  end

"moo".speak(5)           # => moo moo moo moo moo 

"moo".speak("hello!")    # => hello!-moo

"moo".speak([1, 2, 3])   # => 1-moo 2-moo 3-moo

"moo".speak({:love => 'lettuce',:hate => 'cucumber'})
             # => lovelettuce-moo hatecucumber-moo 

"moo".speak('a'..'h') 
      # => a-moo b-moo c-moo d-moo e-moo f-moo g-moo h-moo


オブジェクトには変数という名札を付けられるので
変数を付けてメソッドに渡すこともできる

n = 5
s = "hello!"
a = [1, 2, 3]
h = {:love => 'lettuce',:hate => 'cucumber'}
r = 'a'..'h'

"moo".speak(n)
"moo".speak(s)
"moo".speak(a)
"moo".speak(h)
"moo".speak(r)   # 結果は同じ

ブロック

Rubyの構成要素の中に
ブレース { } または do end で表現される
ブロックというものがある
ブロックは一連の手続きをその間に封入する
封入された一連の手続きは一塊のモジュールになる

  { "block me!" * 3 }

  do
    a = [1,2,3]
    s = "block me!"
    a << s
  end

一連の手続きもモジュールになれば他のオブジェクトと同様に
メソッドに渡すことができそうだけれども
ブロックはオブジェクトではないので
それはできない

  "moo".speak({ "block me!" * 3 })
                # => エラー(odd number list for Hash)

  "moo".speak(do
    a = [1,2,3]
    s = "block me!"
    a << s
  end)                  # => エラー(syntax error)


変数に代入することもできない

  b1 = { "block me!" * 3 }  # => エラー(odd number list for Hash)

  b2 = do
    a = [1,2,3]
    s = "block me!"
    a << s
  end                  # => エラー(syntax error)
 
  "moo".speak(b1)
  "moo".speak(b2)

手続きオブジェクト

それならばブロックをオブジェクト化すればいい
ブロックの前にlambda(λ:ラムダ)(Proc.new、procでもよい)を付けると
ブロックは手続きオブジェクト(Proc)になる
オブジェクトになれば引数としてメソッドに渡せるし
変数への代入もできる
手続きオブジェクトになれば
メソッドと離れて存在することもできるようになる

  class String
    def speak(word)
      word.call.each { |item| print item, "-#{self} " }
               # Procオブジェクトをcallする
    end
  end

  b1 = lambda { "block me!" * 3 }

  b2 = lambda do
    a = [1,2,3]
    s = "block me!"
    a << s
  end

  "moo".speak(b1)      # => block me!block me!block me!-moo

  "moo".speak(b2)      # => 1-moo 2-moo 3-moo block me!-moo

Procオブジェクトを受け取ったメソッド内で
callメソッドを呼べば(Ruby1.9ではyieldメソッドも使える)
渡されたブロック内の手続きが実行される


ブロックがオブジェクトになった以上
他のオブジェクトと同様に
機能的にはメソッドに複数渡せるし
配列などに入れてから渡すこともできる


lambdaを使ってメッセージ送信側で
ブロックをオブジェクト化するのに代えて
メッセージ受信側でオブジェクト化する方法もある
メソッド仮引数にアンパサンド & をプリペンドすると
ブロックはここで手続きオブジェクトに変換されて
メソッド内で変数に代入できるようになる

  class String
    def speak(&word)                      #仮引数に&を付ける
      word.call.each { |item| print item, "-#{self} " }     
    end
  end

  "moo".speak { "block me!" * 3 }  
                    # => block me!block me!block me!-moo

  "moo".speak do
    a = [1,2,3]
    s = "block me!"
    a << s
  end        # => 1-moo 2-moo 3-moo block me!-moo

注意点はブロックをメッセージに付けるとき
それをカッコの中に入れてはいけない
メッセージのカッコはオブジェクト引数のためのものであり
まだその時点ではブロックはオブジェクトになっていないからだ
(カッコに入れていないブロックが受側でカッコで受けられるという構文にはちょっと違和感があるけど)

ブロック付きメソッド

手続きオブジェクトを利用することで
ブロックはメソッドにも渡せるし
変数にも代入できるポータブルなものになった
これこそ純粋オブジェクト指向プログラミングの真骨頂だ


でもその分ユーザは
オブジェクト生成コストを負担しなければいけない
ポータブル性を捨ててもそれを避けたい場合もある


Rubyでは引数とは別に
ブロックを直接メッセージに付ける方法で
これを可能にする(&を用いた場合と同様、引数用のカッコの中にブロックを入れてはいけない)
Rubyは普段オブジェクトしか相手にしないけど
ブロックだけは特別扱いすることにしたのだ
(この点では純粋オブジェクト指向プログラミングの看板に偽りありか?)

  class String
    def speak
      yield.each { |item| print item, "-#{self} " }  
        # yieldでブロックを呼ぶ
    end
  end

  "moo".speak { "block me!" * 3 }
           # => block me!block me!block me!-moo

  "moo".speak do
    a = [1,2,3]
    s = "block me!"
    a << s
  end         # => 1-moo 2-moo 3-moo block me!-moo

メッセージに直接付けたブロックは
オブジェクトのように引数を通してメソッドに渡されるのではなく
メソッド内でキーワードyieldを呼ぶことによって
直接実行される
実行結果は手続きオブジェクトの場合と変わらない


なお手続きオブジェクトをブロックに戻したいときには
メッセージの送信側で先のアンパサンド付き引数が使える

  block = lambda { |i| print i, " " }

  5.times(&block)            # => 0 1 2 3 4

  3.upto(10, &block)      # => 3 4 5 6 7 8 9 10

この例の場合ブロックは手続きオブジェクトの形で送られ
受側メソッドのyieldが呼ばれたときに
ブロックに戻されるようになる(と想像してます)

ブロックとメソッド

考えてみればブロックはメソッドによく似ている
メソッドだって一連の手続きをモジュール化したものだ
メソッド同様ブロックの戻り値は最後の評価式かreturnの引数だ
(returnはオブジェクト化ブロックでのみ有効)

  def meth      # メソッド
    a = [1,2,3]
    s = "method me"
    a << s
  end

  do                  #ブロック
    a = [1,2,3]
    s = "block me!"
    a << s
  end


メソッドには名前は付き物だけれども
ブロックにはそれがないので


いわばブロックは名無しメソッドだ!


名無しの利点は
彼がそこにいるのだったら
いちいち名前を呼ばなくてもいい
ということなんだろうけど
その利点は僕の理解に余る


メソッドは引数を取れる
だからきっとブロックも取れるに違いない

そうその通り
でもやり方がちょっと違う
ブロックではその内側先頭に用意した垂直バーで挟んで
引数を受け取る

  def meth(arg)      # メソッド
    a = [1,2,3]
    s = "method me"
    (a << s) * arg
  end

  do |arg|                 #ブロック
    a = [1,2,3]
    s = "block me!"
    (a << s) * arg
  end

ブロックはyieldが呼ぶのだから
メソッド呼び出しのように
yieldに付けた引数がブロックに渡されることになる
(手続きオブジェクトを生成した場合はProc#callがブロックを呼ぶのでその場合はcallメソッドにつけた引数)


このときブロックを付けたメッセージに引数も付けて
これがyieldの引数として使われるようにしてもいい

  class String
    def speak(i)
      yield(i).each { |item| print item, "-#{self} " } 
         # メソッドの引数 i をyieldに渡す
    end
  end

  "moo".speak(2) do |arg|
    a = [1,2,3]
    s = "block me!"
    (a << s) * arg
  end      
     # => -moo 2-moo 3-moo block me!-moo 1-moo 2-moo 3-moo block me!-moo

こうするとメッセージに付けた引数が
隣のブロックに直接渡されるように見える
でも実際には引数もブロックもメソッドに渡されて
その中のyieldで橋渡しされる

ブロックのパワー

メソッドの挙動は通常クラスの設計時にほぼ決まる
でもブロックを使うことで
メソッドに元からある機能は大きく拡張されうる
ブロックはメソッドに後から一連の手続き(メソッド)を差し込める
まるでクラスにモジュールを差し込むみたいに

そうだからブロックは…


メソッドに対するメソッドのMix-inなんだ!


Mix-inの方法には制限はないけれども
渡したブロックを
対象のオブジェクトと密に結合するものと
粗に結合するものとがある


密に結合するものの代表例はイテレータ(繰り返し)である

  [1, 2, 3].each { |i| print i, " " }     # => 1 2 3

配列オブジェクトのeachメソッドは
任意の手続きを記述したブロックをとって
この手続きを自身の各要素に順次適用するという処理を施す


eachメソッドの実装等価コードは以下のようになる

  class Array
    def each
      n = 0
      while n < self.length
        yield self[n]
        n += 1
      end
    end
  end

yieldは配列の各要素1,2,3を順次取って呼び出され
その都度要素を出力する


ブロックの中身を変えれば
元のメソッドの中身は同じでも
得られる結果は大きく変わる
これこそがブロックのパワーだ

  [1,2,3].each { |i| puts "_R_" * i * i }  

# => _R_
  _R__R__R__R_
  _R__R__R__R__R__R__R__R__R_

汎用性が認められるのなら
イテレータメソッドを自分で定義してもいい

  class Array
    def each_after_divide(i)
      n = 0
      while n < self.length
        yield self[n]/i
        n += 1
      end
    end
  end

[1,2,3].each_after_divide(2.0) { |i| print i, " " }
          # => 0.5 1.0 1.5


粗に結合するものの代表例はリソース管理だ
定型的な前処理、後処理を伴うこの種の手続きは
定型処理をメソッドで定義して
可変的な処理の実体をブロックで書いて渡す


ファイルをオープンしクローズする定型処理を
メソッドで記述しておけば
具体的にファイルの中身を処理するコードだけを
ブロックで追加的に書けばいい

クロージャ

ブロックは名無しのメソッドで
だからほとんどメソッドを記述する気持ちで記述できる
でもちょっと違うところもある


メソッドはdef endの厚い壁によって
外部の空間とは完全に分断されている
だからメソッドの中からは外にあるローカルな変数は見えない
この厚い壁を透過できるのは
インスタンス変数とそれ以上のグローバルな参照を許している変数
(クラス変数、定数、グローバル変数を指しています)のみだ


インスタンス変数とは特定のオブジェクト内で
他のオブジェクトをグローバル参照できるようにする変数である


一方ブロックのdo end あるいは { } は
それよりも壁が薄くて中から外が見える
つまり外にあるローカルな変数が見える
ブロックの中ではインスタンス変数に頼らずに
外の世界を把握し
場合によってはその状況を変えることができる
(ブロックにおける変数の透過性については「初めてのRuby」に詳しい)


一つのブロックから複数の手続きオブジェクトを生成するとき
個々の手続きオブジェクトは
外のローカル変数が参照するオブジェクトを
その生成時に一緒に閉じこめることができる
それはあたかもオブジェクトがその生成時に
その状態をインスタンス変数に閉じこめるようだ


メソッドに
このようなオブジェクト的な状態保持の機能を持たせたもの
それを一般にはクロージャーというようだ
Rubyのブロックはこの点から見れば
特定のオブジェクトに内包された
簡易なオブジェクト生成機構である

メソッドオブジェクト

これほどブロックとメソッドとが似たものならば
当然メソッドもメッセージに付けて
他のオブジェクトに渡せないかと考える


RubyではメソッドをMethodオブジェクト化することによって
それもできる
手続きオブジェクトの場合と同じように
メソッドはオブジェクト化されるので
他のオブジェクト同様メッセージの引数として渡せるようになる

  class String
    def speak(arg, meth)
      meth.call(arg).each { |item| print item, "-#{self} " }
    end
  end

  def my_meth(arg)
    a = [1,2,3]
    s = "hello!"
    (a << s) * arg 
  end

  "moo".speak(2, method(:my_meth))  # methodメソッドを使う
       # =>1-moo 2-moo 3-moo hello!-moo 1-moo 2-moo 3-moo hello!-moo

メソッドのオブジェクト化には
Objectのmethodメソッドを使う


これはあくまでもメソッドなのでブロックと異なり
状態保持の機能はない


ブロック付きメソッドと同じように
メソッドオブジェクトを生成せずに
メソッドを渡す方法もある
Objectのsendメソッドを使う

  class String
    def speak(meth)
      meth.each { |item| print item, "-#{self} " }
    end
  end

  def my_meth(arg)
    a = [1,2,3]
    s = "hello!"
    (a << s) * arg 
  end

  "moo".speak(send(:my_meth, 2))  # sendメソッドを使う
    # =>  1-moo 2-moo 3-moo hello-moo 1-moo 2-moo 3-moo hello!-moo

sendメソッドはメソッド名と共に
引数を同時に取って
そのメソッドの実行結果を返す
だからspeakメソッド内の変数methが
ブロック付きメソッドにおけるyield相当になる


最後にブロックの特性をまとめてみよう

  1. 名前のないメソッドである
  2. メソッドに後から差し込めるMix-inメソッドである
  3. 望めばオブジェクトになれる未登録オブジェクトである
  4. 外部状態を閉じ込めた簡易オブジェクトあるいはクロージャーである


関連記事:
Rubyのシンボルは文字列の皮を被った整数だ! - hp12c
Rubyのyieldは羊の皮を被ったevalだ! - hp12c
Rubyのクラスはオブジェクトの母、モジュールはベビーシッター - hp12c


初めてのRuby

初めてのRuby