はじめに
Ruby にも、JavaScript で言うところのアロー関数のように、関数を引数で渡したり単体で扱うための関数型プログラミングを行うための仕組みが備わっています。しかし無名関数を扱うために用意されている method や使用方法がいくつかあり、理解に時間がかかったので、n番煎じにはなってしまいますがまとめてみました。
先にまとめ
以下の3つのように理解しました。
- オブジェクト指向言語であるRubyにおいて、手続きそのものもオブジェクトとして扱えると使いまわせて便利。そのオブジェクトがProcオブジェクトである
- Proc オブジェクトの生成方法には複数の方法(lambda, proc(Proc.new), ブロック引数等)があり、使用方法に合わせて使い分ける
- あらゆる言語でラムダ式(無名関数を簡潔に記述する構文)で関数を生成する方法が用意されている。Ruby においては lambda(アロー演算子)がそれに当たる
Ruby において手続きを表すもの
Ruby で手続き(処理の塊)を表現する方法として、block と Proc オブジェクトの2つがあります。
(method の proc とオブジェクトの Proc がややこしいので、それぞれ proc メソッド、Proc オブジェクトと呼ばせてください)
1. block
do |a| ~ end で囲まれた、使い回せない、その場限りの無名関数。
a はブロックパラメータもしくはブロック変数と呼ばれます。
method に一つだけ渡すことができます。
使用例
def method
yield
end
method { p 'これはblockです' }
# => "これはblockです"
yield は、渡された block を呼び出す時に使用します。
使用ケース
- 手続きを使い回さない時
- 手続きを一つしか渡さない時
2. Proc オブジェクト
https://docs.ruby-lang.org/ja/latest/class/Proc.html より
ブロックをコンテキスト(ローカル変数のスコープやスタックフレーム)とともにオブジェクト化した手続きオブジェクト。
Proc はローカル変数のスコープを導入しないことを除いて名前のない関数のように使えます。
block を Proc オブジェクトとして保存し使い回せるようにしたものです。
Proc オブジェクトのmethodでの使用例
methodにはblockを渡すが中でProc オブジェクトとして扱う
呼び出し側では block をmethodに渡したいが、内部ではオブジェクトとして扱いたい。そのような時は、yield ではなく、明示的に&のついたblock引数として受け取るようにします。
そうすることで、block が渡ってきた際に Proc というオブジェクトに変換されるので、method内ではオブジェクトとして扱うことができます。
def method(&block) # blockのオブジェクト化
block.call
end
method { p 'これはProcに変換されたblockです' }
# => "これはProcに変換されたblockです"
使用ケース
- 手続きを一つしか渡さない時
- method内ではオブジェクトとして複雑な処理を行いたい時
block引数を使用しても、blockは複数受け取ることはできません。複数受け取りたい場合は、次のようにあらかじめ Proc に変換したものを普通の引数で渡す必要があります。
また、block引数を受け取ると、Proc オブジェクトに変換する分、速度が落ちます。実際、Ruby 4.0 での計測では yield に比べ block.call は約 30% 以上実行速度が低下しました。そのため、yield で実行できる状況であれば、他の理由がない限りはそちらを使うのがよさそうです。
Procオブジェクトをそのままmethodに渡す
先ほどの block 引数では複数の手続きを渡すことはできませんでした。しかし、あらかじめ生成されたProcであれば、普通の引数として複数の手続きを渡すことができます。
def process(on_success, on_error)
result = do_something_process
if result.success?
on_success.call
else
on_error.call
end
end
success_handler = Proc.new { |num| puts "Success" }
error_handler = Proc.new { |num| puts "Error" }
process(success_handler, error_handler)
このように、複数の手続きを渡したい場合や、あらかじめ変数に手続きを格納しておきたい場合にProcオブジェクトを事前に生成しておく方法が使えます。
使用ケース
- 高階関数を作成したい時
- 配列に関数を格納したい時
- 関数を複数渡したい場合
Proc オブジェクトの生成方法
ブロック引数など暗黙的にProcオブジェクトに変換されているようなケースもありますが、明示的な Proc オブジェクトの生成方法として、主に2種類あります。
1. proc メソッド(= Proc.new)
proc = proc { |i| i + 1 }
proc.call(1)
# => 2
proc = Proc.new { |i| i + 1 }
proc.call(1)
# => 2
2. lambda メソッド (= アロー演算子(->))
lambda_proc = lambda { |a, b| a + b }
lambda_proc.call(1, 2) # => 3
arrow_proc = ->(a, b) { a + b }
arrow_proc.call(1, 2) # => 3
proc と lambda の違い
proc と lambda のメソッドは両方ともProcオブジェクトを生成します。しかし、振る舞いに重要な違いが3つあるため、注意が必要です。
参考:
1. 引数の個数が厳密かどうかの違い
proc: 引数の個数が柔軟。足りない引数は nil になります。
b = Proc.new { |a, b, c| p a, b, c }
b.call("Hello", "World")
# => Hello
# => World
# => nil
lambda: 引数が厳密。引数を多く渡すとエラーになります。
b = lambda { |a, b, c| p a, b, c }
b.call("Hello", "World")
# => ArgumentError: wrong number of arguments (given 2, expected 3) (ArgumentError)
2. return を行った場合、呼び出し側で続きを実行してくれるかの違い
proc: return 後にProcを定義したscope自体を抜けてしまいます。
def method
proc = Proc.new { return p "proc" }
proc.call
p "method" # 実行されない
end
method
# => "proc"
lambda: return を行った場合でも、呼び出し側のmethodを最後まで実行します。
def method
lambda1 = lambda { return p "lambda" }
lambda1.call
p "method" # ここも実行される
end
method
# => "lambda"
# => "method"
3. Proc オブジェクトを生成したメソッドの「外」で呼び出した時の挙動の違い
proc: 手続きオブジェクトで return や break を行っている場合、そのオブジェクトを生成したmethodの外で呼び出すと例外 LocalJumpError が発生します。
def foo
proc { return }
end
foo.call # foo の外で実行する
# => in `call': return from proc-closure (LocalJumpError)
lambda: 生成した手続きオブジェクトはメソッドと同じように振る舞うことを意図されているため、例外は発生しません。
def foo
lambda { return }
end
foo.call
# => nil # 例外発生なし
proc (Proc.new) で生成される Proc オブジェクトは block をそのままオブジェクト化したものなので、block を渡した場合の挙動と proc と同じです。block や proc によって、methodの一部を書き換えているだけと捉えると理解しやすかったです。
lambda は厳密なラムダ式の関数として扱うことができ、JavaScript でいうところのアロー関数とイメージでは同じと理解しました。
lambda と proc の使い分け
公式ドキュメントによると、用途に応じた使い分けを推奨しています。
proc(= Proc.new):
- 引数を柔軟にしたい時
- return の後は呼び出し側での処理を抜けたい時
例: イテレータを実装する場合
lambda(アロー演算子):
- 引数を厳密にしたい時
- return の後は呼び出し側での処理を続行したい時
- 自己完結的な関数として便利
- Ruby メソッドとまったく同じように動作
例: 高階関数の引数として使いたい時
*高階関数...関数を引数として受け取ったり返値として返したりする関数
| block | Proc(proc) | Proc(lambda) | |
|---|---|---|---|
| オブジェクトか | No | Yes | Yes |
| 引数チェック | ルーズ | ルーズ | 厳密 |
| return の挙動 | メソッドを抜ける | メソッドを抜ける | 呼び出し元に戻る |
| 主な用途 | 基本の繰り返し | 特殊な制御 | 高階関数 / 部品化 |
block に変換する
block引数が使えるmethodなどに、block以外の手続きを渡したい場合があります。
Procオブジェクトに& をつける
ブロック引数に、& 演算子を使って Proc オブジェクトをblockに変換して渡すことができます。
method = lambda { |e| e + 100 }
[1, 2, 3].map(&method) # => [101, 102, 103]
method = lambda { p "hello" }
def method2(&block)
block.call
end
method2(&method) # blockに変換される
# "hello"
to_proc メソッドを持つオブジェクトに & をつける
例えば、シンボルはSymbol#to_procを持つので、& をつけて渡すと、暗黙的に to_proc を呼び出し Proc オブジェクトに変換した上で block として渡すことができます
['Taro', 'Jiro'].map(&:upcase) # Proc に変換した上で block として渡される
# => ["TARO", "JIRO"]
暗黙でProcを生成した場合、生成方法や渡し方によって、Proc オブジェクトが lambda 型かどうかが分かれます。
ここでは詳しく取り上げませんが、振る舞いを確認するには公式等を参照ください。
https://docs.ruby-lang.org/ja/latest/method/Proc/i/lambda=3f.html
https://docs.ruby-lang.org/en/4.0/Proc.html#method-i-lambda-3F
例:
method = lambda { p "hello" }
def method2(&block)
block.call
block.lambda?
end
method2(&method) # 実引数の場合
# "hello"
# => true 元のProcオブジェクトから引き継がれる
method2 { p "hello" } # 仮引数の場合
# "hello"
# => false false になる
最後に
Ruby において、手続きそのものを表したオブジェクトはProcオブジェクトと呼ばれますが、その生成方法によって挙動が異なることを学びました。
調べ出すとキリがなく長ったらしくなってしまいました...
理解するまでは少し混乱しましたが、それぞれのメリットデメリットを理解し必要に応じて使い分けて行きたいです!
Procオブジェクトを生成する場合、特別な事情がない限りは、引数チェックもあり安全な lambda を利用するのが良さそうに感じました。
実際使ってみる時には、スタイルガイドも参考にしたいですね。
ここには載せていないですが、最初に学んだ言語が JavaScript/TypeScript だったので、TypeScript だとこう言うイメージかなと書いてみるとわかりやすかったです。
自分の理解でもし間違っているところがあればぜひコメントいただけると嬉しいです!
最後まで読んでいただき、ありがとうございました!
参考記事: