読者です 読者をやめる 読者になる 読者になる

( ꒪⌓꒪) ゆるよろ日記

( ゚∀゚)o彡°オパーイ!オパーイ! ( ;゚皿゚)ノシΣ フィンギィィーーッ!!!

「関数型Ruby」という病(6) - 関数合成と文脈、Proc#liftとProc#>=、そしてモナ

前回から一年以上が経過しているけど、最近lambda_driver.gemに機能を追加したので、そのことについて書こうと思う。
Rubyで、モナ……っぽい関数合成を実装した話だ。

Rubyで関数合成とかしたいので lambda_driver.gem というのを作った - ( ꒪⌓꒪) ゆるよろ日記

関数合成


関数合成については以前に書いたので、こちらを見て欲しい。

「関数型Ruby」という病(2) - 関数合成 Proc#compose - ( ꒪⌓꒪) ゆるよろ日記


おさらいをしておくと、関数合成とは、 関数gと関数fから、g(f(x))という関数hを新たに作り出すことだ。

(g ∘ f)(x) = g(f(x))


関数gと関数fの合成関数g ∘ fに引数xを渡した結果は、関数gにf(x)の結果を渡したものと等しい。つまり、このような操作である。

f = lambda{|x| x + 1 }
g = lambda{|x| x * x }

# 合成関数g ∘ f
h = lambda{|x| g.(f.(x)) }


これを図にするとこんな感じ。 上記の例の合成関数h: g ∘ fに引数 3を与えた場合。

f:id:yuroyoro:20140216195815p:plain


lambda_driver.gemでは、この関数合成をProc#>>で行うことができるようになっている。

# Proc#>>で合成
h = f >> g

h.(3) # => 16


Proc#>>の実装は単純で、以下のようになる。

class Prco
  def >>(g)
   # 「自分の計算結果を引数の関数gへ渡す」Procオブジェクトを返す
   lambda{|x| g.call(self.call(x)) }
  end
end

関数合成と計算の失敗


このように、とても便利な関数合成だが、以下のような状況だと少し困ることがある。

f = lambda{|arr| arr.first } # f: 引数のArrayの最初の要素を取り出す
g = lambda{|x| x + 1 }       # g: 引数に1を加算
h = lambda{|x| x * 2 }       # h: 引数を2倍

# f, g, hを合成
i = f >> g >> i

i.([3,5]) # => 4


関数fは、引数に配列を取って最初の要素を返す。関数gはfの結果に1を加算する。関数hはさらにその結果を2倍する。単純だ。
この3つの関数を合成した物が関数i。図にするとこうなる

f:id:yuroyoro:20140216195814p:plain


では、関数iに空配列[]を渡すとどうなるか?

# []を渡すと
i.([])
# => NoMethodError: undefined method `+' for nil:NilClass


関数fはnilを返し、関数gはnilに1を加算しようと+を呼び出してエラーになっている。
図ではこうなる。

f:id:yuroyoro:20140216195813p:plain


ここで、関数がnilを返した場合はその計算は失敗したと仮定する。よって、関数gと関数hでは、引数がnilであるかチェックするように変更する。

# g, hに引数がnilかチェックを追加した
g = lambda{|x| return nil if x.nil?; x + 1 } 
h = lambda{|x| return nil if x.nil?; x * 2 }

i = f >> g >> h

i.([]) # => nil


例外も発生せず、めでたしめでたし……ではない。関数gとiにそれぞれnilをチェックする処理が重複して実装されているのでDRYではない。合成する関数がもっと多くなった場合は面倒だ。
できればこのnilをチェックする処理を共通化したい。

関数合成に細工する


関数を合成するときに、「nilかどうか判定する処理」を間に挟むようにすれば、個々の関数にわざわざnilチェックを実装せずともよい。
以下のように関数合成時に細工を行うProc#>=を実装する。

class Proc
  def >=(g)
    lambda{|x|
      res = self.call(x)
      # 計算結果がnilならば、後続の関数gを呼び出さずにnilを返す
      return nil if res.nil?
      g.call(res)
    }
  end
end


これで、Proc#>=を使って細工された関数合成を行うことで、計算が途中で失敗した場合は以降の計算を打ち切るようにできる。

f = lambda{|arr| arr.first }
g = lambda{|x| x + 1 }
h = lambda{|x| x * 2 }

# Proc#>=で合成する
i = f >= g >= h

i.([3,5]) # => 8
i.([])    # => nil


これは、図にするとこのようなイメージである。

f:id:yuroyoro:20140216195812p:plain


合成する関数がどれだけ増えようと問題がない。

j = lambda{|x| x * 3 }

# 新たに関数jを合成
k = f >= g >= h >= j

k.([3,5]) # => 24

k.([]) # => nil


こんどこそめでたしめでたし。

文脈付き関数合成


Proc#>=によって、関数合成の際に細工をすることで、「途中で計算が失敗したら打ち切る関数合成」を実現できた。
では、nilチェックのような「細工」を任意に指定できるようにしてはどうだろうか?


たとえば、「計算の途中結果をputsで標準出力に吐く」関数合成をしたいとする。
そのために、どのような「細工」をするかを設定するProc#liftメソッドを用意しよう。

class Proc

  def lift(ctx)
    # 引数の「細工」を行うProcオブジェクトをインスタンス変数に設定しておく
    @ctx = ctx

    # 自身の>=メソッドを定義する(特異メソッド)
    def self.>=(g)
      lambda{|x|
        # ctxに、合成する関数gと、自身の計算結果を渡して処理を「細工」する
        @ctx.call(g, self.call(x))
      }.lift(@ctx) # liftの戻り値のProcオブジェクトも同様にliftしておく
    end

    self
  end
end


少々トリッキーな実装なので解説すると、Proc#liftメソッドは細工を行うProcオブジェクト(ctx)を受け取る。
このProcオブジェクト(ctx)は、第一引数にProc#>=メソッドで渡された合成先の関数g、第二引数に合成元の関数fの計算結果であるxを受け取るようにしておく。


liftメソッド内では、特異メソッドとしてProc#>=メソッドを定義する。Proc#>=はインスタンス変数として記憶してあるctxに、合成する関数gと、自身の計算結果を渡して処理を「細工」するようなlambdaを返す。
なお、続けて>=で合成をチェーンできるように、戻り値として返すlambdaも同様に`lift`しておく。


これで準備はできた。
「計算の途中結果をputsで標準出力に吐く」細工を行うctxは、以下のように書く。

ctx = lambda{|g,x|
  # 引数の関数gを呼び出す
  res = g.call(x)  

  # 結果を出力する
  puts "g(#{x}) -> #{res}"

  res
}


では、実際に上記のctxをProc#liftメソッドに渡して、できあがった合成関数を呼び出してみよう。

# Proc#liftで関数合成に細工する
i = f.lift(ctx) >= g >= h

i.call([3,5])
# g(3) -> 4 # ctxから出力
# g(4) -> 8 # ctxから出力
# => 8


関数gと関数hの呼び出しの後に、標準出力へ結果が出力されていることがわかる。
これは、図にするとこのような感じだ。

f:id:yuroyoro:20140216195811p:plain


先ほどの「nilならば計算を途中で打ち切る」細工は、ctxを以下のように定義すればいい。

ctx = lambda{|g, x|
  # nilならばgを呼び出さずにnilを返して後続の計算を打ち切る
  return x if x.nil? 
  g.call(x)
}


これで、先ほどと同じように動作する。

i = f.lift(ctx) >= g >= h

i.call([4,8]) # => 10

i.call([]) # => nil


この細工を行う関数合成は、前回、前々回の内容を一般化したものだ。

「関数型Ruby」という病(4) - Applicativeスタイル(的ななにか) - ( ꒪⌓꒪) ゆるよろ日記
「関数型Ruby」という病(5) - Object#tryはMaybeモナドの夢を見るか? - ( ꒪⌓꒪) ゆるよろ日記

モナ……


さて、Proc#liftで「細工」を指定することで、様々な細工を関数合成に施すことができるようになった。
ここで、もう一度図を見直してみよう。

f:id:yuroyoro:20140216195810p:plain


先ほどの「nilならば計算を途中で打ち切る」細工は、「失敗するかもしれない計算」という【文脈】上で関数合成が動作しているように見える

f:id:yuroyoro:20140216195809p:plain


「標準出力に吐く」細工は、「結果を出力しながらする計算」という【文脈】上で関数合成が動作しているように見える。 あるいは、関数合成の下に「細工」が【配管】されているように見える。


「細工」を設定するメソッドを`lift`と名付けたのは、実は関数合成を【文脈】上に「持ち上げる」という意味を込めているからだ。


ここで上げたほかにも様々な文脈が考えられる。「外部から環境を与えられる文脈」「複数の結果を組み合わせる文脈」「非同期で計算する文脈」など……。


あれ、それってモナ……。おっと誰か来たようだ。


実際、モナ……則どころかモナ……の形すらしていない(returnもbindもない)のでモナ……ではないのだが、よくわからないと評判のアレも実はこういう配管をやるためのデザインパターンの一種である、と捉えると必要以上に恐怖を覚えずとも済む。

f:id:yuroyoro:20140216221452j:plain

Proc#ymsr


なお、このProc#liftは拙作lambda_driver.gemに実装されており、liftは別名`ymsr`にaliasされている。

Aliased Proc#lift to Proc#ymsr · 953d5d9 · yuroyoro/lambda_driver · GitHub

f:id:yuroyoro:20140216221647j:plain