Ruby 入門

Ruby メソッドとコードブロックのインタラクション

Ruby において、コードブロック (Blocks) はメソッドにパッシング(伝達)できるコード・フラグメントです。これらはカスタム・メソッドのビヘイビア(振る舞い)を定義するための極めてパワフルなアプローチです。コードブロック自体はオブジェクトではありませんが、オブジェクト(すなわち Proc、これについては後続のモジュールでカバーします)にコンバートすることが可能です。これらは他のプログラミング言語におけるアノニマス・ファンクション(無名関数)や Lambda エクスプレッション(Lambda 式)にシミラー(類似)しています。

前章では、do...end{} をユースしてコードブロックをディファインするベーシックなナレッジを学習しました。本章では、コードブロックをメソッドにパッシングする方法と、メソッドの内部でこれらのコードブロックをどのように利用するのかを深く探求します。

1. コードブロックのパッシングの基礎

メソッドをディファインする際、コードブロックをパラメータとしてエクスプリシット(明示的)に宣言する必要はありません。代わりに、メソッドをコールする際にコードブロックがプロバイド(提供)された場合、メソッドはそれをインプリシット(暗黙的)にレシーブします。メソッドは block_given? メソッドをユースしてコードブロックをレシーブしたかどうかをジャッジし、yield キーワードをユースしてそのコードブロックをエグゼキュート(実行)することができます。

シンプルなサンプルを見てみましょう:

def my_method
  puts "my_method をエグゼキュート中"
  if block_given?
    puts "コードブロックを検知しました。yield を通じてコントロールをコードブロックにトランスファーします"
    yield
  else
    puts "コードブロックはプロバイドされていません"
  end
  puts "my_method をエグジットします"
end

my_method
# アウトプット: 
# my_method をエグゼキュート中
# コードブロックはプロバイドされていません
# my_method をエグジットします

my_method do
  puts "コードブロック内部のロジックをエグゼキュート中"
end
# アウトプット: 
# my_method をエグゼキュート中
# コードブロックを検知しました。yield を通じてコントロールをコードブロックにトランスファーします
# コードブロック内部のロジックをエグゼキュート中
# my_method をエグジットします

このサンプルにおいて、my_methodblock_given? をユースしてコードブロックがプロバイドされたかどうかをチェックします。プロバイドされている場合、yield をユースしてそのコードブロックをエグゼキュートします。そうでない場合は、「コードブロックはプロバイドされていません」とプリント(出力)します。

コード解析:

  • def my_method: my_method というネームのメソッドをディファインします。
  • block_given?: これは Ruby のビルトイン・メソッドです。カレントのメソッドをコールする際にコードブロックがパッシングされた場合、true をリターンし、そうでない場合は false をリターンします。
  • yield: これはパッシングされたコードブロックをコールするためのキーワードです。プログラムのエグゼキュート・コントロールを一時的にコードブロックへとトランスファー(移譲)するような役割を果たします。
  • my_method do ... end: コードブロックを伴って my_method をコールします。do...end 内部のコードが、パッシングされるコードブロックとなります。

2. コードブロックへのパラメータのパッシング

メソッドはコードブロックをエグゼキュートするだけでなく、yield をユースしてコードブロックにパラメータをパッシングすることも可能です。その後、コードブロックはこれらのパラメータを自身のローカル・バリアブル(局所変数)としてレシーブします。

def my_method_with_args
  puts "my_method_with_args をエグゼキュート中"
  if block_given?
    puts "コードブロックを検知しました。yield をエグゼキュートし、パラメータをパッシングします"
    yield("Hello", "World")
  else
    puts "コードブロックはプロバイドされていません"
  end
  puts "my_method_with_args をエグジットします"
end

my_method_with_args do |arg1, arg2|
  puts "コードブロックをエグゼキュート中、レシーブしたパラメータ: #{arg1}, #{arg2}"
end

# アウトプット:
# my_method_with_args をエグゼキュート中
# コードブロックを検知しました。yield をエグゼキュートし、パラメータをパッシングします
# コードブロックをエグゼキュート中、レシーブしたパラメータ: Hello, World
# my_method_with_args をエグジットします

コード解析:

  • yield("Hello", "World"): ストリング "Hello""World" をパラメータとしてコードブロックにパッシングします。
  • do |arg1, arg2|: 2つのパラメータ arg1arg2 を含むコードブロックをディファインします。これらは yield によってパッシングされた値をレシーブします。
  • コードブロック内部の arg1 の値は "Hello" になり、arg2 の値は "World" になります。

2.1 コードブロック・パラメータのネーミングと欠落パラメータ

コードブロックのパラメータのネーミング・ルールは、レギュラーなメソッドのパラメータと同一です。パラメータのミーニング(意味)をクリアにディスクライブ(記述)するネームをセレクトしてください。

もし yield がいかなるパラメータもパッシングしない場合、コードブロックはパラメータ・バリアブルをディファインするべきではありません。もし yield が1つのパラメータしかパッシングしないにもかかわらず、コードブロックが2つのパラメータ・バリアブルをディファインした場合、2番目のパラメータの値は nil となります。

def method_with_optional_args
  yield 1
end

method_with_optional_args { |a, b| puts "a: #{a}, b: #{b}" }
# アウトプット: a: 1, b: 
# (注:b の値は nil であり、ストリング・インターポレーションの際にはブランクとしてディスプレイされます)

3. 一般的なイテレータにおけるコードブロック

多くの Ruby ビルトイン・メソッド(特に eachmapselectreduce といったイテレータ)は、コードブロックをヘビーにユースしています。

3.1 each メソッド

each メソッドはコレクション(例えばアレイ)をトラバース(走査)し、各エレメントをコードブロックに yield するためにユースされます。

numbers = [1, 2, 3, 4, 5]

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

# アウトプット:
# カレントのナンバーは: 1 です
# カレントのナンバーは: 2 です
# カレントのナンバーは: 3 です
# カレントのナンバーは: 4 です
# カレントのナンバーは: 5 です

3.2 map メソッド

map メソッドは、コードブロックのリターン値に基づいてコレクション内の各エレメントをトランスフォーム(変換)し、トランスフォームされたエレメントを含む新規のアレイをリターンします。

numbers = [1, 2, 3, 4, 5]

squared_numbers = numbers.map do |number|
  number * number
end

puts squared_numbers.inspect
# アウトプット: [1, 4, 9, 16, 25]

3.3 select メソッド

select メソッドは、コードブロックのコンディションに基づいてコレクションのエレメントをフィルタリングします。コードブロックが true をリターンしたエレメントのみを含む新規のアレイをリターンします。

numbers = [1, 2, 3, 4, 5]

even_numbers = numbers.select do |number|
  number.even?
end

puts even_numbers.inspect
# アウトプット: [2, 4]

3.4 reduce (または inject) メソッド

reduce(または inject)メソッドは、コードブロックのロジックに基づいて、コレクション内の全エレメントをシングル・バリュー(単一の値)にコンバイン(結合)します。

numbers = [1, 2, 3, 4, 5]

sum = numbers.reduce(0) do |accumulator, number|
  accumulator + number
end

puts sum
# アウトプット: 15

4. 実践ユースケースのデモンストレーション

コードブロックのパワーをより深く理解するために、いくつかの実践的なアプリケーション・シナリオを見ていきましょう。

4.1 ケース 1:カスタム・ロガー

ログをレコーディングするメソッドをクリエイトしましょう。これはタイムスタンプを自動的に追加し、コードブロックをユースしてログ・インフォメーションをダイナミック(動的)にジェネレートします。

def log_message(log_level)
  timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
  message = yield # コードブロックをエグゼキュートし、ログのコンテンツを取得する
  puts "[#{timestamp}] [#{log_level}] #{message}"
end

log_message("INFO") { "アプリケーションのスタートアップに成功しました。" }
# アウトプット・サンプル: [2024-01-01 12:00:00] [INFO] アプリケーションのスタートアップに成功しました。

log_message("ERROR") { "データベースへのコネクションに失敗しました。" }
# アウトプット・サンプル: [2024-01-01 12:00:00] [ERROR] データベースへのコネクションに失敗しました。

4.2 ケース 2:コード・エグゼキュート時間の計測

特定のコードブロックのエグゼキュート時間(プロセシング・タイム)をプロファイリングするためのメソッドをクリエイトできます。

require 'benchmark'

def measure_execution_time
  time = Benchmark.realtime do
    yield # パッシングされたコードブロックをエグゼキュートし、タイムを計測する
  end
  puts "エグゼキュート・タイム: #{time} 秒"
end

measure_execution_time do
  # タイムコンシューミング(時間のかかる)オペレーションをシミュレート
  sleep(2)
end
# アウトプット・サンプル: エグゼキュート・タイム: 2.0012345 秒 (アプロキシメート値)

4.3 ケース 3:リソース管理

ファイルのオープンとクローズをハンドリングするメソッドをクリエイトできます。コードブロックをユースすることで、いかなるエラーが発生した場合でも、ファイルが最終的にセキュアにクローズされることを保証できます(begin...ensure ストラクチャーの活用)。

def with_file(filename, mode)
  file = File.open(filename, mode)
  begin
    yield(file) # コードブロックをエグゼキュートし、ファイル・オブジェクトをパッシングする
  ensure
    file.close  # ファイルが確実にクローズされることを保証する
  end
end

with_file("my_file.txt", "w") do |file|
  file.write("これはファイルにライト(書き込み)されるテキストです。")
end
# "my_file.txt" がクリエイト/オープンされ、コンテンツがライトされた後、自動的かつセキュアにクローズされます。

4.4 ケース 4:シンプルな DSL (ドメイン特化言語) の構築

コードブロックは、シンプルな DSL をクリエイトするために頻繁にユースされます。直感的なコンフィギュレーション(設定)フォーマットをディファインしたいと仮定します:

def configure
  config = {}
  yield config if block_given?
  config
end

my_config = configure do |c|
  c[:name] = "マイ・アプリケーション"
  c[:version] = "1.0"
  c[:author] = "John Doe"
end

puts my_config.inspect
# アウトプット: {:name=>"マイ・アプリケーション", :version=>"1.0", :author=>"John Doe"}