Control.Arrow (2)

http://d.hatena.ne.jp/mzsms/20091119/1258657366 のつづき。

階乗

このエントリ全体がそうですが、この節はとくに、http://d.hatena.ne.jp/takkan_m/20070905/1189001272 を参考にしました。

ここまでの道具立てで、階乗関数が書けるので、階乗関数を書いてみよう。まず、書きたい関数はつぎのようなもの:

module Scrach where
import Control.Arrow

fac 1 = 1
fac n = n * fac (n - 1)
*Scrach> fac 5
120

まず、パターンマッチングではなく、ifを使った書き方に、それも次の補助関数if'を使った書き方に書き換える:

if' b t e = if b then t else e

fac' n = if' (n == 1) 1 (n * fac'(n - 1))

つぎに、これは明らかにloopを使うので、どういう関数をloopに渡すのかを考える:

  • loopに渡す関数は、\(b, d) -> (c, d)という形。
  • これは有限ループなので、たぶん、\(b, f) -> (f b, g f)というイディオムを使う。
  • 入力bはInt型、出力(f b)もInt型、よってfはInt->Int型、gは(Int->Int)->(Int->Int)型。
  • gについてさらに考える:
    • g f = \n -> ??? という形になるはず。
    • nが1以外のとき、g f = \n -> n * (f (n - 1))。これで、nがInt型、fがInt->Int型だとすれば、すべて型があっている。
    • nが1のとき、g f = \n -> 1。もちろん、型はあっている。
    • よって、g f = \n -> if' (n == 1) 1 (n * f (n - 1))
  • loopに渡す関数は、\(b, f) -> (f b, \n -> if' (n == 1) 1 (n * f (n - 1)))
fac' = loop  (\(b, f) -> (f b, \n -> if' (n == 1) 1 (n * f (n - 1))))

次に、\(b, f) -> という部分をどう消すかを考える。

  • これはタプルを受けて、タプルを返しているので、その点だけをみれば(***)の出番のように思える。
  • しかし、(***)は出力タプルの第一要素は入力タプルの第一要素だけ、出力タプルの第二要素は入力タプルの第二要素だけしか参照できないところ、この関数は出力タプルの第一要素が入力タプルの第二要素を参照しているので、使えない。
  • だから、(&&&)を使って、入力タプルを複製し、それぞれを出力タプルの第一要素、第二要素に変換することにする。イメージとして、「(Int, Int -> Int) -> Int &&& (Int, Int -> Int) -> (Int -> Int)」のような感じ。
fac' = loop  ((snd &&& fst >>> app) &&& (\(b, f) n -> if' (n == 1) 1 (n * f (n - 1))))

最後の\(b, f) n -> ... の部分は、二引数関数だというのがネック。他にも方法がありそうな気はするけど、これは一度タプルにして操作してcurryで二引数関数に戻すことにする:

fac' = loop  ((snd &&& fst >>> app) &&& curry (\((b, f), n) -> if' (n == 1) 1 (n * f (n - 1))))

その上で、\((b, f), n) -> を消す。ここはいろいろ試行錯誤したんだけど… とりあえず、次のように書くこともできる:

fac' = loop  ((snd &&& fst >>> app) &&& curry f)
f = snd *** (flip (-) 1) &&& id &&& ((== 1) >>> if' >>> ($ 1)) >>> ((second fst) >>> app) &&& (snd >>> snd) >>> (second fst >>> uncurry (*)) &&& (snd >>> snd) >>> snd &&& fst >>> app

(いま改めて書いたら、一発で動いた!)

これは補助関数fの先頭でnを参照する操作分だけ分岐させて、それを順番にfを参照したラインに、いわば組み込んでいっている。でも、そうするくらいだったら、nを参照するラインを一つだけにしてそこではnを変更せずに保持し、もう一つのラインに適宜組み込んでいくようにしたほうが分かりやすいかもしれない*1

fac' = loop  ((snd &&& fst >>> app) &&& curry f)
f = first snd >>> (second (flip (-) 1) >>> app) &&& snd >>> uncurry (*) &&& snd >>> second ((== 1) >>> if' >>> ($ 1)) >>> snd &&& fst >>> app

解説つけると、こんな感じ:

fac' = loop  ((snd &&& fst >>> app) &&& curry f)
f =                                           -- ((b, f), n)
    first snd                                 -- (f, n)
    >>> (second (flip (-) 1) >>> app) &&& snd -- (f (n - 1), n)
    >>> uncurry (*) &&& snd                   -- ((f (n - 1)) * n, n)
    >>> second ((== 1) >>> if' >>> ($ 1))     -- ((f (n - 1)) * n, if' (n == 1) 1)
    >>> snd &&& fst                           -- (if' (n == 1) 1, (f (n - 1)) * n)
    >>> app                                   -- if' (n == 1) 1 (f (n - 1)) * n)

補助関数fをfac'に組み込んで、完成:

module Scrach where
import Control.Arrow

fac 1 = 1
fac n = n * fac (n - 1)

if' b t e = if b then t else e

fac' = loop ((snd &&& fst >>> app) &&& curry (first snd >>> (second (flip (-) 1) >>> app) &&& snd >>> uncurry (*) &&& snd >>> second ((== 1) >>> if' >>> ($ 1)) >>> snd &&& fst >>> app))
*Scrach Control.Arrow> fac 5
120
*Scrach Control.Arrow> fac 10
3628800
*Scrach Control.Arrow> fac' 5
120
*Scrach Control.Arrow> fac' 10
3628800

先生! 階乗関数の難読化に成功しました!

(|||)、(+++)、left、right

さらに、(|||)、(+++)、left、right:

-- Data.Either
data  Either a b  =  Left a | Right b   deriving (Eq, Ord, Read, Show)
either                  :: (a -> c) -> (b -> c) -> Either a b -> c
either f _ (Left x)     =  f x
either _ g (Right y)    =  g y

-- Control.Arrow
class Arrow a => ArrowChoice a where
        left :: a b c -> a (Either b d) (Either c d)
        right :: a b c -> a (Either d b) (Either d c)
        right f = arr mirror >>> left f >>> arr mirror
                        where   mirror (Left x) = Right x
                                mirror (Right y) = Left y
        (+++) :: a b c -> a b' c' -> a (Either b b') (Either c c')
        f +++ g = left f >>> right g
        (|||) :: a b d -> a c d -> a (Either b c) d
        f ||| g = f +++ g >>> arr untag
                        where   untag (Left x) = x
                                untag (Right y) = y

instance ArrowChoice (->) where
        left f = f +++ id
        right f = id +++ f
        f +++ g = (Left . f) ||| (Right . g)
        (|||) = either

例えば、次のように使える:

module Scrach where

import Data.Either

f :: (Num t) => Either t [Char] -> String
f (Left x) = show (x + 10)
f (Right s) = "String: " ++ s ++ " + 10"
*Scrach> f (Left 10)
"20"
*Scrach> f (Right "10")
"String: 10 + 10" 

カリー=ハワード対応だと、論理和に対応するのかな? まぁ、それはともかく。(|||)、(+++)、left、rightは、このEitherをArrowの中で駆使する手段。(***)、(&&&)、appが使うタプルは、例えば(Int, String)型だと、Int型とString型の両方の値が入っている。これが Either Int String だと、Int型かString型のどちらかの値が入っている、ということになる。

(つづく)

*1:これを並列計算することを考えるならば、理屈の上ではラインを増やした方が良いかもしれないけど… しかし、そもそもHaskellは、普通に関数を書けば賢く並列計算されるべきだろう、理想的には。なんでGHCはネイティブコードなんでしょうね。簡潔に書いたら、バーチャルマシン上で動的最適化、賢く並列、どんどん投機的実行… っていうほうが向いている言語だと思うんだけど。