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オブジェクトは
状況に応じて七変化するキューティーハニーだったんだ!