目次

10.  ブロックと手続きオブジェクト

 

これは、Rubyに数あるクールな特色のなかでもその際たるものです。 ほかの言語の中にもこの特徴を持っているものがありますが、そこでは (クロージャー などの)他の名前がついています。 でも、ほとんどの有名なプログラミング言語にはないものです。

では、このクールな新しいこととはいったい何でしょう。 それは、コードのブロック を作って(doendの 間のコードをこういいます。)、それをオブジェクトとしてくるんで (手続きオブジェクト(proc) と呼びます。)、変数に保存したり メソッドに渡したりして、それでもって、そのブロックの中のコードは好きな時に (何度でも)実行させることが出来るという能力です。 なので、この特徴はメソッドと似ているのですが、これがオブジェクトに付属しているものでは ないところが異なります(それ自身 がオブジェクトなのです)。 そして、それを他のオブジェクトと同じように保存したり受け渡したり出来るのです。 そろそろ例をお見せしたほうが良いですね。

toast = Proc.new do
  puts 'かんぱーい!'
end

toast.call
toast.call
toast.call
かんぱーい!
かんぱーい!
かんぱーい!

これで、手続きオブジェクトを作りました。(このProcというのは、 手続きという意味の"procedure"の短縮語だと思いますが、より重要なのは、 "proc"というのが、"block"と韻を踏んでいるということではないかと思います。) この手続きオブジェクトにはコードのブロックが含まれていて、それを3回 呼び出し(callし)ました。これで見る限りメソッドとよく似てますね。

実際、手続きオブジェクトはメソッドに本当に近いものと言えるかもしれません。 というのは、ブロックは引数をとることが出来るからです。

doYouLike = Proc.new do |aGoodThing|
  puts '私は *ほんとうに* '+aGoodThing+' が好きだ!'
end

doYouLike.call 'チョコレート'
doYouLike.call 'ruby'
私は *ほんとうに* チョコレート が好きだ!
私は *ほんとうに* ruby が好きだ!

OK。ブロックと手続きオブジェクトが何たるかは分かりましたし、どう使うのかも 分かりました。でも、何がポイントなんでしょうか? 何でメソッドを使うだけではいけないのでしょうか? それは、メソッドでは出来ないことがあるからです。 特に、ひとつのメソッドを他のメソッドに渡すことは出来ません(手続きオブジェクトなら可能です)。 そして、メソッドは他のメソッドを返すことは出来ません(手続きオブジェクトを返すことは可能です)。 これは単純に、手続きオブジェクト(proc)がオブジェクトであるからで、 メソッドはそうでないからなのです。

(ところで、これはどこかで見たことがあることなのではないでしょうか? はい、すでにブロックは見たことがありますね。そう、イテレータを学ぶときに。 でも、ここではもう少し話を進めていきましょう。)

手続きオブジェクトを受け取るメソッド

メソッドに手続きオブジェクトを渡すと、その手続きを、どうやって、どんな時に、何度、 呼ぶかのコントロールが可能になります。たとえば、あるコードが実行される前後に何かやりたいことが あるとしましょう。

def doSelfImportantly someProc
  puts 'みなさん、ちょっとよろしいですか!  私はしたいことがあります...'
  someProc.call
  puts 'Ok みなさん, 終わりました.  していたことを続けてください.'
end

sayHello = Proc.new do
  puts 'コンニチハ!'
end

sayGoodbye = Proc.new do
  puts 'サヨウナラ'
end

doSelfImportantly sayHello
doSelfImportantly sayGoodbye
みなさん、ちょっとよろしいですか!  私はしたいことがあります...
コンニチハ!
Ok みなさん, 終わりました.  していたことを続けてください.
みなさん、ちょっとよろしいですか!  私はしたいことがあります...
サヨウナラ
Ok みなさん, 終わりました.  していたことを続けてください.

おそらくこれを見ても、特別すばらしいとは思えないかもしれませんが、 実はすばらしいのです :)。 プログラムでは、何をいつやらなければならいかということに関して厳密に決められている ということはよくあることなのです。たとえば、もしファイルを保存したいなら、 ファイルを開き、保存したい情報を書き出し、ファイルを閉じます。 もし、ファイルを閉じ忘れたとしたら、良からぬこと(tm)が起こるかも 知れません。しかし、ファイルを保存したり読み出したりするたび毎に、 同じこと、つまり、ファイルを開き、ほんとに やりたいことを行い、 そしてファイルを閉じる、、をしなければならないのです。これは、退屈で忘れやすいことですね。 Rubyでは、ファイルの保存(あるいは読み出し)は上で示したようなコードになっています。 従って、実際に保存し(あるいは読み出し)たいこと以外に気を回す必要がなくなります。 (ファイルを保存したり読み出したりする方法に関しては、それらをどこで調べれば良いのか、 次の章で示しますね。)

手続きオブジェクトを何回呼び出すのか、あるいはそもそも呼び出すかどうか を決定するメソッドを書くことも可能です。 ここに、渡された手続きを半分の回数だけ行うメソッドと、 2回呼び出すメソッドを示します。

def maybeDo someProc
  if rand(2) == 0
    someProc.call
  end
end

def twiceDo someProc
  someProc.call
  someProc.call
end

wink = Proc.new do
  puts '<ウインク>'
end

glance = Proc.new do
  puts '<目くばせ>'
end

maybeDo wink
maybeDo glance
twiceDo wink
twiceDo glance
<ウインク>
<ウインク>
<ウインク>
<目くばせ>
<目くばせ>

(何度かこのページをリロードすると、結果が毎回変わっているのが分かると思います。) 手続きオブジェクトのもう少し普通の使い方として、メソッドだけではできなかったものの例を 示しましょう。つまり、これまでに、ウインクを2回するメソッドを書く方法は覚えましたが 何か を2回するメソッドというのは書けなかったのではないでしょうか。

先に行く前に、もうひとつの例を見てみましょう。 ここまでは、渡している手続きオブジェクトはお互い大体似たようなものでした。 今度は、まったく違ったものです。この例で、このようなメソッドが、 渡された手続きオブジェクトにどれほど依存しているかが見て取れると思います。 ここでのメソッドは、あるオブジェクトと手続きオブジェクトを受け取って そのオブジェクトを引数とした手続きを実行しています。 手続きがfalseを返すとプログラムを終了させます。でなければ、返ってきた値を引数として また手続きを呼び出します。これを、手続きがfalseを返すまで続けます。 (必ず最終的にはfalseを返すようにするべきです。でないとプログラムがクラッシュします。) メソッドは、手続きから返ってきたfalseではない最後の値を返します。

def doUntilFalse firstInput, someProc
  input  = firstInput
  output = firstInput

  while output
    input  = output
    output = someProc.call input
  end

  input
end

buildArrayOfSquares = Proc.new do |array|
  lastNumber = array.last
  if lastNumber <= 0
    false
  else
    array.pop                         #  末尾の数字を取り出して...
    array.push lastNumber*lastNumber  #  ...その2乗の数と置き換え...
    array.push lastNumber-1           #  ...1つ小さい数を後につける。
  end
end

alwaysFalse = Proc.new do |justIgnoreMe|
  false
end

puts doUntilFalse([5], buildArrayOfSquares).inspect
puts doUntilFalse('私はこれを午前3:00に書いています; 私はもうノックアウト!', alwaysFalse)
[25, 16, 9, 4, 1, 0]
私はこれを午前3:00に書いています; 私はもうノックアウト!

OK, これはかなり凝った例と言うことは私も認めます。 でも、このdoUntilFalseというメソッドが、渡された手続きオブジェクトによって まったく違う動作をするということはわかるのではないでしょうか。

inspectメソッドはかなりto_sと似ているのですが、 to_sが人間が読めるようにするための文字列に変換するのと違って、 返す文字列が、渡したオブジェクトを作るためのRubyのコードを表現している ところが違います。ここでは、doUntilFalseを最初に呼んだ時に、 返ってきた配列をまとめて表示するのに使っています。 それと、その配列の最後にある0は、実際には2乗されていないということ に気が付いたかも知れませんね。でも、0の2乗はやはり0なので、 その必要はなかったわけです。 そして、alwaysFalseは見てのとおりいつもfalseを返します。 なので、doUntilFalseは2回目に呼ばれた時はまったく何もしていません。 呼ばれたものを単に返しているだけです。

手続きオブジェクトを返すメソッド

もうひとつ、手続きオブジェクトを使ってできるとてもクールな技は、 それをメソッド内で作成して返すということです。これによって、 (遅延評価 とか、無限データ構造 とか、 カリー化 とか)といったあらゆるクレイジーなプログラミングパワーを 得ることができます。 でも実のところ、私はほとんどこの技を実務で使わないですし、 誰かがこれをコードの中で使っているのを見た覚えはありません。 私が思うに、この技は普通Rubyでしなければならないたぐいのものではないし、 Rubyでは他の解決方法があるような気がします(詳しくはわかりませんが)。 とにかく、ここでは、簡単にこの技法について触れるだけにしましょう。

この例では、composeは2つの手続きオブジェクトを受け取って、 ひとつの新しい手続きオブジェクトを返しています。その返す手続きオブジェクトは 呼ばれたら、最初の手続きをまず行って、その結果を2つ目の手続きに渡すということを します。

def compose proc1, proc2
  Proc.new do |x|
    proc2.call(proc1.call(x))
  end
end

squareIt = Proc.new do |x|
  x * x
end

doubleIt = Proc.new do |x|
  x + x
end

doubleThenSquare = compose doubleIt, squareIt
squareThenDouble = compose squareIt, doubleIt

puts doubleThenSquare.call(5)       # (5+5) * (5+5)
puts squareThenDouble.call(5)       # (5*5) + (5*5)
100
50

ここで、proc1の呼び出しは、最初に実行されなければならないため、 proc2のカッコの中になければならなかったことに注意してください。

メソッドに対して(手続きオブジェクトではなく)ブロックを渡す

以上のことは、学問的に興味深い内容ではありますが、使うかどうかは議論の 分かれるところです。ここで問題なのは、3つのステップ(メソッドを定義し、 (ブロックを使って)手続きオブジェクト作り、 それを引数としてそのメソッドを呼ぶ。)を踏んでいるということです。しかし、 これは2つのステップ(メソッドを定義し、ブロック を、手続きオブジェクトを 使わずそのまま渡す。)でもよさそうに思えます。 なぜなら、メソッドに渡した後はその手続き(ブロック)を使う必要はほとんどないでしょうから。 さて、気が付いていたかもしれませんが、Rubyはそのことをすべて解決している のです。実際、イテレータを使うたびにその2段階のステップを使っているわけです。

最初に手短な例からお見せしましょう。後でそれについて説明します。

class Array

  def eachEven(&wasABlock_nowAProc)
    isEven = true  #  配列は0という偶数から始まるので、最初は"true"です。

    self.each do |object|
      if isEven
        wasABlock_nowAProc.call object
      end

      isEven = (not isEven)  #  偶数から奇数へ、あるいは奇数から偶数へとトグルします。
    end
  end

end

['アップル', '悪いアップル', 'チェリー', 'ドリアン'].eachEven do |fruit|
  puts 'んー, おいしい!  僕は '+fruit+'パイが大好きさ.'
end

#  配列の偶数番目の要素を取っていることを思い出してください。
#  それらはすべて奇数なわけです。
#  私はこの手の問題を起こすのが結構好きなんですよ。
[1, 2, 3, 4, 5].eachEven do |oddBall|
  puts oddBall.to_s+' は偶数 で は な い!'
end
んー, おいしい!  僕は アップルパイが大好きさ.
んー, おいしい!  僕は チェリーパイが大好きさ.
1 は偶数 で は な い!
3 は偶数 で は な い!
5 は偶数 で は な い!

ブロックをeachEvenに渡すには、ブロックをメソッドの後につければ よいだけです。どのメソッドにもこのようにブロックを渡すことができますが、 多くのメソッドはそれをただ無視するだけです。もし、あなたのつくるメソッドが、 これを無視せず、 捕まえて、手続きオブジェクトに変身させるようにするためには、 メソッドの引数の並びの最後に、アンド記号(&)を頭につけて、 手続きオブジェクトの名前を並べてください。 そうすればそのメソッドを、eachtimesのような、ブロックを取る 組み込みメソッドと同じように、何度でも使えるようになります。 (5.times doを思い出してください。)

もし、混乱するようなら、eachEvenの振る舞いを思い出してみましょう: 配列の要素に対してひとつ飛びに、渡されたブロックを呼び出しています。 一度これを書いてしまい、それが動くなら、(どのブロックがいつ呼ばれるかなど) その中で実際に行われていることに関して考える必要はなくなります。実のところ、 このようなメソッドを書く正確な理由 とはこういうことです。なので、 中でどのように動いているのかを再度考える必要はないわけです。使うだけです。

私は以前、プログラムの異なる部分がそれぞれどのくらいの時間を取るのかを 知りたいと思いました。(これは、コードをプロファイル するともいいます。) そこで、コードを実行する前に時間を計り、実行させ、 その後もう一度時間を計り、その差を計算する、メソッドを書きました。 今すぐにはそのコードを見つけることはできませんが、おそらくその必要ありません。 それはおそらくこんな感じです。

def profile descriptionOfBlock, &block
  startTime = Time.now

  block.call

  duration = Time.now - startTime

  puts descriptionOfBlock+':  '+duration.to_s+''
end

profile '25000回同じ数を足し合わせる' do
  number = 1

  25000.times do
    number = number + number
  end

  puts number.to_s.length.to_s+''  #  これは、この巨大な数の桁数です。
end

profile '100万まで数える' do
  number = 0

  1000000.times do
    number = number + 1
  end
end
7526 桁
25000回同じ数を足し合わせる:  0.137398 秒
100万まで数える:  0.500842 秒

なんて簡単なんでしょう。そして、なんてエレガントな。いまや、こんな小さなメソッドで どんなプログラムのどんな一部分でも好きなだけ時間を計測できます。 単に、コードをブロックに入れてprofileに送れば良いのです。 これより簡単になり得る事はないと思います。他のほとんどの言語だったら、明示的に 時間計測用のコードを付け加えなければならないでしょう。 それに比べてRubyでは、そういったコードをすべて一つの場所、しかも(より重要なことですが) 邪魔にならない場所に、一まとめにしておくことができるのです。

練習問題

おじいさんの時計 。 ブロックを取って、今日過ぎた時間の回数だけ、 ブロックを呼ぶメソッドを書きなさい。それで、もし、ブロックとして do puts 'DONG!endを渡したとしたら、それはおじいさんの時計のような (そんなたぐいの)チャイムを打つことになります。できたメソッドを、 (先ほどの例を含めて)2,3の違うブロックを使ってテストしてみなさい。
ヒント:いまの時間を得るには Time.now.hour が使えます。でも、この時間というのは、0から 23の 数字を返しますので、それを普通の時計の前面にある数字 (1から 12)へと 変換しなければなりません。

プログラムロガー。 ブロックを説明する文字列と ブロックを受け取る、logというメソッドを書きなさい。 doSelfImportantlyの例と似ているかもしれませんが、このメソッドでは、 渡されたブロックの開始を告げる文字列、修了を告げるまた別の文字列、 そしてブロックが何を返したかを告げる文字列をputsするようにします。 これにコードブロックを送って、作ったメソッドをテストしなさい。ブロックの中に 別の logへの呼び出しを入れて、それをもうひとつのブロックに 渡しなさい。(これは入れ子構造(nesting) と呼ばれます。) 別の言い方をすると、次のような出力が得られるようにしなさい。

開始 "外ブロック"...
開始 "ある小さなブロック"...
..."ある小さなブロック" 終了, 返り値は:  5
開始 "もうひとつのブロック"...
..."もうひとつのブロック" 終了, 返り値は:  I like Thai food!
..."外ブロック" 終了, 返り値は:  false

ロガー、改良版。 前のロガーの出力は少し読みにくく、多く使うほど より悪くなっていくようです。もし、内部のブロックで行が字下げ(インデント)されていれば 断然読みやすくなると思われます。それをするには、ロガーが、何か出力するたびに 何段階の入れ子になっているか保存しておく必要があるでしょう。その際、 コードのどの場所からも見ることができる変数である、グローバル変数 を使いなさい。この、グローバル変数を作るためには、$で始まる名前を 使えば良いです。たとえば、$global, $nestingDepthあるいは、 $bigTopPeeWeeはグローバル変数です。 そして、最後、ロガーは、こんなふうな出力をするようにします。

開始 "外ブロック"...
  開始 "ある小さなブロック"...
    開始 "ちっちゃなブロック"...
    ..."ちっちゃなブロック" 終了, 返り値は:  lots of love
  ..."ある小さなブロック" 終了, 返り値は:  42
  開始 "もうひとつのブロック"...
  ..."もうひとつのブロック" 終了, 返り値は:  I love Indian food!
..."外ブロック" 終了, 返り値は:  true

さて、これで、このチュートリアルから学ぶことはほとんどおしまいです。 おめでとう! あなたはたくさん のことを学びました。 おそらくは、すべてを覚えていると言う気持ちにはなっていないでしょうし、あるいは、 ある部分を飛ばしているかもしれません。でもそれでよいのです。 プログラミングは知る物ではなくて、理解するものだからです。 忘れたものを思い出す場所を知っている限り順調です。 私がこのチュートリアルすべてを何も見ないで書いたなどとは思わないで下さい。 なぜなら、しょっちゅう何かを参考にしていたからです。 それと、このチュートリアルの例のコードについても、多くの助けを得ています。 では、はどこでそれを見ていたのでしょう。そしては誰に 助けを請うていたのでしょう。次は、 それをお示ししましょう。

 

目次