【Ruby】ブロック

「はじめてのRuby」を読んでのメモ。

今回は第11章「ブロック」


ブロックとは

メソッドの呼び出しの際に引数と一緒に渡すことの出来る処理の塊のこと。ブロックを受け取ったメソッドは必要な回数だけブロックを実行する。

# ブロックは5回実行される
[1, 2, 3, 4, 5].each do |i|
  puts i ** 2
end

do から end までの部分がブロックである。do ~ end の代わりに {} を使っても良い。
|| で囲まれている部分をブロック変数というが、ブロック変数がいくつ渡されるかはメソッドによって異なる。

# 引数1つ
ary = ["a", "b", "c"]
ary.each{|obj| p obj} 

# 引数2つ
ary.each_with_index{|obj, idx|
  p [obj, idx]
}

Array#each_with_index は「要素、そのインデックス」の2つ値がブロックに渡される。

ブロックの使われ方

ブロックの用途は主に3つある。

  • 繰り返し
  • 定形の処理を隠す
  • 計算の一部を差し替える


繰り返し

ブロックつき呼び出しはしばしば繰り返しに用いられる。繰り返しを行うものは特にイテレータと呼ばれ、each メソッドはイテレータの代表的なメソッドである。

alphabet = ["a", "b", "c"]
alphabet.each do |i|
  puts i.upcase  # 要素の文字列を大文字にして出力
end

ハッシュの場合は「[キー, 値]」のペアを配列にして取り出していく。次のようにすれば全てのキーと値のペアを取り出して処理することが出来る。

sum = 0
outcome = {"参加費"=>1000, "ストラップ代"=>1000, "懇親会会費"=>4000}
outcome.each do |pair|
  sum += pair[1]     # 値を指定
end
puts "合計 : #{sum}"  #=> 合計 : 6000

キーのほうを取り出したい場合は pair[0] とする。

キーと値を別の変数で受け取ることもできる。

sum = 0
outcome = {"参加費"=>1000, "ストラップ代"=>1000, "懇親会会費"=>4000}
outcome.each do |item, price|
  sum += price
end
puts "合計 : #{sum}"  #=> 合計 : 6000

また、ファイルオブジェクトには each_line や each_char などのように、データを取り出しながら繰り返しを行うメソッドが「each_○○」という形で多数存在する。

定形の処理を隠す

ブロックには後処理を確実に実行させるための使い方もある。
典型的な例として File.open メソッドがある。

File.open("sample.txt") do |file|
  file.each_line do |line|
    print line
  end
end

通常、開いたファイルを使ったあとは確実に close メソッドでファイルを閉じないと様々な問題の原因になる場合がある。しかし、上の例ではファイルを開けなくてエラーになった場合でもファイルを閉じている。これは、内部的には次のような処理が行われているからである。

file = File.open("sample.txt")
begin 
  file.each_line do |line|
    print line
  end
ensure
  file.close
end

ファイルを使い終わったら閉じるという決まりきった処理はメソッド側で行い、ユーザ側では必要な処理だけをブロック内に記述できるようにすると便利である。

計算の一部を差し替える

配列の並び替えを例に、処理の手順を指定するための使い方を見てみる。

並び替えの順序を指定する

array#sort メソッドでは要素の並べ替えの手順はメソッド内に用意して、要素同士の前後関係を比較する方法だけをユーザが指定するようになっている。array#sort にブロックを指定しなかった場合、それぞれの要素を <=> 演算子で比較した結果順に並べる。
a <=> b の結果は以下のようになる。

a < b -1(0より小)
a == b 0
a > b 1(0より大)
array1 = [2, 3, 1]
p array1.sort    #=> [1, 2, 3]

array2 = ["Ruby", "Perl", "PHP", "php"]
p array2.sort    #=> ["PHP", "Perl", "Ruby", "php"]
# 大文字優先

ブロックを与えない場合と結果が同じようになるようにブロックを与えると、次のようになる。

array1 = [2, 3, 1]
p array1.sort{|a, b| a <=> b}

並び方を変えてみる。

# 降順
p array1.sort{|a, b| b <=> a}            #=> [3, 2, 1]

# 文字列の短い順
p array2.sort{|a, b| a.size <=> b.size}  #=> ["php", "PHP", "Perl", "Ruby"]


並び替えに必要な情報を先に取得する

文字列を長さの順にソートする例を使って、length メソッドが何回呼び出されているかを調べてみる。

call_num = 0     # ブロックの呼び出し回数
sorted = ary.sort do |a, b|
  call_num += 1  # ブロックの呼び出し回数を加算
  a.length <=> b.length
end

puts "ソートの結果 #{sorted}"
puts "配列の要素数 #{ary.length}"
puts "ブロックの呼び出し回数 #{call_num}"

実行結果は次のようになる。

ソートの結果 ["a", "a", "on", "to", "It", "to", ... ]
配列の要素数 28
ブロックの呼び出し回数 91

1回のブロックで length メソッドは2回呼ばれているため、都合182回呼ばれていることになる。変換の結果を <=> 演算子で比較できる場合は、sort_by メソッドを使うと効率よくソートを行うことが出来る。

call_num = 0    
sorted = ary.sort_by do |item|
  call_num += 1
  item.length
end

puts call_num  #=> 28

sort_by メソッドは与えられたブロックを各要素ごとに1回ずつ呼び出した結果を使ってソートを行うため、比較に使う情報を取り出す回数を要素数で済ませることが出来る。

ブロックは、並べ替えの処理の共通部分はメソッドで提供し、並べ替えの順序を差し替えるといった、目的によって異なる部分だけを差し替えるためにも使われる。

ブロックつきメソッドを作る

ブロック変数を渡す、ブロックの結果を得る

def total(from, to)
  result = 0                # 合計の値
  from.upto(to) do |num|    # from から to まで処理する
    if block_given?         #   ブロックがあれば
      result += yield(num)  #     ブロックで処理した値を足す
    else                    #   ブロックがなければ
      result += num         #     そのまま足す
    end
  end
  return result             # メソッドの結果を返す
end

p total(1, 10)                  # 1から10の和 => 55
p total(1, 10){|num| num ** 2}  # 1から10の2乗の値の和 => 385

yield に引数を渡すと、その値がブロック変数としてブロックに渡り、ブロックを実行した結果が yiled の結果となって戻ってきている。
block_given? メソッドは、メソッドの中で使うと、そのメソッドが呼ばれた時にブロックが与えられている場合に true を、そうでない場合に false を返す。


yiled に0個、1個、3個の複数の引数を渡してみる。

def block_args_test
  yield()
  yield(1)
  yield(1, 2, 3)
end

puts "ブロック変数を |a| で受け取る"
block_args_test do |a|
  p [a]
end
puts

puts " ブロック係数を|a, b, c|で受け取る"
block_args_test do |a, b, c|
  p [a, b, c]
end
puts

puts "ブロック係数を|*a|で受け取る"
block_args_test do |*a|
  p [a]
end
puts

実行結果は次のようになる。

ブロック変数を |a| で受け取る
[nil]
[1]
[1]

 ブロック係数を|a, b, c|で受け取る
[nil, nil, nil]
[1, nil, nil]
[1, 2, 3]

ブロック係数を|*a|で受け取る
[[]]
[[1]]
[[1, 2, 3]]

ブロック変数が多い場合は nil となり、ブロック変数の数が足りない場合は値が受け取れないだけで、yield 引数の数とブロック変数の数は違っていても問題ない。

また、配列で入れ子になった値を代入で取り出す場合と同様のルールが、ブロック変数にも当てはまる。Hash.each_with_index のブロック変数は2つで、「yield([キー, 値], インデックス)」の形式で渡される。キーと値を最初から個別の変数に代入できるので便利。

hash = {a: 100, b: 200, c: 300}
hash.each_with_index do |(key, value), index|
  p [key, value, index]
end

実行結果は次のようになる。

[:a, 100, 0]
[:b, 200, 1]
[:c, 300, 2]


ブロックの実行を制御する

  • break

ブロックの中で break を呼ぶとブロックつき呼び出しの場所まで一気に戻ってくるため、結果を返す処理などが全て飛ばされてしまう。メソッドの結果として何か値を返したい場合は「break 0」のように引数を与えてやるとよい。引数を与えなかった場合 nil が返される。

  • next

ブロックの中で next を使った場合は、ブロックのその回の実行を中断する。中断するのはその回だけなので、続きはそのまま実行される。 break と同様、戻り値は何も指定しなければ nil が、「next 0」のように引数を与えると、その値が返される。

  • redo

redo をブロックで呼んだ場合は、ブロックの先頭に戻って同じブロック変数のままはじめからブロックを実行する。ブロックの中の処理でなんとかして同じように redo が呼ばれないようにしないと永遠とブロックの実行が繰り返されるので注意。

ブロックをオブジェクトとして受け取る

ブロックをオブジェクトとして受け取る方法もある。これにより、ブロックを受け取ったメソッドとは別の場所でブロックを実行したり、ブロックを別のメソッドに与えて実行したりできるようになる。
このような場合に登場するのが Proc オブジェクトである。これは、ブロックをオブジェクトとして持ち運ぶためのクラスである。Proc オブジェクトを作る典型的な方法は、Proc.new メソッドをブロックつきメソッドとして呼び出すことである。ブロックの手続きは、Proc オブジェクトに対して call メソッドで呼び出されるまでは実行されない。

hello = Proc.new do |name|
  puts "Hello, #{name}."
end

hello.call("World")  #=> Hello, World.
hello.call("Ruby")   #=> Hello, Ruby.

メソッドからメソッドにブロックを渡すときにはブロックを Proc オブジェクトとして変数で受け取り、次のメソッドに渡すという操作を行う。メソッド定義の際に最後の引数を「&引数名」の形式にすると、そのメソッドを呼び出すときに与えられたブロックは自動的に引数として渡される。この引数を Proc 引数という。メソッド呼び出しの際にブロックが渡されなければ Proc 引数は nil になる。


Proc オブジェクトをブロックとしてほかのメソッドに渡すこともできる。この場合、メソッド呼び出しの際に引数を「&Proc オブジェクト」の形式で指定する。

def call_each(ary, &block)
  ary.each(&block)
end

call_each [1, 2, 3] do |item|
  p item
end


ローカル変数とブロック変数

ブロックは名前空間をブロックの外側と共有しているので、ブロックの外側で作られたローカル変数はブロックの中でも引き続き使える。
一方、ブロック変数として使われる変数はブロックの外側に同じ名前の変数が合ったとしても別物として扱われる。

x = 1
y = 1
ary = [1, 2, 3]

ary.each {|x| y = x}

p [x, y]    #=> [1, 3]

逆に、ブロック内で初出の変数はブロックの外側に持ち出せない。

x = 1
#y = 1     # y を初期化しない
ary = [1, 2, 3]

ary.each {|x| y = x}

p [x,y]    #=> Error (NameError)

ブロック変数とは別に、ブロック内でのみ有効な変数(ブロックローカル変数)を定義することも可能。

x = y = z = 0
ary = [1, 2, 3]
ary.each do |x; y|    # ブロック変数の後ろに ; で区切って定義
  y = x
  z = x
  p [x, y, z]
end
puts
p [x, y, z]

x と y の値は更新されず、次の結果になる。

[1, 1, 1]
[2, 2, 2]
[3, 3, 3]

[0, 0, 3]



この本もようやく折り返し地点。