コンテナ、ブロック、イテレータ(3) 「プログラミング Ruby 1.9 - 言語編 - 4章」

ずいぶん間が空いてしまいましたがピッケル本1.9 再開です。

プログラミングRuby 1.9 −言語編−

プログラミングRuby 1.9 −言語編−

エントリに間があった期間は業務でテキスト処理ばかりでしたよ。
# めんどくさくなってFile.open()をラップする始末

さてさて、4章は残すところイテレータ以外のブロックの使い道についてだけです。

... とはいえ、lambdaを使ったクロージャなど、本のボリュームとは反比例の内容の濃さでした。

トランザクションとしてのブロック

ファイルストリームやDBの接続など、クローズし忘れのエラーを回避したい場合などに使う方法。
selfを使って,クラスメソッドとして実装します。
また,内部で使うメソッドに渡す引数を意識しなくてよいように*argsを引数とします。

例はこんな感じ*1

class File
  def self.open_and_process(*args)
    f = File.open(*args)
    yield f
    f.close()
  end
end

オブジェクトとしてのブロック

ブロックは無名のメソッドであるだけではなく、オブジェクトに変換することが可能です。
ブロックを受け付けるメソッドの最後の引数にアンパサンドを付けることで、
ブロックを明示的な引数としてメソッドで取り扱うことができます。

この際、引き渡されたブロックは"Procオブジェクト"として変換されます。
このProcオブジェクトは、必要に応じてcallメソッドを用いることでブロック内のコードを呼び出すことができます。

class ProcExample
  def pass_in_block(&action)
    @stroed_proc = action
  end
  
  def use_proc(paramater)
    @stroed_proc.call(paramater)
  end
end

eg = ProcExample.new
eg.pass_in_block {|param| puts "The parameter is #{param}"}
eg.use_proc(99) #=> The parameter is 99
eg.use_proc(100) #=> The parameter is 100

さらに、メソッドがProcオブジェクトを返すと以下のようにcallメソッドでパラメタを渡すことができます。

def create_block_object(&block)
  block
end

bo = create_block_object {|param| puts "You called me with #{param}"}

bo.call 99 #=> You called me with 99
bo.call "cat" #=> You called me with cat

この方法は便利な方法なので
わざわざメソッドを定義しなくとも、組み込みのメソッド lambdaかProc.newを使うことで
ブロックをオブジェクトに変換することができます*2

bo = lambda {|param| puts "You called me with #{param}"}
bo.call 99 #=> You called me with 99
bo.call "cat" #=> You called me with cat

クロージャとしてのブロック

ブロックの外側スコープにあるローカル変数を参照できることを利用して、

def n_times(thing)
  lambda {|n| thing *n }
end

p1 = n_times(23)
p1.call(3) #=> 69
p1.call(4) #=> 96

のようにクロージャとして利用することができます。
この際、ブロック(Proc オブジェクト)が存在している限りアクセス可能で、

def power_proc_generator
  value = 1
  lambda { value += value}
end

power_proc = power_proc_generator
puts power_proc.call #=> 2
puts power_proc.call #=> 4
puts power_proc.call #=> 8

のようにvalueが生き続けるので累積して加算することができます。

別の表記法

Ruby 1.9からは lambda を -> と書けます*3

proc1 = -> arg {puts "In proc1 with #{arg}"}
proc2 = -> arg1, arg2 {puts "In proc2 with #{arg1} and #{arg2}"}

proc1.call "ant" #=> In proc1 with ant
proc2.call "dog", "cat" #=> In proc2 with dog and cat

ただし、

proc1 = lambda {|arg| puts "In proc1 with #{arg}"}
proc2 = -> {|arg| puts "In proc2 with #{arg}"}

proc1.call "cat" #=> In proc1 with ant
proc2.call "cat" #=> proc.rb:53: syntax error, unexpected tSTRING_BEG, expecting keyword_do or '{' or '('

と、ブロック内引数として書くとsyntaxエラーになるようです。
文法の説明は以下の「ブロックの引数リスト」にて改めて見てみます。

この不思議な略式lambdaはメソッドにProc オブジェクトを渡す時に好まれて使われるようです。

def my_if(condition, then_clause, else_clause)
  if condition
    then_clause.call
  else
    else_clause.call
  end
end

5.times do |val|
  my_if val < 3,
    -> {puts "#{val} is small"},
    -> {puts "#{val} is big"}
end

#=> 0 is small
#=> 1 is small
#=> 2 is small
#=> 3 is big
#=> 4 is big

条件で表記を切り替えるためになんでこんな面倒な書き方を... とも思いますが、
ブロック内のコードをいつでも再評価できることが利点になります。

例として上がっているのはwhileループを実装し直した物でした。
while条件として渡すブロックが複雑になったり、動的に変化する場合にも対応できるのが利点ですね。

def my_while (cond, &body) 
  while cond.call
    body.call
  end
end

a = 0
my_while -> {a < 3} do
  puts a
  a += 1
end

ブロックの引数リスト

Ruby 1.9からは、ブロックにも引数リストを指定できるようになったようです。

  • lambdaには従来のブロック構文同様||で囲んだ引数リストを渡す
  • -> にはブロックの前に個々の引数リストを渡す *4
proc1 = lambda do | a, *b, &block|
  puts "a = #{a.inspect}"
  puts "b = #{b.inspect}"
  block.call
end

proc1.call(1,2,3,4) {puts "in block1"}

#=> a = 1
#=> b = [2, 3, 4]
#=> in block1

同様に

proc2 = -> a, *b &block do
  puts "a = #{a.inspect}"
  puts "b = #{b.inspect}"
  block.call
end

*1:File.openはすでにブロックを渡した時に同様の動作をします

*2:それぞれの違いは後ほど確認

*3:λに似ているからとか。

*4:これが前述のエラーの原因!