Ruby実践力向上ガイド
クロージャと動的実行
コードを渡すということ
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らしいコードを書く鍵となります。
| 特徴 | Proc | Lambda |
|---|---|---|
| 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らしい、簡潔で力強いコードが書けるようになります。