やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

組込ライブラリ(Proc)

 ブロックとそのコンテキストをオブジェクト化したもの。

成果物

情報源

Proc

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

 lambdaとの違いは引数の数が一致しなくてもエラーにならないことだっけか?

Proc は ローカル変数のスコープを導入しないことを除いて名前のない関数のように使えます。ダイナミックローカル変数は Proc ローカルの変数として使えます。

 ダイナミックローカル変数ってなんぞ? 調べてもわからなかった。ただ、動的に生成したローカル変数のことを言っているのかもしれない。たしかObjectクラスだったか忘れたけど、ローカル変数を生成するメソッドがあった。そうして生成したローカル変数のことを言っているのかもしれない。ちゃんと言葉の定義については説明してくれよ。

Proc がローカル変数のスコープを保持していることは以下の例で変数 var を参照できていることからわかります。

var = 1
$foo = Proc.new { var }
var = 2

def foo
  $foo.call
end

p foo       # => 2

手続きを中断して値を返す

手続きオブジェクトを中断して、呼出し元(呼び出しブロックでは yield、それ以外では Proc#call) へジャンプし値を返すには next を使います。break や return ではありません。

def foo
  f = Proc.new{
    next 1
    2
  }
end

p foo().call       #=> 1

 マジか。breakはたしかループ専用だったっけ。でもreturnまでダメなのかよ。やったらどうなるの? 試したらエラーになった。えー。これはキモチワルイ。

def foo
  f = Proc.new{
    return 1 # unexpected return (LocalJumpError)
    2
  }
end
p foo().call       #=> 1

Proc オブジェクトをブロック付きメソッド呼び出しに使う

ブロック付きメソッドに対して Proc オブジェクトを `&' を指定して渡すと呼び出しブロックのように動作します。しかし、厳密には以下の違いがあります。これらは、Proc オブジェクトが呼び出しブロックとして振舞う際の制限です。

問題なし

(1..5).each { break }

LocalJumpError

pr = Proc.new { break }
(1..5).each(&pr)

 なんかRubyってブロック周りの罠が多いよな。

lambda と proc と Proc.new とイテレータの違い

Kernel.#lambdaProc.new はどちらも Proc クラスのインスタンス(手続きオブジェクト)を生成しますが、生成された手続きオブジェクトはいくつかの場面で挙動が異なります。 lambda の生成する手続きオブジェクトのほうがよりメソッドに近い働きをするように設計されています。

 え、lambdaprocって同じProcクラスなの?!

 しかも同じクラスなのに挙動が違うのかよ。なにそれ頭おかしいんじゃないの?

 オブジェクト化した後でlambdaprocの違いを判定することはできるのか? 

 具体的に両者はどう違うの?

Kernel.#proc は Proc.new と同じになります。引数に & を付けることで手続きオブジェクト化したブロックは、Proc.new で生成されたそれと同じにように振る舞います。

 procProc.newの糖衣構文。

引数の扱い

lambda のほうがより厳密です。引数の数が違っていると(メソッドのように)エラーになります。 Proc.new は引数を多重代入に近い扱い方をします。

b1 = Proc.new{|a,b,c|
  p a,b,c
}
b1.call(2, 4) #=> 2 4 nil

b2 = lambda{|a,b,c|
  p a,b,c
}
b2.call(2, 4) #=> wrong number of arguments (given 2, expected 3)

メソッド呼び出し(super・ブロック付き・yield)/ブロックパラメータの挙動 も参照してください。

ジャンプ構文の挙動の違い

return と break は、lambda と Proc.new では挙動が異なります。例えば return を行った場合、lambda では手続きオブジェクト自身を抜けますが、 Proc.new では手続きオブジェクトを囲むメソッドを抜けます。

def foo
 f = Proc.new { return :foo }
 f.call 
 return 
end

def bar
 f = lambda { return :bar }
 f.call 
 return 
end

def h
 yield
end

def hoge
 h{ return :hoge }
 nil
end

p foo()  #=> :foo
p bar()  #=> nil
p hoge() #=> :hoge

以下の表は、手続きオブジェクトの実行を上の例と同じように、手続きオブジェクトが定義されたのと同じメソッド内で行った場合の結果です。

               return                          next                        break
Proc.new   メソッドを抜ける            手続きオブジェクトを抜ける   例外が発生する
proc       メソッドを抜ける            手続きオブジェクトを抜ける   例外が発生する
lambda     手続きオブジェクトを抜ける  手続きオブジェクトを抜ける   手続きオブジェクトを抜ける
イテレータ メソッドを抜ける            手続きオブジェクトを抜ける   メソッドを抜ける

 この違いを把握した上で、どう使い分ければいいの? どんなときにどちらを使うべきなのか。その例がほしい。なぜわざわざ2種類用意したの? 使用を想定している状況が思いつかない。

 lambdaのほうが影響範囲が小さいのはわかった。自身の中でreturnしても自身を定義した親メソッドを抜けることはできない。それがlambdaの特徴ってことね。

 イテレータって3.times {|i| p i}のときの{}のことだよね? これが一番イメージに近い挙動だな。

orphan な手続きオブジェクトの挙動

Proc を生成したメソッドから脱出した後、手続きオブジェクトからの return, break は例外 LocalJumpError を発生させます。ただし、上でも説明した通り lambda で生成した手続きオブジェクトはメソッドと同じように振る舞うことを意図されているため、例外 LocalJumpError は発生しません。

def foo
  Proc.new { return }
end

foo.call
# => in `call': return from proc-closure (LocalJumpError)

以下の表は、手続きオブジェクトの実行を上の例と同じように、手続きオブジェクトが定義されたメソッドを脱出してから行った場合の結果です。

               return                          next                        break
Proc.new   例外が発生する              手続きオブジェクトを抜ける   例外が発生する
proc       例外が発生する              手続きオブジェクトを抜ける   例外が発生する
lambda     手続きオブジェクトを抜ける  手続きオブジェクトを抜ける   手続きオブジェクトを抜ける

 試しにlambdaで実行したらnilが返ってきた。fooメソッドの戻り値がnilRubyではメソッド内において最後に実行した式の戻り値がそのまま自動的に返されるはず。つまりlambdanilを返したということか?

def foo
  lambda { return }
end
foo.call # nil

 試しにreturn 1のように引数を渡してやると、その値がそのまま返った。

def foo
  lambda { return 1 }
end
p foo.call # 1

 つまりlambda式? の戻り値はlambda式内でreturnしたときの値である。そしてlambda式がfooメソッドの最後に実行した式であるなら、fooメソッドの戻り値はlambda式の戻り値になる。つまりfooメソッドの戻り値はlambda式でreturnした1になる。

メンバ抜粋

特異メソッド

new

インスタンスメソッド

<< === >> [] arity binding call curry hash inspect lambda? 
parameters ruby2_keywords source_location to_proc to_s yield

->

 ドキュメントには書いてないのだが->lambdaを定義できる。

 いやこれは重大な糖衣構文でしょ。ちゃんと書いてくれよ。

p ->{}.lambda?       # => true

 lambdaは変数に代入できる。

l = ->{}
p l.lambda?       # => true

 3を返すだけのラムダ式。実行するときは末尾に[]を付与する。これはメソッド呼出の()と同等の位置づけらしい。

l = ->{3}
p l   # #<Proc:0x01647468 ファイルパス:行数 (lambda)>
p l[] # 3

 引数xを受け付ける。

l = ->x{x+1}
p l[3] # 4

 なお、実行時に引数の数が定義と一致しなければエラーになる。

l = ->x{3}
p l[]    # wrong number of arguments (given 0, expected 1) (ArgumentError)
p l[3,3] # wrong number of arguments (given 2, expected 1) (ArgumentError)

 公式ドキュメントより誰かのブログのほうが勉強になるんだが。その情報どこから得たの?

lambda?

 lambdaであるなら真を返す。

 lambdaもProcクラスであると説明があった。ならlambda,proc,イテレータはどうやって区別するのか疑問だった。どうやらlambdaであるかどうかだけは識別できるらしい。

p lambda{}.lambda?   # => true
p ->{}.lambda?       # => true
p proc{}.lambda?     # => false
p Proc.new{}.lambda? # => false
def n(&b) b.lambda? end
p n {}               # => false

所感

 Rubyのブロック周りはムダに難しい。もっと簡単にできなかったの? ツギハギ感ある。

対象環境

$ uname -a
Linux raspberrypi 5.10.52-v7l+ #1441 SMP Tue Aug 3 18:11:56 BST 2021 armv7l GNU/Linux