RubyのProcオブジェクトはキューティーハニーだ!
RubyのブロックとそのオブジェクトであるProcオブジェクトはとても魅惑的だ。優しそうでいてなかなか複雑だ。外からは浅そうに見えて、中に入ると底が見えてこない。
単純に見えて使い方は実に多様だ。あるときはイテレータであり、またあるときはコールバック関数である。あるときはフィルターであり、またあるときはジェネレーターである。
Procオブジェクトに関し試してみたことを書いてみます。きっと勘違いがあるので指摘してくれるとうれしいです。
あるときはSingletonメソッド・ジェネレータになる
Rubyのブロックはメソッドと同じように手続きの塊を作り、それはlambdaでオブジェクト(Procオブジェクト)化できる。このときProcオブジェクトは外側の変数の参照を自身の状態として取りこめる。ブロック内の手続きは、Proc#callメソッドを呼ぶことによって実行される。こんな感じだ。
name = "taro"
party = "jimin"
pm = lambda do
puts name
puts party
puts "Hello, I'm #{name} of #{party}"
end
pm.call
# >> taro
# >> jimin
# >> Hello, I'm taro of jimin
name = "yukio"
party = "minshu"
pm.call
# >> yukio
# >> minshu
# >> Hello, I'm yukio of minshu
外側の変数(name,party)の参照先が変わると、それに合わせてProcオブジェクト(pm)の状態も変わる。
pmはオブジェクトだからユーザがメソッドを追加してもいい。こんなときsingletonクラス(特異クラス)が使える。
name = "taro"
party = "jimin"
pm = lambda do
class << pm
attr_reader :name, :party
def init(name, party)
@name, @party = name, party
end
def greeting
"Hello, I'm #@name of #@party"
end
end
pm.init(name, party)
end
pm.call
pm.name # => "taro"
pm.party # => "jimin"
pm.greeting # => "Hello, I'm taro of jimin"
name = "yukio"
party = "minshu"
pm.call
pm.name # => "yukio"
pm.party # => "minshu"
pm.greeting # => "Hello, I'm yukio of minshu"
例では先の例のブロック内の各文をメソッドで呼べるようにしている。singletonクラスの参照オブジェクトをpmとしメソッドを定義して、Proc#callでinitメソッドが実行されるようにする。
ただ上のコードはブロックの内部で変数pmを参照しているので、pmの参照先が変わると問題が起きる。ブロックの引数として対象のオブジェクトを渡して問題を解決しよう。
name = "taro"
party = "jimin"
pm = lambda do |obj|
class << obj
attr_reader :name, :party
def init(name, party)
@name, @party = name, party
end
def greeting
"Hello, I'm #@name of #@party"
end
end
obj.init(name, party)
end
pm[pm]
pm.name # => "taro"
pm.party # => "jimin"
pm.greeting # => "Hello, I'm taro of jimin"
name = "yukio"
party = "minshu"
pm[pm]
pm.name # => "yukio"
pm.party # => "minshu"
pm.greeting # => "Hello, I'm yukio of minshu"
ブロックを実行するpm[pm](これはpm.call(pm)と等価)のところがちょっと変な感じがする。
別のオブジェクトをブロック引数として渡したらどうなるんだろう。
class Person
end
me = Person.new
name = 'Charlie'
party = 'N/A'
pm[me]
me.name # => "Charlie"
me.party # => "N/A"
me.greeting # => "Hello, I'm Charlie of N/A"
Personクラスのオブジェクトmeに先のメソッドが追加された。
そうか、pmオブジェクトは任意のオブジェクトにsingletonメソッドを追加するジェネレータとして機能するんだ。じゃあもっとそれっぽく作ってみよう。
singleton_generator = lambda do |obj, properties|
class << obj
def init(properties)
meta = class << self; self end
meta.class_eval do
properties.each do |p, v|
define_method(p) { instance_variable_set("@#{p}", v) }
end
end
end
end
obj.init(properties)
end
class Person
attr_reader :fname, :lname
def initialize(fname, lname)
@fname, @lname = fname, lname
end
end
usp = Person.new('Barack', 'Obama')
singleton_generator[usp, :mname => 'Hussein', :party => 'Democratic']
puts "44th President of the United States is #{usp.fname} #{usp.mname} #{usp.lname} of #{usp.party} party."
#>> 44th President of the United States is Barack Hussein Obama of Democratic party.
singletonメソッドを生成するsingleton_generatorオブジェクトにブロック引数として対象のオブジェクトと、任意のプロパティをハッシュで渡せるようにした。この例ではPersonクラスのオブジェクトuspに対してmnameとpartyメソッドを追加する例を示した。これでsingletonメソッド・ジェネレーターの完成だ!
もっとも同じことは別にメソッドでもできるので、意味はなさそうだけど…
def singleton_generator(obj, properties)
class << obj
def init(properties)
meta = class << self; self end
meta.class_eval do
properties.each do |p, v|
define_method(p) { instance_variable_set("@#{p}", v) }
end
end
end
end
obj.init(properties)
end
あるときは再帰オブジェクトになる
以下のブログでY-Combinatorを使って再帰的な関数を手続きオブジェクトにするやり方が書かれている。
「再帰的な関数」を手続きオブジェクトにする - バリケンのRuby日記 - Rubyist
解説はとても丁寧になされていてとてもためになる。でもY-Combinatorについてはどうにも僕の頭がついていってくれないので、別の方法がないか考えてみた。
fact = lambda do |n|
if n.zero?
1
else
n * fact[n-1]
end
end
fact[10] # => 3628800
ここでブロック内の変数を無くすために、手続きオブジェクトをブロック引数として渡すようにする。
fact = lambda do |f, n|
if n.zero?
1
else
n * f[f, n-1]
end
end
fact[fact, 10] # => 3628800
うまくいった。でもfactを呼ぶときfactを引数で渡すのは格好悪い。
Ruby1.9のProcオブジェクトにはcurryというメソッドがあって、引数の一部を先に渡してそのオブジェクトに部分適用してくれるものがある。関数に対するこのような作用を論理学者ハスケル・カリーに因んでカリー化というらしい。
これが使えるかもしれない。
fact = lambda do |f, n|
if n.zero?
1
else
n * f[f, n-1]
end
end.curry
fact_maker = fact[fact] # => #<Proc:0x5becf8 (lambda)>
fact_maker[10] # => 3628800
factオブジェクトをカリー化し、最初にブロック引数としてfactオブジェクトだけを渡してfact_makerオブジェクトを作る。こうすればfact_makerに対する引数は1つだけになって目的は達成できる。
でもまだブロック内のelse節でProcオブジェクトを渡してる。これも消したい。
fact = lambda do |f, n|
if n.zero?
1
else
n * f[n-1]
end
end.curry
fact_maker = fact[fact] # => #<Proc:0x5becf8 (lambda)>
fact_maker[10] #=> TypeError:Proc can't be coerced into Fixnum
もちろんエラーが出る。エラーがでないようにするためにはブロックに渡す引数は、factオブジェクトじゃなくて既にfactを渡して生成したProcオブジェクトつまりfact_makerじゃなくちゃいけない。
そこでバリケンさんにあったアイディアをもらって、fact_makerを次のようにしてみる。
fact = lambda do |f, n|
if n.zero?
1
else
n * f[n-1]
end
end.curry
fact_maker = lambda do |m|
fact[fact_maker, m]
end
fact_maker[10] # => 3628800
うまくいった。さらにfactにつけたcurryをfact_maker内に移動する。
fact = lambda do |f, n|
if n.zero?
1
else
n * f[n-1]
end
end
fact_maker = lambda do |m|
fact.curry[fact_maker, m]
end
fact_maker[10] # => 3628800
fact_makerをY-Combinatorのようにメソッドにしてみる。
fact = lambda { |f, n| n.zero? ? 1 : n * f[n-1] }
def my_combinator(func)
f = lambda { |m| func.curry[f, m] }
end
fact = my_combinator(fact)
fact[10] # => 3628800
フィボナッチでも試してみる。
fib = lambda do |f, n|
case n
when 0 then 0
when 1 then 1
else f[n-1] + f[n-2]
end
end
def my_combinator(func)
f = lambda { |m| func.curry[f, m] }
end
fib = my_combinator(fib)
fib[10] # => 55
ここまで来たらProcのメソッドにもしてみる。
class Proc
def recur
f = lambda { |m| curry[f, m] }
end
end
fact = fact.recur
fact[10] # => 3628800
fib = fib.recur
fib[10] # => 55
トンチンカンなことやってないか心配だ…
あるときはcaseの判定ラベルになる
Ruby1.9ではProc#callの別名としてProc#===が用意されている。それを用いた楽しいサンプルをDave Thomasさんのブログで見つけた。
PragDave: Fun with Procs in Ruby 1.9
is_weekday = lambda {|day_of_week, time| time.wday == day_of_week}.curry
sunday = is_weekday[0]
monday = is_weekday[1]
tuesday = is_weekday[2]
wednesday = is_weekday[3]
thursday = is_weekday[4]
friday = is_weekday[5]
saturday = is_weekday[6]
case Time.now
when sunday
puts "Day of rest"
when monday, tuesday, wednesday, thursday, friday
puts "Work"
when saturday
puts "chores"
end
sunday, monday..はis_weekdayに曜日数値だけを適用して生成されたProcオブジェクトだ。これをcaseの条件におくと、Proc#===つまりcallメソッドが呼ばれて、Time.nowを引数としてis_weekdayのブロックが評価される。そしてブロックの評価結果はcaseの条件になる。
case式において比較条件の詳細が隠ぺいされていて簡潔だ。自分でも何か書いてみよう。
Person = Struct.new(:name, :height, :weight)
p1 = Person.new('ichiro', 1.75, 60)
p2 = Person.new('jiro', 1.65, 90)
p3 = Person.new('saburo', 1.90, 78)
BMI = lambda do |min, max, person|
(min..max).cover?(person.weight / person.height**2)
end.curry
upper = BMI[23, 25]
middle = BMI[21, 23]
lower = BMI[18.5, 21]
messages = [p1,p2,p3].map do |testee|
result =
case testee
when upper
"You are in upper"
when middle
"You are great!"
when lower
"You are in lower"
else
"Problem!"
end
{testee.name => result}
end
puts messages
#>> {"ichiro"=>"You are in lower"}
#>> {"jiro"=>"Problem!"}
#>> {"saburo"=>"You are great!"}
upper,middle,lowerはBMIオブジェクトにそれぞれの許容範囲の最大値、最小値を部分適用したProcオブジェクトだ。それらはcase式において各testeeの身長と体重を参照して各許容範囲と比較し結果を返す。
もう少し面白い例が思いつけばよかったんだけど…
こんなふうにProcオブジェクトはその使い方によってさまざまな形に化ける。
そう、RubyのProcオブジェクトは状況に応じて七変化するキューティーハニーだったんだ!
blog comments powered by Disqus