Ruby 入門

Ruby コードブロック (Blocks)

コードブロック(Blocks)は Ruby において極めてコアなベーシック・コンセプトです。これらはメソッドにパッシング(伝達)できるコード・フラグメントであり、パワフルでフレキシブルなプログラミング・ケイパビリティを提供します。コードブロックをユースすることで、メソッド自体をモディファイ(変更)することなく、メソッドのビヘイビア(振る舞い)をカスタマイズすることが可能になります。

コードブロックの理解は、Ruby のエクスプレッシブネス(表現力)をマスターし、その特性をフル活用するためのキーとなります。本章では、コードブロックをディファイン(定義)する2つのアプローチ:do...end のユースと {} のユースについて探求します。それらのシンタックス(構文)、プレセデンス(優先順位)の差異、およびそれぞれの最適なアプリケーション・シナリオについてディスカッションします。これらのナレッジは、後続で学習するメソッド内でのコードブロックのハンドリングや、Procs、Lambdas といったハイレベル・コンセプトを深く理解するための強固な基盤となるでしょう。

1. コードブロックの理解:do...end と {}

Ruby において、コードブロックとはメソッドのコール(呼び出し)にアタッチできるコードのセグメントです。これを、パラメータとしてパッシング可能なネームレス・メソッド(無名メソッド)としてイメージすると良いでしょう。コードブロック自体はオブジェクトではありませんが、オブジェクト(すなわち Procs および Lambdas、これらは後続のモジュールでカバーします)にコンバートすることが可能です。コードブロック最大のメリットは、メソッド内部にカスタム・ロジックをインジェクト(注入)できる点にあります。

Ruby にはコードブロックをディファインする2つのアプローチが存在します:

  • do...end キーワードをユースする
  • カーリーブレース {} をユースする

サンプルを通じてそれぞれを探索していきましょう。

2. do...end コードブロック

do...end ストラクチャーは、通常、マルチライン(複数行)のコードブロックをディファインするためにユースされます。これは do キーワードでスタートし、end キーワードで終了します。アソシエート(関連付け)されたメソッドが該当コードブロックをコールした際、do...end 内部のコードがエグゼキュート(実行)されます。

# サンプル:each メソッドと do...end のコンバイン
numbers = [1, 2, 3, 4, 5]

numbers.each do |number|
  puts "カレント・ナンバーは: #{number} です"
  puts "該当ナンバーの平方は: #{number * number} です"
end

puts "---"

# コンディション(条件)チェックを含んだ別のサンプル
numbers.each do |number|
  puts "処理中: #{number}"
  puts "#{number} は偶数です" if number.even?
end

このサンプルにおいて、numbers.eachnumbers アレイ(配列)内の各エレメントをトラバース(走査)するメソッドです。do...end コードブロックは、各エレメントに対してエグゼキュートする具体的なオペレーションをディファインしています。|number| のセクションは、カレントのエレメントをパラメータとしてコードブロック内部にパッシングするためにユースされます。

3. {} コードブロック

カーリーブレース {} は、通常、シングルライン(単一行)のコードブロックをディファインするためにユースされます。マルチラインのシナリオでもユース可能ですが、コードのリーダビリティ(可読性)の観点から、マルチラインの場合は一般的に do...end をユースすることが推奨されます。do...end と比較して、{} のシンタックスはよりコンサイス(簡潔)です。

# サンプル:each メソッドと {} のコンバイン
numbers = [1, 2, 3, 4, 5]

numbers.each { |number| puts "カレント・ナンバーは: #{number} です" }

puts "---"

# コードブロック内にマルチプルなステートメントを含む別のサンプル(セミコロンまたはニューラインをユース)
numbers.each { |number|
  puts "処理中: #{number}"
  puts "#{number} は偶数です" if number.even?
}

このサンプルは前のものと類似していますが、do...end の代わりにカーリーブレース {} をユースしています。{} 内にマルチプルなステートメントを記述する場合、コードをクリーンに保つために、do...end のマルチライン・フォーマットへリファクタリングすることが強く推奨されます。

4. コードブロック・パラメータ

do...end であっても {} であっても、パラメータをレシーブすることが可能です。パラメータはコードブロックの開始部分にある2本のパイプ | | の間にディファインされます。これらのパラメータの数量やミーニング(意味)は、該当コードブロックをコールするメソッドによってディサイド(決定)されます。

# サンプル:マルチプルなパラメータを含むコードブロック
hash = { a: 1, b: 2, c: 3 }

hash.each do |key, value|
  puts "キー: #{key}, バリュー: #{value}"
end

puts "---"

hash.each { |key, value| puts "キー: #{key}, バリュー: #{value * 2}" } # シングルライン {}

このサンプルにおいて、hash オブジェクトの each メソッドは、ハッシュ内の各キーバリュー・ペア (key-value pair) の keyvalue という2つのパラメータをコードブロックにパッシングします。

5. コアな差異:プレセデンス

do...end{} の間の最も主要なディファレンス(違い)は、それらのプレセデンス(優先順位)にあります。

カーリーブレース {} のプレセデンスは do...end よりもハイレベル(高い)です。これは、Ruby が {} コードブロックを、そのライン上で最も近くにあるメソッドとタイトにバインド(結合)させようと試みることを意味します。これに注意を払わないと、予期せぬリザルトを招く可能性があります。対照的に、do...end のプレセデンスは低いため、メソッドチェーンや複雑なエクスプレッション(式)をハンドリングする際、そのインテント(意図)がよりクリアになる傾向があります。

# サンプル:プレセデンスのディファレンスのデモンストレーション
def some_method(arg)
  puts "メソッドがコールされました。パラメータ: #{arg.inspect}"
  yield if block_given? # yield については後述します。その役割はコードブロックのエグゼキュートです。
  return arg
end

# do...end のユース
result = some_method([1, 2, 3]) do |x|
  puts "コードブロックがエグゼキュートされました"
end

puts "---"

# {} のユース - ここのディファレンスに注意してください!
result = some_method([1, 2, 3]) { |x| puts "コードブロックがエグゼキュートされました" }

puts "---"

# プレセデンスのトラップを示す別のクラシックなサンプル
def another_method
  return 1, 2, 3
end

p another_method.map { |x| x * 2 } # 正解 - コードブロックが正確に map メソッドにパッシングされました
p another_method.map do |x| x * 2 end # リザルトは期待と異なります - do...end のプレセデンスは低いため、map ではなく一番外側の p メソッドにパッシングされてしまいます

最後のサンプルにおいて、{} はハイ・プレセデンスであるため、左側の最も近い map メソッドにタイトに「ホールド」されます。しかし do...end はロー・プレセデンスであるため、map をオーバーパスして、自身をライン全体の一番外側にある p メソッドにバインドしようと試み、プログラムのビヘイビアが期待と合致しなくなります。

6. どのコードブロックをユースすべきか?

以下は、do...end{} の間でセレクトする際の一般的なガイドラインです:

  • do...end:マルチラインのコードブロック、特にコードブロック内に複雑なロジックが含まれる場合にユースします。また、メソッドチェーン・コールを行う場合や、レギュラー・パラメータを持つメソッドをコールし、コードブロックをそのメソッドにクリアにアソシエートさせたい場合にもユースしてください。
  • {}:コードをコンサイスに保つためのシングルラインのコードブロックにユースします。エクスプリシット(明示的)にハイ・プレセデンスを要求する場合(例:チェーン・コール内の特定のメソッドにコードブロックをパラメータとしてパッシングする場合)にも、これをユースしてください。

6.1 サマリー比較テーブル

プロパティdo...end{}
シンタックスdo ... end{ ... }
マルチラインの適性マルチラインのコードブロックに推奨ユース可能だが、マルチライン時のリーダビリティは劣る
シングルラインの適性ユース可能だが、コンサイスさに欠けるシングルラインのコードブロックに推奨
プレセデンスローレベル (バインディングがルーズ)ハイレベル (バインディングがタイト)
リーダビリティ複雑なロジックに適し、ストラクチャーがクリアシンプルなロジックに適し、コードがコンパクト

7. 実践ユースケースとデモンストレーション

実際のデベロップメントにおいて、これら2つのコードブロックをどのようにユースするかのサンプルを見ていきましょう。

7.1 アレイのフィルタリング (select メソッド)

# do...end のユース
numbers = [1, 2, 3, 4, 5, 6]

even_numbers = numbers.select do |number|
  number.even?
end
puts "偶数: #{even_numbers}" # アウトプット: 偶数: [2, 4, 6]

puts "---"

# {} のユース
odd_numbers = numbers.select { |number| number.odd? }
puts "奇数: #{odd_numbers}" # アウトプット: 奇数: [1, 3, 5]

ここでは、アレイのナンバーをフィルタリングするために select メソッドをユースしています。コードブロックはフィルタリングのコンディション(true をリターンするエレメントをキープする)をディファインしています。

7.2 平方和のコンピュート

# do...end のユース
numbers = [1, 2, 3, 4, 5]
sum_of_squares = 0

numbers.each do |number|
  sum_of_squares += number * number
end
puts "平方和: #{sum_of_squares}" # アウトプット: 平方和: 55

puts "---"

# {} のユース (inject メソッドとのコンバインでさらにコンサイスに)
numbers = [1, 2, 3, 4, 5]
sum_of_squares = numbers.inject(0) { |sum, number| sum + number * number }
puts "平方和: #{sum_of_squares}" # アウトプット: 平方和: 55

このサンプルでは、アレイ・エレメントの平方和をコンピュートしています。{} バージョンのコードは、アドバンスドな inject メソッドとシングルラインのコードブロックを組み合わせて、極めてコンパクトなコードを実装する方法をデモンストレーションしています。

7.3 ハッシュ (Hash) のトラバース

# do...end のユース
person = { name: "Alice", age: 30, city: "New York" }

person.each do |key, value|
  puts "キー: #{key}, バリュー: #{value}"
end

puts "---"

# {} のユース
person.each { |key, value| puts "#{key.capitalize}: #{value}" }

7.4 メソッドチェーンとプレセデンスの実践

def modify_array(array)
  array.map { |x| x * 2 }.select { |x| x > 5 }
end

numbers = [1, 2, 3, 4, 5]
modified_numbers = modify_array(numbers)
puts modified_numbers.inspect # アウトプット: [6, 8, 10]

ここでは、{} のハイ・プレセデンスにより、2つのコードブロックがそれぞれ対応する mapselect メソッドに正確かつタイトにバインドされています。もしこれを do...end にリプレイス(置換)した場合、コードは高い確率でエラーをスローするか、括弧を使用してエグゼキュートのシーケンスを強制的かつエクスプリシットに指定する必要が生じます。