ソートや大小比較に使える
ブロックは,C言語のqsortのような要素ごとの条件判定にも使えます。例えば,Rubyのソート・メソッドは以下のようにブロックを扱えます。
ary.sort{|a,b| a<=>b}
C言語のqsortと比較するとずいぶん簡単に使えることが分かります。もっとも,sortメソッドはブロックが指定されないと要素を「<=>」演算子を使って比較しますから,このような指定をしてもデフォルトと同じことになるため,あまり意味がありません。
今度は各要素を整数に変換してソートしてみましょう。
ary.sort{|a,b|
a.to_i <=> b.to_i
}
これも簡単ですね。ブロックを指定しない方法では,文字列を辞書順に比較しますが,このようにブロックを指定した場合には,整数として値が小さい順に並び替えます。辞書順の並びでは「10」は「1」と「2」の間に並びますが,数値順であれば「9」の後ろになります。
とはいえ,考えてみると比較するたびに整数化するのはかなり無駄です。ソート手順ではかなり頻繁に比較操作が入りますし,比較回数は要素の数が増えるほど増加します。現在のRubyの実装では,要素の並び方にもよりますが,4要素なら3回,100要素なら500回程度,1000要素では9500回程度呼び出されます。ですから,特に要素数が多い場合,評価用ブロックを実行するコストも無視できません。
このような場合に用いることができるメソッドがsort_byです。sort_byはブロックを評価した結果を使ってソートするメソッドです。ブロックの呼び出しは各要素につき1回だけです。
ary.sort_by{|x| x.to_i}
ブロックで後処理を保証する
Rubyには他のプログラミング言語同様,例外処理がありますから,エラーが発生した場合には処理を中断することがあります。例えば,ファイル処理を進める際に次のような手順を書いたとしましょう。
f = open(path)… # (a)
f.close
もし(a)の部分で例外が発生すると,ファイルがクローズされないまま残ってしまうことが考えられます。このような場合には,Javaのfinallyに相当するensureを使って確実にクローズする必要があります。
f = open(path)
begin
…
ensure
f.close
end
これで安全になりましたが,毎回決まりきった処理を書くのも面倒です。そこで登場するのがブロックです。Rubyのopenメソッドは次のような書き方ができます。
open(path) {|f|
…
}
openメソッドにブロックが与えられた場合,ブロックを抜けると自動的にファイルがクローズされます。
ブロックは新たな制御構造を作る
ブロックを使えば,文法を変更することなく,メソッドを使って制御構造を定義できます。例えば,無限ループを実現するloopメソッドや決められた回数だけ繰り返すtimesメソッドはブロックを使って実現された制御構造です(図10[拡大表示])。
ブロックは条件指定にも使えます。例えば,コンテナのrejectメソッドはブロックを評価した結果が真の要素を削除した配列を返します(図11[拡大表示])。
rejectと同じ処理は,明示的にループを使って,図12[拡大表示]のように書くこともできますが,rejectならわずか1行で記述できる処理に6行もかかっています。rejectは短いだけでなく,意図も明確です。
この種のメソッドはRubyには他にいくつもあって,すべて「-ect」で終わる名前を持っています(表1[拡大表示])。
コールバックに使えるブロック
ブロックをコールバックとして使うこともできます。図13[拡大表示]はTk*8を利用してGUIを表示するプログラムです。
図13のプログラムを実行すると「hello」というラベルの付いた小さなボタンを表示します。ボタンを押すと標準出力に「hello」という文字列が出力されます。
commandメソッドで指定されたブロックは,クロージャ・オブジェクトとしてボタン・オブジェクトの中に保存されるため,ボタンが押されたときに実行されるのです。
ブロックの扱いが特別である理由
先ほども述べたようにRubyのブロックには,次にような特徴があります。
●通常の引数とは別に渡される
●それ自体はオブジェクトではない(lambdaメソッドでクロージャ・オブジェクト化できる)
クロージャを持つ他の言語,例えばLispやSmalltalkではこのような区別はなくクロージャはいつもオブジェクトとして存在しています。その点でRubyは「変わっている」と言えます。これはいったいなぜなのでしょうか。
理由は2つあります。一つはオブジェクトの生成数を減らすためです。初期のRubyはクロージャ・オブジェクトの生成コストが高かったため,できるだけクロージャ・オブジェクトの生成を避けようとしていました。本当にオブジェクトとして必要な時まで生成を遅延することで少しでもパフォーマンスを向上させようという涙ぐましい努力があったのです。
もう一つの理由は,外見上の理由です。もし,LispやSmalltalkのようにブロックをクロージャを作る式だとするならば,eachメソッドは次のような外見になったことでしょう。
ary.each({|x|puts x})
これではあまり制御構造のようには見えません。Rubyにおいて文法を拡張せずにより自然な形で制御構造を追加できたのは,メソッド呼び出しにおいてブロックを特別扱いしているためなのです。
どのように特別扱いしているのか,Smalltalkと比較してみましょう。Smalltalkではwhile文と同様の処理を以下のように記述します。
[i < 10] whileTrue: [ i := i + 1.]
これは最初のブロック[i < 10]のwhileTrue:というメソッドを呼び出しています。後ろのブロック[ i := i + 1.]はこのメソッドの引数です。whileTrue:メソッドはレシーバを実行した結果が真の間,引数のブロックを繰り返し実行します。
繰り返し処理まですべてメソッド呼び出しで実行してしまうSmalltalkには恐れ入りますが,Rubyのような制御構造に普通の文法を採用している言語にはなじみません。
一方,Lispにおいてeachをクロージャを使って実現するなら,次のような感じになるでしょう。
(foreach ary (lambda(x) (puts x)))
クロージャを作るのにいつもlambdaを必要とするLispでは制御構造を拡張したというイメージはありません。Lispで制御構造を追加したい場合,次のようにマクロを使うことが多いでしょう。
(foreach x ary (puts x))
同じようにクロージャを許す言語でも,文法の違いから性格が異なっているところが興味深いです。
Rubyのようなやり方は,自画自賛になってしまいますが,文法を変更しないままで制御構造を追加するうまい方法だと思います。もちろん,メソッド呼び出しに1つだけしかブロックを付けられないという制約はあります。しかし,実際に複数のブロックが必要になることはほとんどありません。
今月はRubyの特徴的な機能であるブロックと,その背景となっている高階関数,クロージャについて学びました。ブロックの活用はRubyを使いこなす鍵だと思います。みなさんもいろいろ試してみてください。
■変更履歴 図13の拡大画像のリンク先が間違っていました。お詫びして訂正します。本文は修正済みです。 [2007/07/24 18:00] |