No history yet

クロージャと動的実行

コードを渡すということ

Rubyの大きな特徴の一つは、メソッドに「コードの断片」を直接渡せることです。このコードの断片は「ブロック」と呼ばれます。多くのメソッド、特にイテレータは、この仕組みを利用して柔軟な処理を実現しています。

メソッド内で渡されたブロックを実行するには yield キーワードを使います。yield は、メソッドの実行を一時中断し、ブロックに制御を移します。ブロックの処理が終わると、実行は yield の次の行から再開されます。

def process_data
  puts "処理を開始します。"
  # ここでブロックが実行される
  yield
  puts "処理を終了しました。"
end

process_data do
  puts "データ処理中です..."
end

#=> 処理を開始します。
#=> データ処理中です...
#=> 処理を終了しました。

yield に引数を渡すと、それがブロックの引数として受け取られます。これにより、メソッド内部のデータをブロックに渡して処理させることが可能になります。

def greet(name)
  # ブロックに'name'を渡す
  puts yield(name)
end

greet("Alice") do |n|
  "こんにちは、#{n}さん!"
end

#=> こんにちは、Aliceさん!

ブロック、Proc、そしてLambda

ブロックは便利な構文ですが、それ自体はオブジェクトではありません。そのため、変数に代入したり、メソッドの返り値として使ったりすることはできません。ブロックをオブジェクトとして扱いたい場合、Procオブジェクトに変換する必要があります。

# ブロックをProcオブジェクトに変換して変数に格納
greeter = Proc.new do |name|
  "ようこそ、#{name}さん"
end

# callメソッドで実行
puts greeter.call("Bob")
#=> ようこそ、Bobさん

Procと似たものにLambdaがあります。lambda メソッドやリテラル構文 -> で生成でき、Procオブジェクトの一種ですが、いくつかの重要な違いがあります。この2つは似て非なるものであり、挙動の違いを理解することがRubyらしいコードを書く鍵となります。

特徴ProcLambda
returnの挙動定義されたスコープから抜ける自身の呼び出し元から抜ける
引数のチェック緩やか(数が違ってもOK)厳密(数が違うとエラー)

まず return の挙動を見てみましょう。Proc内の return は、Procを定義したメソッド自体から抜けてしまいます。一方、Lambda内の return は、Lambdaの実行を終了し、呼び出し元のメソッドに制御を戻すだけです。これは通常のメソッド呼び出しと同じ感覚で使えます。

def proc_return_test
  my_proc = Proc.new { return "Procからreturnしました" }
  my_proc.call
  return "メソッドの最後です"
end

def lambda_return_test
  my_lambda = -> { return "Lambdaからreturnしました" }
  my_lambda.call
  return "メソッドの最後です"
end

puts proc_return_test    #=> Procからreturnしました
puts lambda_return_test  #=> メソッドの最後です

次いで、引数の扱いです。Lambdaは引数の数に厳密で、数が合わないと ArgumentError が発生します。対してProcは柔軟で、引数が足りなければ nil が入り、多ければ無視されます。

my_proc = Proc.new { |a, b| "Proc: a=#{a.inspect}, b=#{b.inspect}" }
my_lambda = ->(a, b) { "Lambda: a=#{a.inspect}, b=#{b.inspect}" }

puts my_proc.call(1)       #=> Proc: a=1, b=nil
puts my_lambda.call(1, 2)  #=> Lambda: a=1, b=2

# こちらはエラーになる
# puts my_lambda.call(1) #=> ArgumentError: wrong number of arguments

使い分けの基準: メソッドのように振る舞ってほしい場合はLambdaを、より柔軟なブロックとして使いたい場合はProcを選ぶのが一般的です。

クロージャとしての振る舞い

ProcやLambdaは、単なるコードの断片ではありません。これらはクロージャ(閉包)として機能します。クロージャとは、それが定義された時点のスコープ(環境)を記憶し、後からその環境にある変数にアクセスできるオブジェクトのことです。

def counter_generator
  count = 0
  # このProcは、変数`count`を記憶する
  Proc.new do
    count += 1
    puts "カウント: #{count}"
  end
end

# counterは`count`変数が0の状態を記憶している
counter = counter_generator

counter.call #=> カウント: 1
counter.call #=> カウント: 2

# 新しく作れば、また別の`count`が生まれる
new_counter = counter_generator
new_counter.call #=> カウント: 1

上の例では、counter_generator メソッドの実行が終わった後も、counter というProcオブジェクトは変数 count への参照を保持し続けています。これにより、counter.call が呼ばれるたびに状態(count の値)を更新できます。これは、オブジェクト指向におけるプライベートなインスタンス変数のように、状態をカプセル化する強力なテクニックです。

コンテキストを操る

Rubyでは、self は常に現在のオブジェクトを指します。しかし、ブロックを実行する際の self を意図的に変更したい場合があります。特に、DSL(ドメイン固有言語)を構築する際にこのテクニックが非常に役立ちます。そのために使われるのが instance_eval です。

class Config
  attr_accessor :host, :port

  def setup(&block)
    # ブロック内の'self'をConfigインスタンス自身に変更して実行
    instance_eval(&block)
  end
end

config = Config.new
config.setup do
  # このブロックの中では self が config オブジェクトになる
  self.host = "localhost"
  self.port = 8080
end

puts "Host: #{config.host}, Port: #{config.port}"
#=> Host: localhost, Port: 8080

instance_eval は、レシーバオブジェクトのコンテキストでブロックを実行します。これにより、ブロック内からそのオブジェクトのインスタンス変数やメソッドに直接アクセスできるようになります。Railsのルーティング (routes.rb) や設定ファイルは、この仕組みを多用して、簡潔で読みやすいDSLを提供しています。

最後に、便利なショートカットを紹介します。メソッド呼び出しの最後の引数として &:メソッド名 という形式を使うと、シンボルをProcオブジェクトに変換できます。これは、各要素に対して同じメソッドを呼び出すだけのシンプルなブロックを渡す際に非常に便利です。

numbers = ["1", "2", "3"]

# 通常のブロック
integers = numbers.map { |s| s.to_i }
#=> [1, 2, 3]

# &演算子を使ったショートカット
integer_short = numbers.map(&:to_i)
#=> [1, 2, 3]

# 上の行は内部的に以下のように解釈される
# numbers.map { |s| s.send(:to_i) }

この &:to_i は、Rubyの柔軟性と表現力を象徴する構文の一つです。ブロック、Proc、Lambda、そしてコンテキストの操作を理解することで、よりRubyらしい、簡潔で力強いコードが書けるようになります。