【Ruby】Procオブジェクトについて整理する

2020年5月5日

どうも、てーやまです。

Procオブジェクト難しくないですか?

僕は今まで出てきたオブジェクトと比較すると使いみちが思いつきませんでした。自分と同じような方もいるんじゃないかと思うので、調べて分かったことを少しずつ、記していこうと思います。

Procオブジェクトとは

ブロックをオブジェクト化できる

ブロックは do ~ end  や  { } の中に書かれた一まとまりの処理ですが、どうやらこれをオブジェクト化するのがProcクラスのようです。

#Procクラスのインスタンスを作成
proc_sample = Proc.new { puts 'hoge' }

#callメソッドでブロック処理を呼び出し
proc_sample.call

#実行結果
hoge
=>nil
  • Procオブジェクトを生成するには
     →Procクラスのインスタンス作成時にブロックを渡す
  • ブロックを実行するには
     →Procクラスのcallメソッドを使う

スコープまで含んだオブジェクトである

少し論点がズレますが、他のオブジェクトとは違って面白かった点なので記載しておきます。

ブロックをコンテキスト(ローカル変数のスコープやスタックフレーム)とともにオブジェクト化した手続きオブジェクトです。

Ruby 2.7.0 リファレンスマニュアル

これは何を指しているかというと、次の例をみてください。

num = 100
proc_sample2 = Proc.new { puts num }

#実行
proc_sample2.call

#実行結果
100
=>nil

Procオブジェクト生成より前に定義したローカル変数のスコープまで含まれています!ローカル変数の値を代入し直すと実行結果も変わりますよ。

#numの値を変更
num = 200

proc_sample2.call

#実行結果
200
=>nil

スコープまで含んだオブジェクトなんて初めて聞きました!

(ただしローカル変数はProcオブジェクト生成前に定義されていないとダメです)

Procオブジェクトの使い方

メソッドに渡す方法

ブロックをProcオブジェクト化する最大の利点は引数としてメソッドに渡せるところだと思います。

まず、メソッド側で仮引数に’&’を付けることでブロックを受け取ることができます。そして、受け取ったブロック.callとすることで実行できます。

#ブロックが仮引数のメソッドを定義
def proc_def(&proc_obj) #'&'を付けることでブロックを受け取れる
  puts proc_obj.call(1) #integerを渡すと引数として受け取ったブロックの評価される
end

#メソッドの呼び出し
proc_def { |n| n + 1 } #引数としてブロックを渡す

#実行結果
2
=>nil

そして、ブロックをProcオブジェクトとした場合はこうなります。

def proc_def(&proc_obj)
  puts proc_obj.call(1)
end

#Procオブジェクトを生成
pr = Proc.new { |n| n + 1 }

#メソッドの呼び出し
proc_def(&pr) #引数に'&'を付ける

#実行結果
2
=>nil

Procオブジェクトの作り方

lambda(ラムダ)で作成する

Procオブジェクトの作成の仕方は上で紹介したProc.newによってインスタンスを作成する以外にlambda(ラムダ)を使うことが出来ます。

結局はlambdaもProcクラスのインスタンスを返却するのですが、挙動が違うのでそれは後述します。

#lambdaを使ってProcオブジェクトを生成する
proc_sample = lambda { puts 'hoge' }

#Procオブジェクトの実行
proc_sample.call

#実行結果
hoge
=>nil

挙動の違い(引数)

同じProcオブジェクトでも、Proc.new を使って生成したか、 lambda を使って生成したかによって挙動が違います。

まず、Procオブジェクト実行時に渡す引数が、lambdaで作成した方が厳密です。引数の数が合わないと例外を返します。次の構文を見てください。

#Proc.newで作成する
new_proc = Proc.new { |a, b, c| puts a, b, c }

#lambdaで作成する
lambda_proc = lambda { |a, b, c| puts a, b, c }

#それぞれ実行する
new_proc.call(1, 2)

#実行結果
1
2

=>nil

lambda_proc.call(1, 2)

#実行結果
ArgumentError: wrong number of arguments (given 2, expected 3)

挙動の違い(return)

ブロックの中に return を書くと、それぞれのProcオブジェクトを実行した時の挙動に差があります。

  • Proc.newで作成すると呼び出し時にエラーになる
  • lambdaで作成するとエラーにならない
  • return ではなく next を使用することでエラーにならない
new_proc = Proc.new { return :foo }
lambda_proc = lambda { return :bar }

new_proc.call

#実行結果
LocalJumpError: unexpected return

lambda_proc.call

#実行結果
=>bar

#nextに書き換えてみる
new_proc = Proc.new { next :foo }

new_proc.call

#実行結果
=>foo

仮引数を省略できるyield

yield(イールド)…こいつが1番の難敵でした。まじで存在意義がわからなかった。

けれども、何となく、分かってきました。まずは次の例を見てください。

#yieldを使わない書き方
def proc_def(&block)
  block.call(1, 2)
end

proc_def { |a, b| a + b }

#実行結果
=>3

#yieldを使った書き方
def proc_def
  yield(1, 2)
end

proc_def { |a, b| a + b }

#実行結果
=>3

実行結果はいずれも同じですが、yieldを使用すると、仮引数を省略できることが分かります。

そしてyieldとはメソッドで受け取ったブロックを任意のタイミングで実行できる「Procクラスのcallメソッド」と同等というものだということです。

最初は仮引数を省略できることに反対でした。何故なら、明示的に書かないとバグの発生を助長すると思ったからです。

しかし、yieldを使っているということは受け取っているのはブロックなのです。うん、ならば良いかなという感じです。

こうして、綺麗に書けるのもRubyの醍醐味なのだと、そう感じました。

今後の更新予定

  • yieldについて 2020/5/6 追記
  • lambda等、Procオブジェクト生成について 2020/5/6 追記