こんにちは!Webコーダー・プログラマーの貝原(@touhicomu)です。
今日は、RubyによるProc、lambdaの使い方と使い分けについて解説したいと思います。
この記事ではProc、lambdaの、
- Procとlambdaの使い方
- Procとlambdaへの引数の渡し方
- Procをメソッドの引数に渡す方法
という基本的な内容から、
- アロー演算子
- クロージャー
などの応用的な使い方に関しても学習していきます。
このページで、RubyのProc、lambdaの使い方をよく把握して自分のスキルとしていきましょう!
Procとlambdaの使い方と使い分け
Procとlambdaの使い方
Procは、defをインスタンスにしたもので、Proc.newメソッドによってdefのインスタンス変数を生成できます。
p1 = Proc.new do |val| p "arg=" + val.to_s end p1.call(1) #=>"arg=1" p2 = p1 p2.call(100) #=>"arg=100"
以上のように、インスタンス変数p1にProcオブジェクトを代入し、callメソッドにより、Proc.new時に定義したProcオブジェクトを呼び出すことができていますね。
Procオブジェクトはdefとほぼ同じです。
また、p1をp2に代入してp2から全く同じProcオブジェクトを呼び出すことができています。
対してlambdaはどうでしょう。
実は、lambdaはProc.newとほぼ同じ動作をします。
違いは、次節で解説します。
l1 = lambda do |val| p "arg=" + val.to_s end l1.call(1) #=>"arg=1" l2 = l1 l2.call(100) #=>"arg=100"
Procとlambdaの違い
lambdaは引数の数をチェックしている
lambdaは引数の数を文法チェックしています。
そのため、呼び出す時に引数の数が違うと、エラーとなります。
Procはdefと同じで、引数に値の指定がなかった場合はnilが引数に代入されます。
p1 = Proc.new do |val1, val2| p "arg=" + val1.to_s + " , " + val2.to_s end l1 = lambda do |val1, val2| p "arg=" + val1.to_s + " , " + val2.to_s end p1.call(1) #=> l1.call(1) #=> wrong number of arguments (given 1, expected 2) (ArgumentError)
lambdaは、引数の数が違うと、(ArgumentError)を表示していますね。
returnするときの挙動が違う
Procとlambdaはreturn文の挙動が違います。
Procのreturnは、callの呼び出し元でreturnを実行したことと見なされます。
lambdaのreturnは、callの呼び出し先、つまりlambda自身のreturnであるものと見なされます。
def proc_def p1 = Proc.new { return p "proc's return"} p1.call p "proc_def's end" end def lambda_def l1 = lambda{ return p "lambda's return"} l1.call p "lambda_def's end" end proc_def #実行結果 #=>"proc's return" lambda_def #実行結果 #=>"lambda's return" #=>"lambda_def's end"
proc_defでは、Procでreturnした時点で、proc_defが終了していますね。
対して、lambda_defでは、lambdaがreturnしても、lambda_defは処理を継続しています。
Procとlambdaへの引数の渡し方
渡すためのメソッド
Procとlambdaへの引数の渡し方にはいくつか種類のメソッドがあります。
それぞれProcとlambdaのインスタンスメソッドとなっています。
まずは、Procの例です。
p1 = Proc.new do |val| p "arg=" + val.to_s end p1.call(1) #=>"arg=1" p1["value"] #=>"arg=value" p1.([1,2,3]) #=>"arg=[1, 2, 3]"
次にlambdaの例です。
l1 = lambda do |val| p "arg=" + val.to_s end l1.call(1) #=>"arg=1" l1["value"] #=>"arg=value" l1.([1,2,3]) #=>"arg=[1, 2, 3]"
それぞれ、call()、[]、.()のインスタンスメソッドが定義されています。
基本的な渡し方
Procへの基本的な引数の渡し方として、
- 配列引数の渡し方
- デフォルト引数の渡し方
- キーワード引数の渡し方
について学習していきましょう。
配列引数の渡し方
まずは、配列引数の渡し方
procのcall()メソッドに、複数の引数を与えた場合、受け取るprocの側では、それらの引数を配列として受け取ります。
注意点として、引数を受け取るproc側の引数名には「*(アスタリスク)」を明示的につけておかないといけない点です。
「*」をつけていない引数名、たとえば「arg」の場合、call()メソッドの第1引数のみ「arg」に入ります。
以下のサンプルコードでは、配列「args」に「each」メソッドを使用してループし、順に引数の順番と値をコンソールに出力しています。
# 引数を配列として受け取る dump = Proc.new do |*args| idx = 1 # 配列をループ args.each do |item| # 受け取った引数を1個1個出力 p "No.#{idx} arg = #{item}" idx+=1 end end dump.call(10,9,8,"c","b","a")
実行結果:
"No.1 arg = 10" "No.2 arg = 9" "No.3 arg = 8" "No.4 arg = c" "No.5 arg = b" "No.6 arg = a"
実行結果が、「dump.call()」に渡した引数の順番と値に一致していますね。
Procの引数定義部(「|」から「|」内)に「*args」としていても、Proc内部では「args」だけで配列として使用できます。
デフォルト引数の渡し方
デフォルト引数とは、Procの呼び出し元の引数が省略されてProcが呼び出された際に使用される(デフォルトで決めてある)値がある引数です。
呼び出し元で明示的に値が指定されている場合は、デフォルト値は使用されません。
デフォルト値は、以下の疑似コードのように指定します。
Proc.new do | 引数名1 = デフォルト値1, 引数名2 = デフォルト値2, ・・・| コード部1 コード部2 コード部3 ・・・ end
実際のサンプルコードをみていきましょう。
# 面積と体積を計算するProc # y、 zについては、デフォルト引数を受け取るので省略可能。 keisan = Proc.new do |x, y = 5, z = 1| # 受け取った引数を表示 p "x = #{x}, y = #{y}, z = #{z}" # 長方形の面積を表示 p "menseki = " + (x * y).to_s unless x == nil # 立方体の体積を表示 p "taiseki = " + (x * y * z).to_s unless x == nil end p "keisan No.1 ---------------" keisan.call(1) p "keisan No.2 ---------------" keisan.call(2, 10) p "keisan No.3 ---------------" keisan.call(2, 20, 10)
実行結果:
"keisan No.1 ---------------" "x = 1, y = 5, z = 1" "menseki = 5" "taiseki = 5" "keisan No.2 ---------------" "x = 2, y = 10, z = 1" "menseki = 20" "taiseki = 20" "keisan No.3 ---------------" "x = 2, y = 20, z = 10" "menseki = 40" "taiseki = 400"
引数yや、z、を省略した場合、でデフォルト値が使用されています。
注意点として、デフォルト引数は引数記述部の右橋から順に指定する必要があります。
キーワード引数の渡し方
引数をキーワード引数とすることもできます。
キーワード引数には以下の特徴があります。
- 指定する順番に依存しない
- デフォルト値を指定できる
- ハッシュを指定できる
- ハッシュを渡すのと事実上同じ
以下、実際のサンプルコードをみていきましょう。
# 引数をキーワード引数として受け取る hello = Proc.new do |who: "you", whom: "jobs", what: "hello"| p who.to_s + " said " + whom.to_s + " that " + what.to_s end hello.call() hello.(whom:"Bill") hello.(what:"Billed to you", to:"Tim") #=> unknown keyword: to (ArgumentError)
実行結果:
"you said jobs that hello" "you said Bill that hello" def_keyword_args.rb:2:in `block in <main>': unknown keyword: to (ArgumentError)
実行結果の1行目と2行目は、キーワード引数の省略がうまくできています。
2行目はキーワード引数が順番に依存しないことを示す例です。
3行目は存在しないキーワード名を与えるとunknown keywordエラーとなってしまう例です。
なおRubyのProcの引数に関して詳しくは、下記の記事を参照されてください。
応用的な渡し方
引数の数を示すarity
Procのarityメソッドは引数の数を返します。
実際のサンプルコードをみていきましょう。
なお、ここでは、Proc.newメソッドではなく、procキーワードでProc.newと等価なことを行っています。
また、procキーワードの書き方はlambdaキーワードの書き方と同じであることを把握しておいてください。
proc1 = proc do |x| x end proc2 = proc do |x,y| [x,y] end proc3 = proc do |*ary| ary end proc4 = proc do |x, *ary| [x,ary] end proc5 = proc do |key1:0,key2:0| key1 == key2 end p proc1.arity p proc2.arity p proc3.arity p proc4.arity p proc5.arity
実行結果:
1 2 -1 -2 0
実行結果のように1行目と2行目は、引数の数を返しています。
3行目は、引数を「配列引数」と定義したため、「-1」となっています。
4行目は、「第1引数は通常の引数、それ以降は配列引数」と定義したため、「-2」となっています。
5行目は、引数を「キーワード引数」と定義したため、「0」となっています。
このように、メソッドが可変長引数を受け付ける場合、負の整数「-(必要とされる引数の数 + 1)」を返します。
引数情報を示すparameters
Procのparametersメソッドは、引数情報を返します。
この引数情報は以下の特徴があります。
- 1引数あたり1個の配列で表されている
- 全体の引数は配列で、複数の子配列をもつ親配列
- 引数のタイプを示す情報は規定のシンボルになっている
- 引数名はシンボルで表される
proc1 = proc do |x| x end proc2 = proc do |x,y| [x,y] end proc3 = proc do |*ary| ary end proc4 = proc do |x, *ary| [x,ary] end proc5 = proc do |key1:0,key2:0| key1 == key2 end p proc1.parameters p proc2.parameters p proc3.parameters p proc4.parameters p proc5.parameters
実行結果:
[[:opt, :x]] [[:opt, :x], [:opt, :y]] [[:rest, :ary]] [[:opt, :x], [:rest, :ary]] [[:key, :key1], [:key, :key2]]
引数タイプを表すシンボルについては、以下のURLを参考にしてください。
instance method Method#parameters (Ruby 2.4.0)
Procをメソッドの引数に渡す方法
基本的な渡し方
Procは、「&」をつければ、ブロックとしてeachメソッドなどに渡せます。
以下では、配列のeachメソッドに対し、proc1に「&」をつけて渡しています。
# proc proc1 = Proc.new do |x| p x * 2 end # iを1から5まで数え上げる [1,2,3,4,5].each(&proc1)
実行結果:
2 4 6 8 10
eachでは1から5まで数え上げ、それぞれのループ中でproc1に引数(1から5までのループ変数値)と処理の制御を渡しています。
proc1では引数の2倍の数値をコンソールに出力しています。
応用的な渡し方
通常のメソッドにもProcに「&」をつけてブロックとして渡せます。
メソッド内では「yield n1, n2, … 」メソッドで引数n1、n2、・・・と引数を与えながら、制御をブロックに移せます。
以下の例ではProcを引数を1つ受け取るブロックとして渡す方法をサンプルコードで示します。
# 引数を1つもつProcの例 proc1 = proc do |i| p i.to_s end # ブロックを受け取る関数 def test yield "hello" end # test関数にProcに&をつけてブロックとして渡す test &proc1
実行結果:
"hello"
メソッドtestの「yield “hello”」にてブロック「&proc1」に対し引数「”hello”」を渡し制御をブロックに以降しています。
そして、「proc1」がコンソールに引数「”hello”」をpメソッドで出しています。
次にProcを引数を2つ受け取るブロックとして渡す方法をみていきましょう。
# 引数を2つもつProcの例 proc1 = proc do |i,j| p (i+j).to_s end # ブロックを受け取る関数 def test yield 5, 10 end # test関数にProcに&をつけてブロックとして渡す test &proc1
実行結果:
"15"
ブロックの引数が2個の場合でも、「yield」の引数を2個与えれば、OKであることがわかります。
Procとlambdaの応用
アロー演算子
Rubyにはlambdaと同等の機能を持つアロー演算子があります。
lambdaと同じく
- defの引数
- defのコード
を定義します。
a1 = -> (val) { p "arg=" + val.to_s } a1.call(1) #=>"arg=1" a1["value"] #=>"arg=value" a1.([1,2,3]) #=>"arg=[1, 2, 3]" a2 = a1 a2.call(100) #=>"arg=100" a3= a1 a3.call(200,300) #=>wrong number of arguments (given 2, expected 1) (ArgumentError)
->がアロー演算子で、「lambda」キーワードと同等の機能を提供します。
(val)がアロー演算子での引数です。lambdaでは|val|でしたね。
{・・・}がコード部です。
lambdaと同じく、アロー演算子でも引数の数をチェックしています。
クロージャー
クロージャーとは、Procやlambdaの定義されているスコープ内で定義されている変数を、Procやlambda内でも使用できる機能のことです。
例えば、Procメソッドが定義されているdefメソッドのスコープ内の変数は、全てProcメソッドからアクセスできます。
def hello(str) closed_str = str Proc.new do "hello, " + closed_str.to_s end end h1 = hello("Jon") p h1.call #=> "hello, Jon" c2 = hello("Kity") p c2.call #=> "hello, Kity"
lambdaでも、Procと同等のクロージャーが組めます。
def hello(str) closed_str = str lambda do "hello, " + closed_str.to_s end end h1 = hello("Jon") p h1.call #=> "hello, Jon" c2 = hello("Kity") p c2.call #=> "hello, Kity"
まとめ
今回は、RubyのProc、lambdaを学習しました!
学習のポイントを振り返ってみましょう!
- Procとlambdaはほぼ同じ機能を持ち、関数を持ち歩ける
- Procの引数の仕様はdefの引数と同じ
- lambdaの引数の仕様はdefの引数とは違い厳しくチェックされる
- arityで引数の数を、parametersで引数の情報を得られる
- Procに「&」を付けるとメソッドにブロックとして渡せる
- lamdaの簡略表現として「アロー演算子」がある
- Proc、lambdaと同じスコープにある変数はProcからアクセスできる(クロージャー)
以上の内容を再確認し、ぜひ自分のプログラムに生かし学習を進めてください!