rubyのProcって何をしてるの

2022.06.15

Procとは

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

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

ブロックをオブジェクト化する。ちなみにブロックとはdo~endとか{}の中に書かれた処理のこと。

スコープとは変数の有効範囲。

ブロックをコンテキストと共にオブジェクト化したものを手続オブジェクト。つまりprocオブジェクトは手続きオブジェクト。

なんで作られたと思う?(存在意義)

ある処理のかたまりであるブロックをオブジェクトにしたいっていうの時に出てきたのがProcだと思う。ブロックをオブジェクトにして引数とかで扱えるようにしたかったとか。

rubyはオブジェクト指向型。

procがあることで何がどう省略されたか?

proc有りバージョン

def lunch(food)
 puts "食材を準備する"
 food.call
 puts "ピクニックに行く"
end

tacos = Proc.new {
 puts "2つ折りに巻く"
 puts "こぼれる"
}

burritos = Proc.new {
 puts "くるくるしっかり巻く"
 puts "こぼれにくい"
}

lunch(tacos)
lunch(burritos)

無しバージョン

def lunch_tacos
 puts "食材を準備する"
 puts "2つ折りに巻く"
 puts "こぼれる"
 puts "ピクニックに行く"
end

def lunch_burritos
 puts "食材を準備する"
 puts "くるくるしっかり巻く"
 puts "こぼれにくい"
 puts "ピクニックに行く"
end

lunch_tacos
lunch_butrritos

タコスとブリトーで共通の処理をまとめられる。


変数を扱う場合

def lunch(food)
 puts "食材を準備する"
 food.call(3)
 puts "ピクニックに行く"
end

tacos = Proc.new {|amount|
 puts "2つ折りに巻く"
 puts "#{amount}枚作る"
 puts "こぼれる"
}

burritos = Proc.new {|amount|
 puts "くるくるしっかり巻く"
 puts "#{amount+1}枚作る"
 puts "こぼれにくい"
}

ちなみにprocオブジェクトの作成方法は以下の3通り。&引数で呼び出すとブロックがprocオブジェクトに変換される。

tacos = Proc.new {puts "2つ折りに巻く"}
tacos = proc {puts "2つ折りに巻く"}

def lunch(&food)
 省略
end
lunch {|amount|
 puts "くるくるしっかり巻く"
 puts "#{amount+1}枚作る"
}

yieldと何が違う?

  • procだとブロック処理を変数に格納できる。
  • yieldはブロック処理を呼び出し時に毎回書く必要がある。
#procの場合
def lunch(food)
 puts "食材を準備する"
 food.call
 puts "ピクニックに行く"
end

tacos = Proc.new {
 puts "2つ折りに巻く"
 puts "こぼれる"
}

def b
 lunch(tacos)
end

def c
 lunch(tacos)
end
#yieldの場合
def lunch
 puts "食材を準備する"
 yield
 puts "ピクニックに行く"
end

def b
 lunch do |_|
  puts "2つ折りに巻く"
  puts "こぼれる"
 end
end

def c
 lunch do |_|
  puts "2つ折りに巻く"
  puts "こぼれる"
 end
end

まとめ

yield・・呼び出し時にブロック処理を書く必要がある。一度しか呼び出すことがない場合に使える。ブロック処理をわざわざ変数にする必要もないところ。

proc・・ブロック処理を変数にまとめることができる。ブロックを繰り返し呼び出したい時に使える。

おまけ

スコープ(変数の有効範囲)が変わったりする。

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

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

procを使うとスコープがなくなるのでaという値が更新され続ける。

a = 1
p1 = proc{p a += 1}

p1.call
p1.call
p1.call
p1.call

xxx #=> 2
xxx #=> 3
xxx #=> 4
xxx #=> 5

呼び出し時に引数を設定することでダイナミックローカル変数をprocローカル変数として扱えるようになる。aにスコープがつく。

a = 1
p1 = proc{|a| p a += 1}

p1.call(a)
p1.call(a)
p1.call(a)
p1.call(a)

xxx #=> 2
xxx #=> 2
xxx #=> 2
xxx #=> 2

普通にメソッドだとaはcメソッド内のスコープになる。

a = 1
def c(a)
 puts a += 1
end

c(a)
c(a)
c(a)
c(a)

xxx #=> 2
xxx #=> 2
xxx #=> 2
xxx #=> 2