ラベル 継続 の投稿を表示しています。 すべての投稿を表示
ラベル 継続 の投稿を表示しています。 すべての投稿を表示

2013年4月27日土曜日

Haskell で callCC 関数 - 「継続渡しスタイル」をつなげた計算を途中で破棄する

1. Haskell で callCC の定義を考える

これまで Haskell による「継続渡しスタイル」の計算と、その計算を「つなげる」関数について見てきた。

  1. 継続渡しスタイル (CPS)
  2. 「継続渡しスタイル」の計算をつなげる関数

今回は、「継続渡しスタイル」をつなげた計算の流れをコントロールする関数である

callCC

について考える。

 

a. callCC 関数の大雑把なイメージ

はじめに、定義したい関数の大雑把なイメージをしておく。

前回までの復習

「継続渡しスタイル」の計算では、関数が「目的とする結果」を単純に返さない。関数が「目的とする結果」を得るために必要な引数に加え、「目的とする結果を与える関数」を引数に与えることにより、「目的とする結果」の利用先を指定する。

「継続渡しスタイル」の関数に、「目的とする結果」を得るために必要な引数を与えたときの関数の型は、

(a –> r) –> r

型変数 a が「目的とする結果」。関数 a –> r は「目的とする結果を与える関数」。

型が複雑なので、この型に対して別名を付けておく。

type Cont r a = (a -> r) -> r

先回、このような型の計算を「つなげる」 comb 関数を定義した。型のみ示す。

comb :: Cont r a -> (a -> Cont r b) -> Cont r b

SnapCrab_No-0152

comb 関数は、「継続渡しスタイル」の計算を「つなげた」結果、再び「継続渡しスタイル」の計算になるように定義した。そのため、同じ comb 関数を使い、「継続渡しスタイル」の計算を次々につなげていくことができる。

SnapCrab_No-0155

callCC の動作イメージ

今回、定義したい callCC 関数は、計算の流れを分岐させることを目的とする。「つながっている計算」における「任意の時点の計算結果」を、別の計算へとつなげる。

SnapCrab_No-0151

 

b. モナドについて考えない

callCC 関数は Control.Monad.Cont.Class に定義されている。

Continuation モナド によると、

MonadCont クラスは callCC 関数を提供します.この関数は,Continuation モナド用の脱出継続機構を提供するものです.脱出継続は現在の計算を中断し,即座に値を返します.これにより,Error モナドでの throwError および catchError に類似した効果が得られます.

callCC の定義は、以下のように示されている。

callCC f = Cont $ \k -> runCont (f (\a -> Cont $ \_ -> k a)) k

ここではモナドについて考えず、「継続渡しスタイル」の計算をつなげ、「つなげた計算を途中で脱出する」という観点から、callCC がなぜこのように定義されているかについて考える。

 

2. Scheme における call/cc の動作を確認する

予め Scheme における  call/cc について確認しておく。

Scheme では、計算の「任意の時点における継続」を取り出すことができる。継続を取り出すための手続きは

Call-with-current-continuation

略して、

call/cc

と呼ばれる。

Scheme の特徴的は、計算を「継続渡しスタイル」で記述しなくても、call/cc により通常の計算式に対して、任意の時点で継続を取り出せること。

Scheme 入門 16. 継続 には、一般的な「継続渡しスタイル」の計算と比較しながら、「Scheme の継続」と call/cc について、次のように述べられている。

継続はクロージャの連鎖です。

しかし、継続渡しスタイルでプログラムを読み書きするのはわずらわしいので、 通常の書き方のプログラムで継続を扱えると便利です。

そこで、Scheme では継続はファーストクラスオブジェクト(つまり普通の変数)として実装されており、 call-with-current-continuation という関数を使って任意の時点での継続を取り出すことができます。また、取り出した継続は好きなだけ再利用することができます。

 

a. call/cc を使わない計算式

SnapCrab_No-0173例えば、次の簡単な計算を元にして、call/cc の動作を確認する。

(display
 (- (* (+ 1 2) 3) 4))   ;=> 5

Scheme は「内側の括弧から外側へ」と評価がされる。そのため、上記の式は次の順序で計算が進む。

  • 1 + 2 => 3
  • 3 * 3 => 9
  • 9 – 4 => 5

それぞれの演算子に対応した「計算方法」と「結果」があり、結果が「後続の計算」に与えられる、と見なせる。

 

b. 「継続」を呼び出さない場合

先ほど述べたように、Scheme では「継続渡しスタイル」で計算を記述しなくても、任意の時点における「継続」を call/cc で取り出すことができる

SnapCrab_No-0175例えば、上記の計算に対して、次のように call/cc を呼び出し、「継続」を取り出すことができる。

(display
 (- (call/cc
     (lambda (exit)
       (* (+ 1 2) 3)))
    4))   ;=> 5

この call/cc 手続きにより取り出された「継続」は、

「ある値から 4 を引いた結果を表示する」

こと。Scheme の call/cc 手続きは、「継続」が call/cc に与えるラムダ式の「引数」に与えられる。(ここでは引数 exit )

上記の計算の場合、ラムダ式本体で「継続」 exit の呼び出しを行なっていない。このとき、ラムダ式本体を評価した結果が返される。計算は先ほどと同じように、次の順序で行われる。

  • 1 + 2 => 3
  • 3 * 3 => 9
  • 9 – 4 => 5

call/cc により継続を取り出したが、継続を全く活用していない。

 

c. 「継続」を呼び出す場合

SnapCrab_No-0176次に、継続を call/cc に与えたラムダ式本体で呼び出す場合の動作を確認する。

(display
 (- (call/cc
     (lambda (exit)
       (* 
        (exit (+ 1 2)) 
        3)))
    4))   ;=> -1

先ほどと違い、「 1 と 2 を足した結果」から、「 4 を引いた結果が表示」された。

注目する点は、call/cc に与えたラムダ式本体にある「 3 をかける計算」が行われなかったこと。継続 exit を呼び出すことにより、計算が途中でキャンセルされ、実行位置がジャンプしたように見える。

計算は次の順序で行われる。

  • 1 + 2 => 3
  • 3 – 4 => –1

先ほどの計算と比べると、

  • 1 + 2 => 3
  • 3 * 3 => 9   ← この式の実行がキャンセルされた
  • 3 – 4 => –1

 

d. call/cc のまとめ

call/cc 手続きの呼び出しをまとめると、call/cc の評価結果を次のように見なせることが分かる。

call/cc に与えるラムダ式の本体において、

  1. 「継続」が呼び出されない場合、ラムダ式本体が通常通り評価され、その結果が call/cc を呼び出した結果となる。そのため、call/cc 手続きの呼び出しがあってもなくても計算結果は同じ。
  2. 「継続」が呼び出される場合、継続に与える値が call/cc を評価した結果となる。

 

3. Haskell で callCC を定義する

Scheme の call/cc 手続きを参考にして、Haskell で同じような関数を定義したい。

繰り返すが、Scheme では通常の計算に対して、call/cc により「任意の時点における継続」を取り出すことができる。「継続渡しスタイル」の計算を明示的に記述し、継続を扱う必要はない。

Haskell では、通常の計算に対して、継続を取り出す関数は用意されていない。そのため、Scheme のように継続を扱うためには、「継続渡しスタイル」の計算において考えなければならない。

Scheme の call/cc 手続きに相当する関数を Haskell で定義するには、「継続渡しスタイル」の計算の「つなげ方」をコントロールすることにより実現する。

 

a. callCC に与える引数について

Scheme の call/cc に相当する関数を、Haskell で callCC 関数として定義する過程を考える。

最初に call/cc 手続きに与えるラムダ式に注目する。先ほどの Scheme の例(「継続」を呼び出す場合)を再掲。

(display
 (- (call/cc
     (lambda (exit)
       (* 
        (exit (+ 1 2)) 
        3)))
    4))   ;=> -1

ここで、以下の点について確認しておく。

  1. call/cc に与える引数は「関数」である。
  2. その関数は「一つの引数(ここでは exit )」をとる。
  3. 引数 exit には call/cc を呼び出した時点の「継続」が与えれる。
  4. 「継続」は、値を一つとる関数である。

 

4. 「継続」を適用しない場合

次に、Scheme で call/cc 手続きに与えるラムダ式本体で、「継続を呼び出さない」場合を参考にしながら、Haksell の callCC の定義を考えていく。

(display
 (- (call/cc
     (lambda (exit)
       (* (+ 1 2) 3)))
    4))   ;=> 5

まずは、この関数と同じ内容の計算を Haskell で定義してみる。

 

a. Haskell で継続渡しスタイルの計算

先ほど述べたように、Scheme では継続渡しスタイルで call/cc 手続きを考えなくても良い。しかし、Haskellでは「継続渡しスタイル」で考えなければならない。

予め、「継続渡しスタイル」にする関数と、「継続渡しスタイル」を「つなげる」関数が、以下のように定義されているとする。

ret x = \k -> k x
comb m n = \k -> m (\x -> n x k)

また、上記の関数を元に、継続渡しスタイルの「足し算・かけ算・引き算」が用意されているとする。

add_cps x y = ret $ x + y
mul_cps x y = ret $ x * y
sub_cps x y = ret $ x - y

 

b. callCC 関数を適用した場合を想像する

SnapCrab_No-0177そして、callCC 関数を次のように使うと想定する。

main = print $ ((callCC $ \exit ->
                 add_cps 1 2 `comb` \x ->
                 mul_cps x 3) `comb` \y ->
                sub_cps y 4) id

最初に callCC 関数に与えられる「無名関数」 に注目。

\exit –> …

これより、callCC 関数に与える引数 f を想定できる。

callCC f =

SnapCrab_No-0179

この「無名関数」 の引数 exit が callCC 関数を適用した後の「継続」を導く。

  1. exit 関数を適当な値に適用すると、
  2. callCC 関数を適用した後の「継続」に値を与え、
  3. exit 関数以降の計算はキャンセルされる。

という仕組みを考えていく。

 

c. callCC の定義

上記の「無名関数」の引数は1つ ( exit ) なので、次のような形で定義できる。

callCC f = f 

ここでは の中身について、とりあえず考慮しない。

繰り返すが、 Haskell では「継続渡しスタイル」の計算の「つながり」に対して callCC 関数の定義する。

SnapCrab_No-0151もし、callCC 関数を適用した結果、「継続渡しスタイル」の型が返されるなら、その結果に対して更に計算をつなげることができる。つまり、callCC 関数を適用した結果、「継続渡しスタイル」が返されると都合が良い。よって、次のように定義する。

callCC f = \k -> f 

問題は「継続」である引数 k を、何に対して適用するのか?それとも、何に対して与えるのか?ということ。

ここでもし、

 f 

の結果が「継続渡しスタイル」として返されることが保証されるなら、callCC 関数の引数 k を

 f 

によって生成された「継続渡しスタイル」の計算に与えることにより、callCC 関数の後にくる計算へとつなげることができる。

callCC f = \k -> f k

上記の計算では、引数 exit を callCC 関数の中で適用していない。そのため、 の型がどのようなものであっても、最終的な結果に影響を与えない。

 

d. 具体例で確認する

ここまで定義で、具体的な式を定義で置き換え、動作を確認する。

(callCC $ \exit -> add_cps 1 2 `comb` \x -> mul_cps x 3)
=> \k -> (\exit -> add_cps 1 2 `comb` \x -> mul_cps x 3)  k
=> \k -> (add_cps 1 2 `comb` \x -> mul_cps x 3) k
=> \k -> (\k -> k 9) k
=> \k –> k 9

f の結果が「継続渡しスタイル」であれば、exit 関数を適用しない場合、の中身は何でも良いことが分かる。

そのため、 に相当する式としてどのような値を与えても、この場合、計算に影響を与えない。例えば、 の代わりに  undefeined を与えても答えを求めることができる。

callCC f = \k -> f undefined k

 

5. 「継続」を適用する場合

SnapCrab_No-0178次に、Scheme で call/cc 手続きに与えるラムダ式本体において、「継続を呼び出す」例を参考にしながら、callCC 関数の定義を考えていく。

先ほどの Scheme の例を再掲する。

(display
 (- (call/cc
     (lambda (exit)
       (* 
        (exit (+ 1 2)) 
        3)))
    4))   ;=> -1

これを Haskell で「継続渡しスタイル」にして書きなおすと、以下のようになる。

main = print $ ((callCC $ \exit ->
                 add_cps 1 2 `comb` \x ->
                 exit x      `comb` \y ->
                 mul_cps y 3) `comb` \z ->
                sub_cps z 4) id

今回は、callCC 関数に与える無名関数の引数である exit を、callCC 関数に与えた無名関数の中で適用している。そのため、先ほどの callCC 関数の定義では思うような結果は得られない。

callCC f = \k -> f undefined k

とりあえず undefined として、保留していた部分を考えなければならない。

 

a. exit 関数の役割

ところで、この undefined は、callCC 関数を適用した場合、exit 関数に与えられる。

SnapCrab_No-0183

exit 関数の役割は、exit 関数を値に適用したら、

  • その値が callCC 関数の「後に続く計算」(継続)に与えられること。 … 【条件A】
  • exit 関数を適用した「後に続く計算」を破棄すること。 … 【条件B】

【条件A】より、callCC 関数の「後に続く計算」を表す引数 k に「値を渡す」関数を想定できる。

callCC f = \k –> f (\x –> k x) k

しかし、この定義ではコンパイルエラーとなる。なぜなら、【条件B】が満たされていないため。

 

b. 後続の「継続」を破棄する

では、どのような定義にすれば、【条件B】を満たすことができるのだろう?

もう一度、「継続渡しスタイル」について、簡単な例で考えてみる。

main = print $ (add_cps 1 2 `comb` \x ->
                mul_cps x 3) id  -- 9

ここでは、2つの「継続渡しスタイル」の計算をつなげている。この2つの計算の間に、次のような「継続渡しスタイル」の計算を挿入してみる。

main = print $ (add_cps 1 2 `comb` \x ->
                (\k -> k x) `comb` \y ->
                mul_cps x 3) id  -- 9

新たに挿入した「継続渡しスタイル」の計算は、全体の計算に影響を与えない。

これに対して、「継続」に値を与えないように式を変更すると、結果として挿入した「継続渡しスタイル」の x の値が返される。なぜなら、後続する計算に中間結果を与えないため。

main = print $ (add_cps 1 2 `comb` \x ->
                (\k -> x)   `comb` \y ->
                mul_cps x 3) id  -- 3

変数 k は使われないので、ワイルドカードで良い。

main = print $ (add_cps 1 2 `comb` \x ->
                (\_ -> x)   `comb` \y ->
                mul_cps x 3) id  -- 3

確認のため、この式を定義で置き換えてみる。

add_cps 1 2 `comb` \x -> (\_ -> x) `comb` \y -> mul_cps x 3
=> (\_ -> 3) `comb` \y -> mul_cps x 3
=> \k -> (\_ -> 3) (\x -> (\y -> mul_cps x 3) x k)
=> \k -> 3

「継続」に値を与えないことにより、後続の計算が破棄されたのが分かる。

次に、上記の式を exit 関数を与える test 関数として定義しなおしてみる。

test exit = (add_cps 1 2 `comb` \x ->
             exit x      `comb` \y ->
             mul_cps x 3) id

先ほどと同じように、途中で「継続」を破棄するためには、test 関数に次の無名関数を与えれば良い。

main = print $ test (\x -> \_ -> x)

callCC の定義に戻る。先ほどコンパイルエラーとなったのは、下記の式だった。

callCC f = \k –> f (\x –> k x) k

これを「後続する継続を破棄するときに与えた無名関数」を真似て、次のように変更する。

callCC f = \k –> f (\x –> \_ –> k x) k

これにより、先ほど挙げた【条件B】を満たすことができる。

確認のため、callCC を使った式を定義で置き換えてみる。

(callCC $ \exit -> add_cps 1 2 `comb` \x -> exit x `comb` \y -> mul_cps y 3) 
=> \k -> (\exit -> add_cps 1 2 `comb` \x -> exit x `comb` \y -> mul_cps y 3) (\x -> \_ -> k x) k
=> \k -> (\exit -> exit 3 `comb` \y -> mul_cps y 3) (\x -> \_ -> k x) k
=> \k -> ((\x -> \_ -> k x) 3 `comb` \y -> mul_cps y 3) k
=> \k -> ((\_ -> k 3) `comb` \y -> mul_cps y 3) k
=> \k -> (\k' -> (\_ -> k 3) (\x -> (\y -> mul_cps y 3) x k')) k
=> \k -> (\k' -> (k 3)) k
=> \k -> k 3

これで、callCC 関数の定義における「無名関数」の意味が理解できた。

しかし、自分の頭では直感的に理解できないなぁ。。

2012年5月2日水曜日

Haskell で「継続渡しスタイル」の計算をつなげる関数

Haskell で継続渡しスタイル (CPS) のつづき

1. 継続モナドに相当する関数を考える

前回は、「継続渡しスタイル」の関数の例を見た。

今回は、継続モナドにおける

(>>=) 関数

の定義について、順を追って考える。

All About MonadsContinuation モナド には、継続モナドが、以下のように定義されている。

newtype Cont r a = Cont { runCont :: ((a -> r) -> r) } 
  
instance Monad (Cont r) where 
    return a       = Cont $ \k -> k a                   
    (Cont c) >>= f = Cont $ \k -> c (\a -> runCont (f a) k) 
  1. 継続渡しスタイルの計算をフィールドに持つ型を定義し、
  2. その型コンストラクタを Monad クラスのインスタンスとしてる。

ここでは、Monad クラスについては立ち入らず、継続渡しスタイルの計算を「つなげる」関数を定義してみる。

 

2. 継続渡しスタイルでは、本質的な計算結果を、別の関数に引き渡す

継続渡しスタイルでは、関数の結果を返すのではなく、結果を別の関数に引き渡す。

継続渡しスタイル - Wikipedia によると、

継続渡しスタイルで書かれた関数は、通常の直接スタイル(direct style)のように値を「返す」かわりに、「継続」を引数として陽に受け取り、その継続に計算結果を渡す。継続とは、関数の計算結果を受け取るための(一般には元の関数とは別の)関数のことである。

継続渡しスタイルの関数を、次の2つの部分に分けて考えることができる。

  1. 本質的な計算
  2. 「本質的な計算」の結果を、別の関数に与える計算

ここで「本質的な計算」とは、継続以外の引数を元に、直接スタイルで定義する計算に相当するものと捉えておく。

 

継続渡しスタイルの例

直接スタイルの関数から、継続渡しスタイルの関数を定義してみる。

例えば、2つの値を足し合わせる関数 add の定義は、

add x y = x + y

add 関数を、継続渡しスタイルにする。

add_cps x y k = k $ x + y

add_cps が行う「本質的な計算」は、2つの値を足し上げること。

x + y

継続渡しスタイルでは、2つの値を足し上げた結果を、別の関数に与える。

k $ x + y

変数 k は「本質的な計算」の結果を受け取る 1 引数関数で、継続を表す。

 

3. 継続渡しスタイルおける「中間結果」と「最終結果」

add_cps 関数の型を調べると、

*Main> :t add_cps
add_cps :: (Num a) => a -> a -> (a -> b) -> b

add_cps 関数に、足し上げる 2 つの数値を与えたときの型は、

*Main> :t add_cps 1 2
add_cps 1 2 :: (Num t) => (t -> b) -> b

add_cps 1 2 の計算結果は、

  1. 数値を受け取る1引数関数(継続)を与えると、
  2. 最終的な結果を生成する関数が返される。

一般的に、ある計算結果を受け取る1引数関数を与えると、最終的な結果を生成する関数の型は、

(a -> r) -> r

この関数における、型変数の意味を確認しておく。

  1. 型変数 a は、継続渡しスタイルにおける「本質的な計算」結果。
  2. 関数 (a -> r) は、「本質的な計算」の結果を与えると、最終的な結果を生成する 1 引数関数(継続)。
  3. 型変数 r は、継続を与えたときの最終結果。

ここまで、型変数 a に対して、「本質的な計算結果」という表現を使ってきた。理由は、継続渡しスタイルにおいて、「本質的な計算結果」を渡す先である継続は任意であり、関数が行う中心的な計算は、継続に渡す前の値であるため。

ただし、関数全体から見ると、

(a -> r) -> r

という型における型変数 a は、「最終的な計算結果」である型 r に対して、

「中間結果」

と見なせる。よって、以降、この型変数に対応する値を「本質的な計算」結果ではなく、「中間結果」と呼ぶ。

最初に、『継続渡しスタイルの計算を「つなげる」関数』を定義すると述べた。正確には、

  1. 継続渡しスタイルの計算に対して、「中間結果」を生成する引数が与えられており、
  2. 後は、継続を与えるだけの状態にある計算を「つなげる」関数。

 

4. 計算をつなげる方法

具体的な例で、継続渡しスタイルの関数を「つなげる」ことを考える。

先ほど、足し算の継続渡しスタイルを書いた。これに加え、かけ算と引き算の継続渡しスタイルを定義する。

mul_cps x y k = k $ x * y
sub_cps x y k = k $ x - y

上記の関数から、

(a -> r) -> r

という型になる計算の例を挙げると、

  • add_cps 1 2
  • mul_cps 3 3
  • sub_cps 9 4

これらの計算は、継続を与えると、最終的な結果が生成される。このような計算をつなげたい。img_0120

ここで、2つの継続渡しスタイルの計算 m, n を、「つなげる」方法について、次のように考える。

  • 計算 m の「中間結果」を、計算 n へ与える。 …(1)
  • 2つの継続渡しスタイルの関数をつなげた結果、再び継続渡しスタイルになる。 … (2)

(1) により、計算 n は、計算 m の計算結果を参照できる。計算 n の型は、

a -> (a -> r) -> r

上記の計算例に当てはめると、以下のような関数となる。

  • \x -> mul_cps x 2
  • \x -> sub_cps x 3
  • \x -> sub_cps x 4

img_0128

継続渡しスタイルでは、中間結果を、別の関数に渡す。

継続渡しスタイルにする前の、直接スタイルの関数で考えると、上例の計算を「つなげる」ことは、足し算、かけ算、引き算を組み合わせて計算を構成することを意味する。

img_0129

大雑把なイメージで考えると、下図の通り。

  1. 計算を行い、
  2. その結果を次の計算に渡し、
  3. それを繰り返し、次々と連鎖させる。

img_0126

(2) のように、計算をつなげた結果、つなげる前と同じ型の計算にする理由は、個別につないだ計算を、更に大きな計算の組み合わせの部品として使うことができるようにするため。

img_0127

 

5. 継続渡しスタイルの計算をつなげる関数の実装

a. comb 関数の型

継続渡しスタイルの2つの計算をつなげる関数を comb とする。

comb 関数の第1引数は、継続渡しスタイルの関数において、後は、継続を与えるだけの状態である関数の型と捉えておく。

(a -> r) -> r

第2引数は、継続渡しスタイルの関数に対して、「中間結果」を受けとる引数を追加する。

a -> (a -> r) -> r

とりあえず、comb 関数の型を、以下のように考えておく。

comb :: ((a -> r) -> r) ->
        (a -> (a -> r) -> r) ->
        (a -> r) -> r

予め、comb 関数を使い、2 つの計算をつなげる方法を想像すると、

add_cps 1 2 `comb` \x -> mul_cps x 3

 

b. comb 関数の実装方針

上記の具体的な計算を見ながら、comb 関数を定義するとき、以下の2点を満たすことを考える。

  1. comb 関数の第2引数に与える無名関数の引数 x は、add_cps の「中間結果」を与える。
  2. comb 関数を適用した結果、add_cps 1 2 や mul_cps x 3 と同じ型になる。

 

c. comb 関数の実装

では、comb 関数の定義を順に考えていく。

comb 関数は、2つ計算をつなげる関数。よって、引数を2つ与える。

comb m n = …

引数 m の型は、(a -> r) -> r) 、引数 n の型は、(a -> (a -> r) -> r)

第1引数 m は継続渡しスタイルの関数で、継続が与えられると最終結果を生成する。そのため、計算 m の中間結果を、1引数関数(継続)に与える。よって、次のような無名関数を与える。

comb m n = m (\x -> … )

第2引数 n は、第1引数の中間結果を受けとる。よって、n に無名関数の引数 x を与える。これにより、comb 関数の第2引数に与える関数は、第1引数の計算の中間結果を参照できる。

comb m n = m (\x -> n x … )

この結果、関数 n から、継続渡しスタイルの関数が取り出される。

ところで、comb 関数により、2つの継続渡しスタイルの関数をつなげた結果、再び継続渡しスタイルとなるようにしたい。そのためには、1引数関数を与えると、最終的な結果が得られる関数となるようにする必要がある。

comb m n = \k -> m (\x -> n x … )

ここで、無名関数の引数 k は、1引数関数(継続)で、2つの継続渡しスタイルの計算をつなげた計算の「中間結果」を受け取る。つまり、k は comb 関数で構築した本質的な計算の結果を、最終的に渡す先の関数となる。

先ほど、第2引数である関数 n を、無名関数の引数 x に適用し、継続渡しスタイルの関数を取り出した。この関数の結果を受け渡す先として、関数 k を定義する。

comb m n = \k -> m (\x -> n x k)

 

d. comb 関数を使う

定義した comb 関数を使ってみる。

main = print $ (add_cps 1 2 `comb` \x -> 
                mul_cps x 3) id           --> 9

3 つの計算をつなげてみる。

main = print $ (add_cps 1 2 `comb` \x -> 
                mul_cps x 3 `comb` \y ->
                sub_cps y 4) id           --> 5

 

e. comb 関数の型宣言を変更

上記2つの計算例は、中間結果と、最終的な結果が伴に数値だった。これを変更し、最後に文字列を返すようにしてみる。

main = print $ (add_cps 1 2 `comb` \x -> 
                mul_cps x 3 `comb` \y ->
                (\k -> k $ "Answer: " ++ show y)) id

その結果、実行するとエラーがでた。(+_+) エラーが出ないようにするためには、comb 関数の型宣言を変える必要がある。

先ほど、comb 関数の型を、以下のように宣言した。この型では、第1引数の中間結果と、それを元に作る中間結果の型が同じ型であるという制約が付いてしまう。

comb :: ((a -> r) -> r) ->
        (a -> (a -> r) -> r) ->
        (a -> r) -> r

第1引数の中間結果から、他の型へ変換できるようにするためには、次のような型宣言にする。

comb :: ((a -> r) -> r) ->
        (a -> (b -> r) -> r) ->
        (b -> r) -> r

これにより、先ほどのエラーがでなくなる。

 

f. type 宣言

しかし、このままでは型宣言が長くて読みにくい。(@_@;

type 宣言で別名を付ける方が良い。

type Cont r a = (a -> r) -> r

上記の型を用い、comb 関数の型宣言を変更する。

comb :: Cont r a -> (a -> Cont r b) -> Cont r b
先ほどエラーとなった comb 関数は、以下のように宣言していた。
comb :: Cont r a -> (a -> Cont r a) -> Cont r a

関数の型が長いときは、別名を付けた方が理解しやすい。

 

g. 中間結果を変換しない関数

継続渡しスタイルの計算を comb 関数でつなげる中で、与えられた中間結果の値を別の値に変換した。

例えば、以下の計算は、中間結果を与えると、別の値に変換する継続渡しスタイルの関数が返る。

  • \x -> mul_cps x 3
  • \y -> sub_cps y 4

これに対して、中間結果を与えても、値を変化させない継続渡しスタイルの関数を返す関数を定義したい。この関数を ret 関数とすると、型は、

ret :: a -> Cont r a

まずは、中間結果を、引数 x に与えると考える。

ret x = …

結果を継続渡しスタイルの関数で返すために、継続を受け取る1引数関数を返すとする。

ret x = \k -> …

中間結果を変換せずに、関数 k に引き渡せばよいので、以下のようになる。

ret x = \k -> k x
ret 関数を使った例を挙げる。
*Main> (add_cps 1 2 `comb` ret) id
3
*Main> (mul_cps 3 4 `comb` ret) id
12

comb 関数の第1引数の中間結果が、最終的に返されるのが分かる。

*Main> (ret 100 `comb` \x -> add_cps x 1) id
101

ret 関数により、任意の値を継続渡しスタイルの計算に投入し、comb 関数でつなげることができた。

よって、ret 関数を使うと、直接スタイルの関数の結果を、継続渡しスタイルにおける中間結果に変換できる。

*Main> :t ret $ 1 + 2
ret $ 1 + 2 :: (Num a) => (a -> r) -> r

そのため、先ほどの main 関数、

main = print $ (add_cps 1 2 `comb` \x -> 
                mul_cps x 3 `comb` \y ->
                sub_cps y 4) id           --> 5

を ret 関数を用いて、次のように書き換えることができる。

main =  print $ (ret (1 + 2) `comb` \x -> 
                 ret (x * 3) `comb` \y ->
                 ret (y - 4)) id

つまり、ret 関数と comb 関数を組み合わせることにより、普通に定義した関数を継続渡しスタイルの連鎖のなかに投入し、結果を別の関数に与え、計算を構築することが可能となる。

 

6. comb 関数を使う例

前回見た継続渡しスタイルの関数を、comb 関数使い、置き換えてみる。

各々、以下の定義を挙げる。

  1. 直接スタイル
  2. 継続渡しスタイル
  3. comb 関数を使う

 

階乗
fact n | n == 0    = 1
       | otherwise = n * fact (n-1)
fact_cps n k | n == 0    = k 1
             | otherwise = fact_cps (n-1) (\x -> k (n * x)) 
fact 0 = ret 1
fact n = fact (n-1) `comb` \x ->
         ret $ x * n

 

累積
prod []     = 1
prod (x:xs) = x * prod xs
prod_cps []     k = k 1
prod_cps (x:xs) k = prod_cps xs (\y -> k (x * y))
prod []     = ret 1
prod (x:xs) = prod xs `comb` \y ->
              ret $ x * y

 

木の葉の数を数える
leafCount (Leaf _)     = 1
leafCount (Branch l r) = leafCount l + leafCount r
leafCount_cps (Leaf _) k = k 1
leafCount_cps (Branch l r) k = leafCount_cps l $ \x ->
                               leafCount_cps r $ \y ->
                               k $ x + y
leafCount (Leaf _)     = ret 1
leafCount (Branch l r) = leafCount l `comb` \x ->
                         leafCount r `comb` \y ->
                         ret $ x + y

 

フィボナッチ数
fib n | n == 0    = 0
      | n == 1    = 1
      | otherwise =  fib (n-1) + fib (n-2)
fib_cps n k | n <= 1    = k n
            | otherwise = fib_cps (n-1) $ \x ->
                          fib_cps (n-2) $ \y ->
                          k $ x + y
fib n | n == 0 = ret 0
      | n == 1 = ret 1
      | otherwise = fib (n-1) `comb` \x ->
                    fib (n-2) `comb` \y ->
                    ret $ x + y

 

リストの平坦化
concat_ []       = ret []
concat_ (xs:xss) = concat_ xss `comb` \yss ->   ret $ xs ++ yss
concat_cps []       k = k []
concat_cps (xs:xss) k = concat_cps xss $ \xss' -> k $ xs ++ xss' 
concat_ [] = ret []
concat_ (xs:xss) = concat_ xss `comb` \yss ->
                  ret $ xs ++ yss

 

foldr
foldr_ _ z []     = z
foldr_ f z (x:xs) = x `f` foldr_ f z xs
foldr2 _ z [] k = k z
foldr2 f z (x:xs) k = foldr2 f z xs $ \y -> k $ f x y 
foldr2 _ z []      = ret z
foldr2 f z (x:xs) = foldr2 f z xs `comb` \y ->
                    ret $ f x y

 

関連記事

2011年5月5日木曜日

Haskell で継続渡しスタイル (CPS)

0. 目次

  1. 継続を理解するには「継続渡しスタイル(CPS)」から
  2. 足し算、かけ算、引き算
  3. 階乗
  4. 木の葉の数を数える
  5. フィボナッチ数
  6. リストの平坦
  7. foldr (畳み込み関数)

 

1. 継続を理解するには「継続渡しスタイル(CPS)」から

All About Monads」の Continuation モナド が理解できない。特に callCC 関数の定義。

callCC f = Cont $ \k -> runCont (f (\a -> Cont $ \_ -> k a)) k

うーん、わずか一行なんだけれど… (+_+)

callCC を含め、継続モナドを理解するための前提が次のように書かれている。

Continuation モナドを使う前に継続渡しスタイル(CPS)について確実に理解しているか,自身の設計課題について継続が最良のソリューションなのかを確認してください.他の言語では継続が要求されるような多くのアルゴリズムにおいて,Haskell では遅延評価意味論のおかげで,継続を必要としません.

(同上より、太字は引用者による)

どうやら「継続渡しスタイル(CPS)」から学ぶことがいいらしい。今回はこれについて調べる。

ところで、「継続」と言って連想するのは Scheme 。「Scheme 入門 16. 継続」によると、

… 多くの解説書ではまず Scheme の継続について説明してから、継続渡しスタイルについて説明していますが、 先に継続渡しスタイルについて説明したほうが、なぜ Scheme に継続というデータ型があるのかがわかりやすいと思います。

今後は、以下の順で理解していきたい。 (希望的観測)

  1. 継続渡しスタイル (CPS)
  2. 継続モナド
  3. callCC 関数

 

2. 足し算、かけ算、引き算

最初は、上記「3. 継続渡しスタイル」で説明されている CPS の例を真似たコードを Haskell で書く。

普通に足し算・かけ算・引き算を定義するなら、

add x y = x + y
mul x y = x * y
sub x y = x - y

これ自体は演算子 +, *, - を置き換えたに過ぎない。

組み合わせて使ってみる。

print (sub (mul (add 1 2) 3) 4)   -- 5

これを「継続渡しスタイル」にしたい。

 

a. CPS の書き方

「継続渡しスタイル」について述べらている解説をいくつか引用する。(装飾は引用者による)

Scheme 入門 16. 継続

継続渡しスタイルとは、関数が、値を返す代わりに、計算した値どの関数に渡すのかを明示的に指定する方法です。

Scheme:CPS

継続と聞くとSchemeのcall/ccを連想するかもしれませんが、むしろここで重要なのは、「継続渡しスタイル(Continuation Passing Style, CPS)」です。CPSそのものは、 PerlでもRubyでもJavaでも書けます。どっちかというと、普通の手続き指向から考え方を変えるのがポイントなんで。

CPSのポイント。手続きは呼び出したら戻ってきません。行ったっきりです。ですから、その手続きの後で何か別のことをやりたいなら、「その後にして欲しいこと=継続」を手続きに渡してやります。

M.Hiroi's Home Page / お気楽 OCaml プログラミング入門

一般のプログラミング言語では、Scheme のように継続を取り出して保存することはできません。そこで、継続 (次に行う処理) を関数 (クロージャ) で表して、それを引数に渡して実行することにします。これを「継続渡しスタイル (CPS) 」といいます。

Haskell/Continuation passing style - Wikibooks

Continuation Passing Style is a format for expressions such that no function ever returns, instead they pass control onto a continuation. Conceptually a continuation is what happens next, …

Continuation – HaskellWiki

1.1.2 Functional metaphors
  • Rather than return the result of a function, pass one or more Higher Order Functions to determine what to do with the result. ...

ポイントは、関数を定義するとき、

  1. 最後に値を返すのではなく、
  2. 関数を呼び出したときに与えた関数に引き渡す

ということ。

先ほど定義した関数 add, mul, sub で考えるなら、

  1. 計算結果を返す代わりに、
  2. 引数 x, y を元に行う計算の結果に対して、関数 k を適用する。

という定義に変更。

add_cps x y k = k $ x + y
mul_cps x y k = k $ x * y
sub_cps x y k = k $ x - y

例えば、 add を変更した add’cps は次のように読む。

add_cps は引数 x, y を足した結果に、関数 k を適用する。

 

b. CPS スタイルの使い方

add_cps を使い、1 と 2 を足した結果を、値を変化させず、そのまま返す関数に渡すなら、

print $ add_cps 1 2 (\x -> x)   -- 3

結果を 2 倍したいなら、

print $ add_cps 1 2 (\x -> x * 2)   -- 6

ところで、add_cps の型を調べると、

add_cps :: (Num a) => a -> a -> (a -> b) -> b

add_cps の第 3 引数である関数 k の型は

a –> b

であり、「返す値」が「関数 k を適用する値」の型と異なっていてもよいことがわかる。

例えば、結果を String 型に変換したいなら、

print $ add_cps 1 2 (\x -> "###" ++ show x ++"###")   -- "###3###"

IO () 型に変換したい場合は、

add_cps 1 2 (\x -> print x)

足し算、かけ算、引き算を組み合わせた、先ほどと同じ計算を行うには、第 3 引数に無名関数をネストさせていく。

print (add_cps 1 2 (\x ->      
       mul_cps x 3 (\y ->      
       sub_cps y 4 (\z -> z))))   -- 5

書くときに意識することは、

  1. add_cps 1 2 の結果を x に渡し、
  2. 次に mul_cps x 3 の結果を y に渡し、
  3. 続いて sub_cps y 4 の結果を z に渡し、
  4. 最後に結果 z をそのまま返す。

… とは言ったもののの、この方法で計算がちゃんとされるのは、何かだまされたような気分。 (@_@;

 

c. どういう風に計算されるのか?

計算の順序を気にせず、計算の様子をイメージしてみる。

予め無名関数を以下のように対応付けておく。

h : \z -> z
g : \y -> sub_cps y 4 h 
f : \x -> mul_cps x 3 g

先ほどの計算を順に考える。

add_cps 1 2 f
=> f $ 1 + 2
=> (\x -> mul_cps x 3 g) 3
=> mul_cps 3 3 g
=> g $ 3 * 3
=> (\y -> sub_cps y 4 h) 9
=> sub_scps 9 4 h
=> h $ 9 - 4
=> (\z -> z) 5
=> 5

定義したときには想像しにくかった具体的なイメージが少しわかったような気がする。

 

d. 関数を適用する流れ

ところで、Haskell では関数の引数が複数の場合、一部引数を適用すると、残りの引数を受け取る関数が返される。

add’cps の一部に引数を適用したときの型を調べる。

add_cps 1 2 :: (Num t) => (t -> b) -> b

定義で置き換えると、具体的には、

add_cps 1 2
=> k $ 1 + 2
=> k 3

ここですぐに値 3 を取り出したい場合、

(add_cps 1 2) (\x -> x) -- 3

値を取り出す前に、ドミノ倒しの要領で、無名関数をつなげていった場合の型を調べてみる。

(add_cps 1 2) (\x -> mul_cps x 3) :: (Num t) => (t -> b) -> b

ついでに、もう一つつなげたら、

(add_cps 1 2) (\x -> mul_cps x 3) (\y -> sub_cps y 4)
  :: (Num t) => (t -> b) -> b

最後に取り出して終わり。

(add_cps 1 2) (\x -> mul_cps x 3) (\y -> sub_cps y 4) (\z -> z)
  :: (Num b) => b

値を取り出す前の型はすべて、

(t -> b) –> b

であることが確認できる。この型がポイントになるので、頭の隅に入れておくこと。

ただし、上記の方法は無名関数をネストさせてないので、引き継いでいく各々の結果を、最後の無名関数内で参照することはできない。

例えば以下のように。

print (add_cps 1 2 (\x ->      
       mul_cps x 3 (\y ->      
       sub_cps y 4 (\z -> 
       [x,y,z]))))         -- [3,9,5]

ところで、これを見て Python による次のようなコードを連想した。

x = 1 + 2
y = x * 3
z = y - 4
print [x,y,z]

Haskell のコードをたとえるなら、ネストした内側の世界は、ネストしている外側の世界を覗くことができる。純粋関数型において「前提として成り立つ世界が存在すること」が、手続き型の「順次流れていく時間」と対応している。

 

e. 「計算の結果を渡す関数」が複数ある場合

add_cps 関数では、第 3 引数が「結果を渡す先」の関数だった。これに対して、「結果を渡す先」の関数をもう一つ増やし、「条件によって渡す先を決める」ように変更してみる。

例えば、x, y が 10 よりも小さい値のときに適用する関数を k とし、それ以外を k’ とする。

add1_cps x y k k' 
    | x < 10 && y < 10 = k  add'
    | otherwise        = k' add'
    where
      add' = x + y

使ってみる。

print $ add1_cps 1 2 (\x -> x * 10) (\x -> x)    -- 30
print $ add1_cps 11 22 (\x -> x * 10) (\x -> x)  -- 33

「条件」も関数として渡すなら、述語関数 p を想定し、

add2_cps x y p k k' 
    | p x y     = k  add'
    | otherwise = k' add'
    where
      add' = x + y

この場合は、

print $ add2_cps 1 2 (\x y -> x < y) 
          (\x -> x * 10) (\x -> x)    -- 30
print $ add2_cps 1 2 (\x y -> x > y) 
          (\x -> x * 10) (\x -> x)    -- 3

次に、「x が y よりも小さいときは計算結果を返し、そうでない場合はエラーを表示したい」とする。

Haskell では返す型が同じ必要があるので、予め以下の型を定義。

data Value a = Return a | Raise Exception deriving Show
type Exception = String

データコンストラクタ Return は値を返す場合に使い、Raise はエラーのときの値を生成するために使う。

print $ add2_cps 1 2 (<) 
      Return (\_ -> Raise "Error")   -- Return 3
print $ add2_cps 2 1 (<) 
      Return (\_ -> Raise "Error")   -- Raise "Error"

 

f. 「結果を渡す先の関数」の引数が複数ある場合

これまでは、「結果を渡す先の関数」が結果を一つ受けとるだけだった。複数の引数が渡された場合、どうなるだろう?

例えば、add3_cps 関数では、関数 k に最初に渡した引数も与えてみる。

add3_cps x y k = k x y $ x + y

これを使い、計算過程と結果を文字列として表示させてみる。

print $ add3_cps 1 2 $ \x y z -> 
    show x ++ " + " ++ show y ++ " = " ++ show z  -- "1 + 2 = 3"

( 全体は gist: 956701 — Gist )

 

3. 階乗

次は階乗の計算で継続渡しスタイルを考える。

普通に階乗を定義するなら、

fact n | n == 0    = 1
       | otherwise = n * fact (n-1)

これを継続渡しスタイルに変更したい。

まずは、n が 0 のとき、値 1 を関数 k に適用する。

fact_cps n k | n == 0    = k 1

次に上記以外の場合をどう定義すればいいのか?

fact’cps n k の意味は、

n の階乗の結果に関数 k を適用する

ということ。よって、意識することは fact_cps の結果を x に渡し、

             | otherwise = fact_cps (n-1) (\x –> …

n の値と上記 x の値をかけて、最後に関数 k に渡す。

             | otherwise = fact_cps (n-1) (\x -> k (n * x)) 

この最後に関数 k を値に適用しているのところがポイント。

( gist: 962816 — Gist )

n が 3 における計算の過程を考えると、

fact_cps 3 (\x -> x)
=> fact_cps 2 (\x -> (\x -> x) (3 * x))
=> fact_cps 1 (\x -> (\x -> (\x -> x) (3 * x)) (2 * x))
=> fact_cps 0 (\x -> (\x -> (\x -> (\x -> x) (3 * x)) (2 * x)) (1 * x))
=> (\x -> (\x -> (\x -> (\x -> x) (3 * x)) (2 * x)) (1 * x)) 1
=> (\x -> (\x -> (\x -> x) (3 * x)) (2 * x)) 1
=> (\x -> (\x -> x) (3 * x)) 2
=> (\x -> x) 6
=> 6

第 1 引数 n の値が小さくなるにつれて、ニョキニョキと無名関数が伸びていく。

正直イメージがしにくい。 てゆうか、具体的に創造できない。。(@_@;

 

a. 累積

ついでに、数値のリストの累積を継続渡しスタイルで書いてみる。

prod_cps []     k = k 1
prod_cps (x:xs) k = prod_cps xs (\y -> k (x * y))

ちょっと書き方に慣れてきた。

 

4. 木の葉の数を数える

なんでも継続」の「末尾再帰と継続」で挙げらている例、

与えられた木の全ての葉の数を数える関数

を考えてみる。

まずは木を次のように定義。

data Tree a = Leaf a | Branch (Tree a) (Tree a) deriving Show

普通に木の葉の要素を数えるには、

leafCount (Leaf _)     = 1
leafCount (Branch l r) = leafCount l + leafCount r

これを継続渡しスタイルにしたい。

a. 継続渡しスタイルで数える

葉のときは要素数 1 は明らかなので、1 に対して関数 k を適用する。

leafCount_cps (Leaf _)     k = k 1

このとき意識することは、

leafCount_cps (Leaf _) の要素数を得たら、その値を関数 k に渡す

ということ。

数える対象が葉でないときも、定義する前に、関数の意味をしっかりと意識しておく。

leafCount_cps (Branch l r) k 

を以下のように解釈する。

  1. leafCount_cps (Branch l r) により、対象の木の葉の数を得ることができる。
  2. そして、その値を関数 k に与える。

これに基づき、左の木の葉の数を数え、その結果を受けとる無名関数を定義する準備をする。

leafCount_cps (Branch l r) k = leafCount_cps l $ \x –>

次に、上記無名関数の中で、右の葉の数を数え、その結果を更に続く無名関数に与える準備をする。

                               leafCount_cps r $ \y ->

最後に上記 2 つの結果を足し合わせ、その結果に対して関数 k を適用する。

                               k $ x + y

ここでも最終的な結果に関数 k を適用しているところがポイント。

でも、何かわかったような、わからないような… (+_+)

 

b. 計算のされ方の確認

具体的な木を作り leafCount_cps 関数を試してみる。

t = Branch 
    (Branch 
     (Leaf 1) 
     (Leaf 2))
    (Branch
     (Leaf 3)
     (Branch 
      (Leaf 4)
      (Leaf 5)))

これに対し、関数を適用。

main = do print $ leafCount t                -- 5
          print $ leafCount_cps t (\x -> x)  -- 5
          leafCount_cps t print              -- 5

( gist: 952890 — Gist )

より単純な場合を想定し、順に定義を置き換えてみる。( Haskell の内部において、遅延評価により実際にどのように計算が行なわれるのかわからないけれど。 )

まずは、葉を 2 つだけ持つ木。

leafCount_cps (Branch (Leaf 1) (Leaf 2)) (\x -> x)
=> leafCount_cps (Leaf 1) $ \x -> leafCount_cps (Leaf 2) $ \y -> (\x -> x) $ x + y
=> (\x -> leafCount_cps (Leaf 2) $ \y -> (\x -> x) $ x + y) 1
=> leafCount_cps (Leaf 2) $ \y -> (\x -> x) $ 1 + y
=> (y -> (\x -> x) $ 1 + y) 1
=> (\x -> x) $ 1 + 1
=> 2

一つ葉を増やした場合で考える。

leafCount_cps (Branch (Branch (Leaf 1) (Leaf 2)) (Leaf 3)) (\x -> x)
=> leafCount_cps (Branch (Leaf 1) (Leaf 2)) $ \x -> leafCount_cps (Leaf 3) $ \y -> (\x -> x) $ x + y
=> leafCount_cps (Leaf 1) $ \x -> leafCount_cps (Leaf 2) $ \y -> (x -> leafCount_cps (Leaf 3) $ \y -> (\x -> x) $ x + y) $ x + y
=> (\x -> leafCount_cps (Leaf 2) $ \y -> (x -> leafCount_cps (Leaf 3) $ \y -> (\x -> x) $ x + y) $ x + y) 1
=> leafCount_cps (Leaf 2) $ \y -> (x -> leafCount_cps (Leaf 3) $ \y -> (\x -> x) $ x + y) $ 1 + y
=> (\y -> (x -> leafCount_cps (Leaf 3) $ \y -> (\x -> x) $ x + y) $ 1 + y) 1
=> (x -> leafCount_cps (Leaf 3) $ \y -> (\x -> x) $ x + y) $ 1 + 1
=> leafCount_cps (Leaf 3) $ \y -> (\x -> x) $ 2 + y
=> (\y -> (\x -> x) $ 2 + y) 1
=> (\x -> x) $ 2 + 1
=> 3

上記 2 行目の内側の関数を先に置き換えたら、どうなるのかな?

leafCount_cps (Branch (Branch (Leaf 1) (Leaf 2)) (Leaf 3)) (\x -> x)
=> leafCount_cps (Branch (Leaf 1) (Leaf 2)) $ \x -> leafCount_cps (Leaf 3) $ \y -> (\x -> x) $ x + y
=> leafCount_cps (Branch (Leaf 1) (Leaf 2)) $ \x -> (\y -> (\x -> x) $ x + y) 1
=> leafCount_cps (Branch (Leaf 1) (Leaf 2)) $ \x -> (\x -> x) $ x + 1
=> leafCount_cps (Leaf 1) $ \x -> leafCount_cps (Leaf 2) $ \y -> (x -> (\x -> x) $ x + 1) x + y
=> (\x -> leafCount_cps (Leaf 2) $ \y -> (x -> (\x -> x) $ x + 1) x + y) 1
=> leafCount_cps (Leaf 2) $ \y -> (x -> (\x -> x) $ x + 1) 1 + y
=> (\y -> (x -> (\x -> x) $ x + 1) 1 + y) 1
=> (x -> (\x -> x) $ x + 1) 1 + 1
=> (\x -> x) $ 2 + 1
=> 3

うーん、頭の中が爆発しそう。。 (+_+)  ともかく、クロージャがすごく伸びたり縮んだりしている様子はわかった。

 

5. フィボナッチ数

フィボナッチ数列とは、1,1,2,3,5,8・・・と続く数列で、隣り合う2つの数を足し算すると、その上位の数になる数列を言います。

1+2=3 2+3=5 3+5=8 5+8=13 8+13=21・・・と、永遠に続いて行きます。

(株・個人投資家の喫茶店 より)

定義は、フィボナッチ数 - Wikipedia よると、

F_0 = 0\,

F_1 = 1 \,

F_{n+2} = F_n + F_{n+1} \quad (n \ge 0)

これをそのまま書くと、

fib n | n == 0    = 0
      | n == 1    = 1
      | otherwise =  fib (n-1) + fib (n-2)

n が 0 と 1 の場合をまとめ、継続渡しスタイルに変更。

fib_cps n k | n <= 1    = k n
            | otherwise = fib_cps (n-1) $ \x ->
                          fib_cps (n-2) $ \y ->
                          k $ x + y

( cf. c - What is a trampoline function? - Stack Overflow )

( gist: 956745 — Gist )

 

6. リストの平坦化

M.Hiroi's Home Page / お気楽 OCaml プログラミング入門」 の「CPS の便利な使い方」によると、

階乗やフィボナッチ関数の場合、CPS に変換するメリットはほとんどありませんが、場合によっては CPS に変換した方が簡単にプログラムできることもあります。たとえば、リストを平坦化する関数 flatten で、リストの要素に空リストが含まれていたら空リストを返すようにプログラムを修正することを考えてみましょう。

まずは、リストを平坦化する関数を継続渡しスタイルで書く。

concat_cps []       k = k []
concat_cps (xs:xss) k = concat_cps xss $ \xss' -> k $ xs ++ xss' 

「空リストが含まれていたら空リストを返す」ように変更したい場合、次の一行を追加するだけで良い。

concat_cps ([]:_)   k = []

( gist: 954835 — Gist)

print $ concat_cps [[1,2],[3,4],[5,6]] id   -- [1,2,3,4,5,6]
print $ concat_cps [[1,2],[3,4],[],[5,6]] id -- []

ところで、ライブラリに用意されている関数を利用し、同じ機能の関数を定義するなら、

flatten xss = if elem [] xss then [] else concat xss

しかし、型を比較すると、

flatten :: (Eq a) => [[a]] -> [a]
(`concat_cps` id) :: [[a]] -> [a]

flatten 関数はリストの要素間で比較できないと利用できない。例えば、リストの要素が関数である場合はだめ。

print $ length $ flatten [[odd, even], [flip elem [0..10]]]

これに対して、concat_cps では計算が行われる。

print $ length $ concat_cps [[odd, even], [flip elem [0..10]]] id  -- 3

ただし、末尾再帰で書くなら問題ない。

flatten1 xss = flatten' [] xss
    where
      flatten' acc []       = acc
      flatten' acc ([]:_)   = []
      flatten' acc (xs:xss) = flatten' (acc++xs) xss

こちらも要素に空リストがあった場合、すぐに脱出している。

上記のような形の関数になった場合、大抵 fold 系の関数で置き換えることがきるので、試しに書いてみる。

要素が [] の存否を表わす Bool 型をフラグとして用いるなら、

flatten2 = fst. foldr f ([], False)
    where
      f _  (_,  True)  = ([], True)
      f [] (_,  False) = ([], True)
      f xs (xy, False) = (xs ++ xy, False)

しかし、この場合、foldr を使い末尾から先頭へと一巡しているので、先ほどのように途中で計算を抜けているわけではない。

 

7. foldr (畳み込み関数)

では、foldr を継続渡しスタイルしたら、リストの要素を判定することにより、途中で計算を抜けることができるだろうか?

まずは、foldr の定義から確認。

foldr' _ z []     = z
foldr' f z (x:xs) = x `f` foldr' f z xs

 

a. 継続渡しスタイル

foldr を継続渡しスタイルにしてみる。名前を foldr2 とする。

まずは、リストの要素が空の場合は、第 2 引数に与えたに対して関数 k を適用する。

foldr2 _ z []     k = k z

リストの要素がある場合、リストの先頭要素以外のリストに対して、foldr2 を適用し、その結果を無名関数の引数に渡す準備。

foldr2 f z (x:xs) k = foldr2 f z xs $ \y –> … 

上記結果とリストの先頭要素に関数 f を適用し、最後に関数 k を適用する。

foldr2 f z (x:xs) k = foldr2 f z xs $ \y -> k $ f x y 

これを使って、1 から 10 までのリストの要素を足し合わせる。

print $ foldr2 (+) 0 [1..10] id   -- 55

上記結果を 2 倍したいなら、

print $ foldr2 (+) 0 [1..10] (* 2)  -- 110

( gist: 955325 — Gist )

 

b. 要素の値により、値を渡す先を変更

ここで先ほどの flatten 関数の仕様、

リストの要素に空リストが含まれていたら空リストを返す

というように変更。

要素を見て、空リストを返せばいいので、

foldr2' _ z [] k = k z
foldr2' f z (x:xs) k = foldr2' f z xs $ \y ->
                       if x == [] then [] else k $ f x y 

これを使うと、

print $ foldr2' (++) [] [[1,2],[3,4],[5,6]] id    -- [1,2,3,4,5,6]
print $ foldr2' (++) [] [[1,2],[3,4],[],[5,6]] id -- []

ただし、この定義では関数の型を確認すると、第 3 引数が「リストのリスト」に固定されてしまっている。

foldr2'
  :: (Eq a) =>
     ([a] -> a2 -> a2) -> a2 -> [[a]] -> (a2 -> [a1]) -> [a1]

( gist: 955345 — Gist )

上記を元により一般的な形にする。

  1. 条件は述語 p を与えるようにし、
  2. 述語が真であるとき、その要素の値に対して適用する関数を k’ とする。
foldr2_cps _ z []     p k _ = k z
foldr2_cps f z (x:xs) p k k' 
    | p x       = k' x
    | otherwise = foldr2_cps f z xs p (\y -> k $ f x y) k'

今度は型が以下のようになった。

foldr2_cps
  :: (t -> a -> a)
     -> a
     -> [t]
     -> (t -> Bool)
     -> (a -> b)
     -> (t -> b)
     -> b
使ってみると、適用対象が「リストのリスト」に限定されてないのが確認できる。
print $ foldr2_cps (++) [] [[1,2],[3,4],[5,6]] 
          (== []) id id                           -- [1,2,3,4,5,6]
print $ foldr2_cps (++) [] [[1,2],[3,4],[],[5,6]] 
          (== []) id id                           -- []
print $ foldr2_cps (+) 0 [1..10] (== 0) id id     -- 55
print $ foldr2_cps (+) 0 [1..10] (== 5) id id     -- 5

( gist: 955465 — Gist )

 

c. 「引数の関数」を継続渡しスタイルにする場合

ついでなので、「Haskell/Continuation passing style - Wikibooks」 の thrice 関数の例に書かれていたように、引数の関数を継続渡しスタイルにしてみる。

foldr で関数を引数に与えているのは第 1 引数。引数を 2 つ受け取り、何らかの計算をした結果を返す。

foldr' _ z []     = z
foldr' f z (x:xs) = x `f` foldr' f z xs

第1引数の名前を f_cps に変更し、「引数 2 つ受け取り、何らかの計算をした結果」に対して関数を適用するように変更してみる。

CropperCapture[170]

foldr3 _     z []     k = k z
foldr3 f_cps z (x:xs) k = f_cps x (foldr3 f_cps z xs k) $ \y -> k y

使ってみる。

print $ foldr3 (\x y k -> k $ x + y) 0 [1..3] id    -- 6
print $ foldr3 (\x y k -> k $ x + y) 0 [1..3] (* 2) -– 34

( gist: 956504 — Gist )

計算の過程は、下図の通り。

CropperCapture[171]

ところで、先ほど定義した foldr2 と foldr3 を並べて比較。

foldr2 _ z []     k = k z
foldr2 f z (x:xs) k = foldr2 f z xs $ \y -> k $ f x y 
foldr3 _     z []      k = k z
foldr3 f_cps z (x:xs ) k = f_cps x (foldr3 f_cps z xs k) $ \y -> k y

共に空リストに対する定義が

k z

となっている。

しかし、foldr2 が末尾再帰になっているのに対して、 foldr3 はそうではない。この定義の違いにより、動作が異なる。つまり、foldr2 が計算の過程で再帰呼び出しにより、そっくり定義が置き換えられていくのに対し、foldr3 では関数の呼び出しが積み重なっていく。

foldr2_cps の定義に類似した foldr3_csp を考える。

foldr3' _     z [] p      k _ = k z
foldr3' f_cps z (x:xs)  p k k'
    | p x = k' x
    | otherwise = f_cps x (foldr3' f_cps z xs p k k') $ \y -> k y

一見同じように見えるけれど、

print $ foldr3' (\x y k -> k $ x ++ y) []
          [[1,2],[3,4],[5,6]] (== []) id id     -- [1,2,3,4,5,6]
print $ foldr3' (\x y k -> k $ x ++ y) []
          [[1,2],[3,4],[],[5,6]] (== []) id id  -- [1,2,3,4]

foldr3_cps の方は、述語 p で指定した条件に合った場合、脱出時に一気に抜けるのではなく、それまでに生成したクロージャの計算がなされ、その値が返される。

( gist: 956626 — Gist )

これで、CPS の感覚が大分つかめるようになったかな。

Haskell で継続渡しスタイル (CPS) につづく