まずはブロックを理解しよう
Procクラスを理解するには、Rubyのブロックを知っておく必要があるため、まずはブロックについてから解説していきます。
ブロックとは、引数と一緒に渡すことのできる処理のかたまりです。eachメソッドなどの繰り返しで使用されることが多いため、繰返し処理の中で行われる処理というイメージがあるかもしれませんが、それはブロックの1つの使い方であって、厳密にはdo~endや{}で囲また処理をブロックと呼びます。
ブロックの使い方
ブロックの使い方を見ていきましょう。
Rubyのブロックには、do~endで処理を書く方法と{}で囲まれた中に処理を書く方法の2つの書き方が存在します。
do~endで書くブロック
(1..10).each do |i|
puts i
end
{}(中括弧)で書くブロック
(1..10).each { |i| puts i }
どちらの書き方でもブロックであり、機能としては同じであるため、好みによってどちらを使っても構いません。ただし、一般的には処理が複数行に渡る場合はdo~endで書き、処理が1行で済む場合は{}で囲う書き方を用います。
ブロックはファイル入出力でも使用
Rubyのブロックは、ファイルの入出力処理でも使います。
File.openメソッドでファイルを開き、ブロックで開いたファイルに対する入出力の処理を定義します。そしてブロックを抜けるとファイルが自動でクローズされるため、閉じ忘れもなく処理を行えます。
File.open("qiita_todo_with_block.txt", "w") do |f|
f << "1行目の書き込み内容\n"
f << "2行目の書き込み内容\n"
end
ブロックを引数で渡す
ブロックをメソッドの引数として受け取り、渡されたブロックを実行するメソッドを定義できます。
このようなメソッドを作ることで、メソッドの一部の処理を引数で受けとったブロックの処理で置き換えることが可能になり、ある部分までは共通化された処理で、一部だけ機能によって処理が異なる部分のメソッドを共通化しやすくなります。
次のサンプルコードは、引数で渡されたブロックを10回実行して、その合計値を返すメソッドを定義する例です。
def total()
result = 0
for num in 1..10
if block_given?
#ブロックが引数で渡されたとき
result += yield(num)
else
#ブロックが渡されなかったときのデフォルトの処理
result += num
end
end
return result
end
puts "ブロックを引数で指定"
puts total() {|num| num ** 2}
puts "ブロックを省略"
puts total()
実行結果
-------------------
ブロックを引数で指定
385
ブロックを省略
55
このように、ブロックを受け取るメソッドではblock_given?でブロックが指定されているか判定し、ブロックの指定が省略された場合は、デフォルトの処理を通すようにするのが一般的です。
Procとは
それでは、本題のProcについて触れていきましょう。
Procとは「ブロックをオブジェクト化したProcクラスのインスタンス」です。
前章で解説したブロックを、ローカル変数のスコープやスタックフレームと共にオブジェクト化し、ブロックの代わりにメソッドの引数に渡すことができます。
ブロックはその特性上、メソッドの引数に1つしか指定することができませんが、Procにオブジェクト化したブロックであれば、通常の変数と同じようにメソッドで複数渡すことも可能です。
Procの作り方
Procオブジェクトは、次の3つの方法で作ることができ、作成したProcオブジェクトのcallメソッドより、指定したブロックの処理を実行することができます。
1) procキーワードでブロックをProcオブジェクトに変換
my_proc = proc { |n| n * 2 }
my_proc.call(2) #=> 4
2) Procクラスのコンストラクタにブロックを指定してProcオブジェクトを作成
my_proc = Proc.new { |n| n * 2 }
my_proc.call(2) #=> 4
3) メソッドの引数として渡す
def sample_proc(&my_proc)
puts my_proc.call(2) #=> 4
end
sample_proc { |n| n * 2 }
「&引数名」のようにメソッドの引数を宣言すると、呼び出し元で指定されたブロックがProcオブジェクトに変換され、メソッドの中で引数として渡されたブロックをcallメソッドで呼び出すことができます。
複数のProcをメソッドの引数として渡す
Procはブロックをオブジェクト化したものであるため、他の数値や文字などの変数と同じ扱いになり、メソッドの引数としても複数渡せます。
次のサンプルコードは、引数を2倍と3倍にするProcオブジェクトを2つ用意して、メソッドの引数として渡し、呼び出し先のメソッド内で2つのProcオブジェクトの処理をそれぞれ実行する例です。
#複数のProcを受け取るメソッド
def sample_proc2(p1, p2)
puts p1.call(2)
puts p2.call(2)
end
#引数を2倍と3倍にして返すProcをそれぞれ定義
my_proc1 = proc { |n| n * 2 }
my_proc2 = proc { |n| n * 3 }
#メソッドの引数に複数のProcを渡す
sample_proc2(my_proc1, my_proc2)
実行結果
-------------------
4
6
Procとlambda
lambdaは、Procオブジェクトを作るもう一つの方法ですが、procキーワードや、Proc.newで作成したProcオブジェクトとで、returnの挙動が異なります。
Proc.newでreturnした時
Proc.newやprocキーワードで作成したProcオブジェクトは、そのブロック内でreturnをすると、呼び出し元のメソッドから抜けてしまいます。
def sample1
proc1 = Proc.new { return p "proc" }
proc1.call
p "proc1#call実行後の処理"
end
sample1
実行結果
-------------------
proc
lambdaでreturnした時
一方、lambdaで作成したProcオブジェクトは、そのブロック内でreturnをしても、呼び出ししたメソッドの処理に戻り、後続の処理が実行されます。
def sample1
lambda1 = lambda { return p "lambda" }
lambda1.call
p "lambda1#call実行後の処理"
end
sample1
実行結果
-------------------
lambda
lambda1#call実行後の処理
まとめ
RubyのブロックとProcクラスについて解説してきました。ブロックの概念は最初は難しいかもしれませんが、Rubyを学ぶ上では避けてわ通りない道ですので是非理解しておきましょう。
【関連記事】
▶Rubyのlambdaとは?ブロックの使い方から詳しく解説
Proc.newやprocキーワードで作成したProcオブジェクトでreturnをする時、callメソッドを呼んだメソッドを抜けるわけではなく、そのProcを宣言したメソッドから処理が抜けることに注意が必要です。