Rubyプログラミングの基礎知識
実践Ruby on Rails 4 現場のプロから学ぶ本格Webプログラミング
顧客管理システムの構築を体験しながら、Railsアプリケーション開発のノウハウを習得! この記事は、書籍『実践Ruby on Rails 4 現場のプロから学ぶ本格Webプログラミング』の内容を、Think IT向けに特別にオンラインで公開しているものです。詳しくは記事末尾の書籍紹介欄をご覧ください。ブロックとProcオブジェクト
この節はやや高度な内容を含みます。難しく感じられたらいったん読み飛ばし、時間をおいて復習してください。
ブロック
ブロックとは
ブロックは、Rubyの初学者を悩ませる概念の筆頭です。おそらく最初に出会うのが配列のeachメソッドに付随するブロックでしょう。たとえば次のような。
[ 2, 3, 5, 7, 11 ].each { |n| puts n ** 2 }
このコードを実行すれば、4、9、25、49、121という数が結果として表示されます。
これはもちろんブロックの由緒正しき利用法です。問題は、ブロックを伴うメソッドを自分で定義するときです。そのようなメソッドを自由自在に定義できるようになると、プログラミングの幅が格段に広がります。もちろんRailsによるアプリケーション開発でも役立ちます。
簡単な使用例を示しましょう。
chapter03/block01.rb
class Robot def job puts "Started." yield puts "Finished." end end robot = Robot.new robot.job { puts "Hello World!" }
最終行の波括弧({})で囲まれた部分がブロックです。ブロックはメソッドにコードを渡すための仕組みです。「メソッドにコードを渡す」とは、どういう意味でしょうか。普通はメソッドに引数としてさまざまなオブジェクトを渡しますね。たとえばr.job(7)のように。この7というオブジェクトの代わりにputs "Hello World!"というコードを渡しているのです。
さて、4行目にyieldと書いてあります。ここでブロックが実行されます。3行目と5行目を見てください。ここに書かれているコードがブロックを実行する前と後に実行されます。
つまり、このプログラムを実行すると次のような結果となります。
$ ruby block01.rb Started. Hello World! Finished.
なお、ブロックの開始と終了は波括弧({})の代わりにdoとendでも示せます。
robot.job do puts "Hello World!" end
通常、私はブロックが1行に収まるときは波括弧({})、収まらないときはdoとendを使うことにしています。
ブロック変数
メソッドからブロックにオブジェクトを渡したいときはブロック変数を利用します。これも実例から学びましょう。
chapter03/block02.rb
class Robot def initialize(name) @name = name end def job puts "Started." yield(@name) puts "Finished." end end robot = Robot.new("Alice") robot.job { |name| puts "Hi, my name is #{name}!" }
ブロックの開始直後にあるパイプ文字(|)で囲まれたnameがブロック変数です。この例では、インスタンス変数@nameの値である"Alice"という文字列がブロック変数nameにセットされます。そして、そのnameの値を用いてputs "Hi, my name is #{name}!"というコードが実行されます。
このプログラムを実行すると次のような結果となります。
$ ruby block02.rb Started. Hi, my name is Alice! Finished.
ブロックの活用法
次に、もう少し複雑なブロックの使用例をお見せしましょう。
chapter03/block03.rb
class Robot attr_reader :x, :y def initialize @x = 0 @y = 0 end def job x0, y0 = x, y yield(self) puts "(#{x0}, #{y0}) => (#{x}, #{y})" end def move(d1, d2) @x += d1 @y += d2 end end robot = Robot.new robot.job do |r| r.move(1, 0) r.move(0, 1) end robot.job do |r| r.move(1, 0) r.move(1, 1) r.move(0, -3) end
jobメソッドの定義に着目してください。
def job x0, y0 = x, y yield(self) puts "(#{x0}, #{y0}) => (#{x}, #{y})" end
まずローカル変数x0とy0に属性xとyの値をセットしています。そして、ブロック変数としてself、つまり自分自身をセットしてブロックを実行します。この間に属性xとyの値は変化します。最後に属性xとyの値がどう変化したかを出力しています。
次に、このメソッドを呼び出す側のコードを読み解きましょう。Robotオブジェクトが生成された後、jobメソッドが2回呼ばれています。1回目の呼び出しはこうです。
robot.job do |r| r.move(1, 0) r.move(0, 1) end
ブロックの内部ではブロック変数rのmoveメソッドが2回呼ばれています。このrの正体は何でしょうか。メソッドjobがyieldを通じて送ってくるselfすなわちRobotオブジェクトです。
上記コードに現れるローカル変数robotとブロック変数rは同じオブジェクトを指しています。しかし、メソッドの中でyield(self)が呼ばれたから同じになっただけで、常にそうなるわけではありません。混乱しやすいところです。注意してください。
ブロックの中でr.move(1, 0)というコードが実行されると、このr(Robotオブジェクト)の属性xに1が加算されます。同様に、次のr.move(0, 1)でrの属性yに1が加算されます。ですから、1回目のjobメソッドコールが終わるとターミナルには次のような結果が表示されていることになります。
$ ruby block03.rb (0, 0) => (1, 1)
続いて2回目のjobメソッドコールに処理が移ります。
robot.job do |r| r.move(1, 0) r.move(1, 1) r.move(0, -3) end
ローカル変数robotは前回と同じRobotオブジェクトを参照していて、そのRobotオブジェクトは自らの状態を記憶していますので、属性xとyの値は1と1です。そして、ブロック変数rのmoveメソッドが3回呼ばれます。属性xの値は2加算されて、属性yの値は2減らされています。
結局のところ、最終的なプログラムの実行結果は次のようになります。
$ ruby block03.rb (0, 0) => (1, 1) (1, 1) => (3, -1)
ところで、このプログラムの要点はどこにあるのでしょうか。単にRobotオブジェクトの属性をmoveメソッドで変更するだけならこんなに複雑なプログラムを書く必要はありません。私がしたかったのは、一連のmoveメソッドコールの前後で属性xとyがどう変化したかを画面に表示することです。ブロックを使うと、その目的をエレガントに達成できます。
通常のメソッド呼び出しはメソッド側で定義されたコードしか実行できません。しかし、ブロック付きでメソッドを呼び出す場合は、メソッド側で定義されたコードの中にメソッドを呼び出す側から別のコード(ブロック)を挿入することができるのです(図3-1)。