Rubyのブロック、Proc、lambdaを理解する
概要
この記事は、「メタプログラミングRuby 第2版」第4章を読み学習した内容を個人学習用にまとめ直したものです。
Rubyのブロックは、強力かつ柔軟な機能の一つです。本記事では、ブロックの基本的な仕組みから、Proc、lambdaとの違い、実践的な使い方まで解説します。
ブロックはクロージャ
ブロックを{}もしくはdo...endで定義すると、定義された同じレベルに存在する変数を参照することができる。
yieldでメソッドに渡した場合などでは、この変数への参照も引き継ぐことができる(クロージャ)。
def my_method
x = "Goodbye"
yield("cruel")
end
x = "Hello"
# ブロックを作成すると、同じレベルのxへの参照を引き継ぐ
my_method {|y| "#{x}, #{y} world"} # => "Hello, cruel world"
Ruby 3.4以降では、ブロック引数が1つの場合にitで暗黙的に参照できる。
[1, 2, 3].map {|x| x + 1} # 従来の書き方
[1, 2, 3].map { it + 1 } # itで簡潔に書ける(Ruby 3.4+)
一方、ブロック内で作成した変数はブロックの外では参照できない。
def just_yield
yield
end
top_level_variable = 1
just_yield do
top_level_variable += 1
local_to_block = 1 # 新しい変数を定義
end
top_level_variable # => 2
local_to_block # => Error
スコープゲートとは?
プログラムが以下の境界に出入りすると、スコープが変化する(スコープゲート)。
- クラス定義(
class) - モジュール定義(
module) - メソッド(
def)
# 外側のスコープ
variable = "外側の変数"
class MyClass
# クラス定義のスコープゲート
# puts variable # => NameError: 外側の変数にアクセスできない
class_variable = "クラス内の変数"
def my_method
# メソッド定義のスコープゲート
# puts variable # => NameError
# puts class_variable # => NameError
method_variable = "メソッド内の変数"
puts method_variable # => OK
end
module MyModule
# モジュール定義のスコープゲート
# puts variable # => NameError
# puts class_variable # => NameError
end
end
スコープを共有する方法
フラットスコープ
スコープゲートのキーワードを代用できるメソッド(Class.new, define_method)呼び出しに変更すれば、ゲートの外のスコープを共有できるようになる(フラットスコープ)。
variable = "外側の変数"
MyClass = Class.new do # classの代用
# puts variable # => OK
class_variable = "クラス内の変数"
define_method :my_method do # defの代用
# puts variable # => OK
# puts class_variable # => OK
end
end
共有スコープ
複数のメソッドで変数を共有したいが、その他からは見えないようにしたい場合はスコープゲートでスコープを閉じた後に、フラットスコープで変数を共有すればいい(共有スコープ)。
def define_methods # スコープゲートで外部から遮断
shared = 0
# Kernelメソッドをフラットスコープ化
Kernel.send(:define_method, :increment) do
shared += 1
end
Kernel.send(:define_method, :counter) do
shared
end
end
define_methods
increment # => 1
increment # => 2
counter # => 2
# shared変数には外部から直接アクセスできない(カプセル化されている)
# puts shared # => NameError
instance_eval
BasicObject#instance_evalを使用することで、オブジェクトのコンテキストでブロックを評価することもできる。
instance_evalに渡したブロックは、レシーバをselfにしてから評価され、レシーバのprivateメソッドやインスタンス変数にアクセスできる。
また、他のブロックと同様、instance_evalを定義したレベルの変数も参照できる。
class MyClass
def initialize
@value = "インスタンス変数"
end
private
def secret_method
"プライベートメソッド"
end
end
obj = MyClass.new
variable = 1
# instance_evalでオブジェクトのコンテキストに入る
obj.instance_eval do
puts self.class # => MyClass(selfがobjになる)
puts @value # => "インスタンス変数"(インスタンス変数にアクセス可能)
puts secret_method # => "プライベートメソッド"(プライベートメソッドも呼べる)
puts variable # => 1(通常のブロックと同様に定義箇所と同じレベルの変数を参照できる)
end
# 外側からは通常アクセスできない
# obj.secret_method # => NoMethodError
instance_exec
以下の例のように、インスタンス変数はself(レシーバ)のコンテキストで評価されるので、instance_evalブロックの外側で定義されたインスタンス変数にはアクセスできない。
class C
def initialize
@x = 1
end
end
class D
def twisted_method
@y = 2
C.new.instance_eval { "@x: #{@x}, @y: #{@y}"} # ここでレシーバが変更される
end
end
# インスタンス変数はself(レシーバ)によって決まるので外側の@yにはアクセスできない
D.new.twisted_method # => "@x: 1, @y: "
オブジェクトのコンテキストで処理を実行しつつ、外部から値を渡したい場合はinstance_execを用いる。
class D
def twisted_method
@y = 2
C.new.instance_exec(@y) {|y| "@x: #{@x}, @y: #{y}"}
end
end
D.new.twisted_method # => "@x: 1, @y: 2"
クリーンルーム
ブロックを評価するためだけに作られたオブジェクトをクリーンルームと呼ぶ。
既存のオブジェクトでinstance_evalを使うと、そのオブジェクトのインスタンス変数やメソッドと衝突するリスクがある。
クリーンルームを使えば、余計な状態を持たない安全な環境でブロックを評価できる。
# 既存のオブジェクトで評価すると衝突のリスクがある
class MyClass
def initialize
@name = "元の値"
end
end
obj = MyClass.new
obj.instance_eval { @name = "上書き" } # 既存の@nameを壊してしまう
# クリーンルーム: 空のオブジェクトで安全に評価する
clean = BasicObject.new
clean.instance_eval do
@name = "新しい値" # 衝突の心配がない
end
BasicObjectはRubyのクラス階層の最上位にあり、メソッドが最小限なので、よりクリーンルームとして適している。
ブロックを呼び出し可能オブジェクトにする
呼び出し可能オブジェクト
評価ができてスコープが持ち運べるコードのこと。
Procオブジェクト
ブロックをオブジェクトに変換するために、Procクラスが用意されている。
inc = Proc.new {|x| x + 1} # ブロックを渡してオブジェクトを作成
inc.call(2) # ブロックを後で評価できる
# その他のProcオブジェクトを作成する方法
# inc = lambda {|x| x + 1}
# inc = ->(x) { x + 1 }
& 修飾でブロックとオブジェクトを相互に変換する
ブロックとProcオブジェクトを相互に変換するには&修飾を使う。
ブロック → Proc(メソッドの引数で受け取る)
メソッド定義の最後の引数に&を付けると、渡されたブロックがProcオブジェクトに変換される。
def my_method(&block)
block.class # => Proc(ブロックがProcに変換されている)
block.call # Procオブジェクトとして呼び出せる
end
my_method { "ブロックです" } # => "ブロックです"
Proc → ブロック(メソッド呼び出しで渡す)
逆に、Procオブジェクトに&を付けてメソッドに渡すと、ブロックに変換される。
my_proc = Proc.new { |x| x * 2 }
# &を付けてProcをブロックとして渡す
[1, 2, 3].map(&my_proc) # => [2, 4, 6]
# 上記は以下と同じ意味
[1, 2, 3].map { |x| x * 2 } # => [2, 4, 6]
メソッドを呼び出し可能オブジェクトにする
Object#methodを使うと、メソッドをMethodオブジェクトとして取得できる。MethodオブジェクトはProcと同様にcallで呼び出せる。
class MyClass
def my_method(x)
x * 3
end
end
obj = MyClass.new
# メソッドをMethodオブジェクトとして取得
m = obj.method(:my_method)
m.class # => Method
m.call(2) # => 6
MethodとProcの違い
Methodオブジェクトは定義されたオブジェクトのスコープで評価される。一方、Proc(lambda)は定義されたスコープ(クロージャ)で評価される。
class MyClass
def initialize
@x = 10
end
def my_method
@x
end
end
# Methodオブジェクト: 定義されたオブジェクトのスコープで評価される
m = MyClass.new.method(:my_method)
m.call # => 10
# lambda: 定義されたスコープ(クロージャ)で評価される
x = 20
l = -> { x }
l.call # => 20
UnboundMethod
Module#instance_methodを使うと、特定のオブジェクトに束縛されていないUnboundMethodを取得できる。UnboundMethodはそのままでは呼び出せず、bindでオブジェクトに束縛してから使う。
# UnboundMethodを取得
unbound = MyClass.instance_method(:my_method)
unbound.class # => UnboundMethod
# unbound.call # => エラー: そのままでは呼び出せない
# オブジェクトに束縛して呼び出す
bound = unbound.bind(MyClass.new)
bound.call # => 10
# 別のクラスに束縛して呼び出すこともできる
another_bound = unbound.bind(AnotherClass.new)
bound.call # => 20
呼び出し可能オブジェクトのまとめ
全て、Proc.new、Method#to_proc、&修飾などを使ってある呼び出し可能オブジェクトから別の呼び出し可能オブジェクトに変換できる。
| 種類 | クラス | スコープ | return | 呼び出し時の引数 |
|---|---|---|---|---|
| ブロック | オブジェクトではないが呼び出し可能 | 定義されたスコープで評価 | 呼び出し可能オブジェクトの元のコンテキストから戻る | 引数は柔軟(足りない場合はnilで補完) |
| Proc(非lambda) | Proc | 定義されたスコープで評価 | 呼び出し可能オブジェクトの元のコンテキストから戻る | 引数は柔軟(足りない場合はnilで補完) |
| Proc(lambda) | Proc(lambda?がtrue) |
定義されたスコープで評価 | 呼び出し可能オブジェクトから戻る | 引数は厳密(足りない場合はArgumentError) |
| Methodオブジェクト | Method | 定義されたオブジェクトのスコープで評価 | 呼び出し可能オブジェクトから戻る | 引数は厳密(足りない場合はArgumentError) |
def proc_return
p = Proc.new { return "proc から return" }
p.call
"ここには到達しない"
end
def lambda_return
l = lambda { return "lambda から return" }
l.call
"ここに到達する"
end
# Proc の return はメソッド自体を抜ける
puts proc_return # => "proc から return"
# lambda の return は lambdaの中だけで完結する
puts lambda_return # => "ここに到達する"
まとめ
- ブロックは定義時のスコープを持ち運ぶクロージャである
-
class/module/defはスコープゲートとしてスコープを区切る -
Class.newやdefine_methodでスコープゲートを回避し、変数を共有できる(フラットスコープ) -
instance_evalでオブジェクトのコンテキストに入り、privateメソッドやインスタンス変数にアクセスできる - ブロックは
Proc.newやlambdaで呼び出し可能オブジェクトに変換でき、&修飾でブロックとProcを相互変換できる -
Methodオブジェクトは定義されたオブジェクトのスコープで、Procは定義されたスコープ(クロージャ)で評価される
参考文献
この記事は以下の情報を参考にして執筆しました。