Twitterのどこかで「Contモナドを使わない継続の説明を書いてくれ」みたいな話を目にしたので書きました.
この記事は,以下のような方を対象に書かれています.
- Haskellはなんとなく使える.モナドもなんとなくわかる.
- 息をするように新しいモナドを定義したりはできない.
- 継続が何かは全く分からない.Schemeとかを使ったこともない.
継続とは
Haskell(に限らずほとんどの言語)では,以下のようなステップでプログラムを実行していきます.
- 何らかの関数に引数を与えて,その計算結果を受け取る.
- 受け取った計算結果を別な関数の引数に与える.
- 以下繰り返し.
例えばHaskellで,与えられた3つの数の平均を取る関数を(非常に冗長に)書いてみると,以下のようになるでしょう.
module Cont where
import Prelude hiding (div)
add :: Num a => a -> a -> a
add x y = x + y
div :: Fractional a => a -> a -> a
div x y = x / y
average3 :: Fractional a => a -> a -> a -> a
average3 x y z =
let xPlusY = add x y
xPlusYPlusZ = add xPlusY z
average = div xPlusYPlusZ 3
in average
これを実行すると,以下のようになります.
*Cont> :l Cont
[1 of 1] Compiling Cont ( Cont.hs, interpreted )
Ok, modules loaded: Cont.
*Cont> average3 5 4 3
4.0
このような通常の書き方を「直接スタイル」といいます.
直接スタイルでは,最初に説明したとおり「何らかの関数に引数を与えて,その計算結果を受け取る」のが基本の操作になります.
これに対して,関数が「その関数自体の実行が終わった後の残りの処理」を受け取り,それに明示的に計算結果を渡していくような書き方を「継続渡しスタイル」といい,「関数の実行が終わった後の残りの処理」のことを継続といいます.
継続渡しスタイルの例
以上のような説明だけではよくわからないかもしれませんが,実は皆さんもどこかで使ったことがあるかもしれません.例えば,JQueryの$.get
では,
$.get(
'/path/to/some/resources',
{},
(data) => {
// GETリクエストが成功したときの処理.
// data にレスポンスが入る.
});
というように,$.get
の第3引数に「リクエストが終わった後の処理」を渡します.この方法が,継続渡しスタイルに他なりません.
継続渡しスタイルの書き方
それでは,先ほど作ったaverage3
を,継続渡しに書き換えてみましょう.以降では,関数hoge
の継続渡しバージョンをhogeCPS
という名前で定義します.
Haskellにおいて,「関数の実行が終わった後の残りの処理」は,それ自体一つの関数で表すことができます.
まずは例として,add
を継続渡しスタイルで書いてみましょう.
module Cont where
...
addCPS :: Num a => a -> a -> (a -> result) -> result
addCPS x y cont = cont (x + y)
この定義では,cont
が残りの処理全体になります.cont
は,add
の結果であるx
とy
の和を受け取って,何らかの型result
の値を返す関数になるでしょう.
このaddCPS
を実際にインタプリタ上で使ってみましょう.
*Cont> add 1 2
3
*Cont> addCPS 1 2
<interactive>:29:1: error:
• No instance for (Show ((a0 -> result0) -> result0))
arising from a use of ‘print’
(maybe you haven't applied a function to enough arguments?)
• In a stmt of an interactive GHCi command: print it
このように,addCPS 1 2
自体は,残りの処理全体を受け取ってそれに1 + 2
を渡す関数となります.この「残りの処理全体」はどんなものでも大丈夫なので,試しに計算結果を文字列に変換する継続を与えてみましょう.
*Cont> addCPS 1 2 $ \sum -> show sum
"3"
$
以降では,1 + 2
の結果を受け取って,それを文字列に変換しています.このように,計算結果を直接受け取るのではなく,残りの処理を関数に渡してあげるのが継続渡しスタイルでした.
これで雰囲気はわかったと思うので,同様にdiv
も継続渡しスタイルに変換していきます.
module Cont where
...
divCPS :: Fractional a => a -> a -> (a -> result) -> result
divCPS x y cont = cont (x / y)
こちらも試しに適当な継続を与えて実行してみましょう.
-- 結果を5回繰り返す継続
*Cont> divCPS 4 2 $ \d -> [d, d, d, d, d]
[2.0,2.0,2.0,2.0,2.0]
最後に,average3CPS
をaddCPS
とdivCPS
を使って書いていきますが,その前にaverage3
の定義を見直してみましょう.
average3 :: Fractional a => a -> a -> a -> a
average3 x y z =
let xPlusY = add x y
xPlusYPlusZ = add xPlusY z
average = div xPlusYPlusZ 3
in average
average3
とその周辺では,以下のような処理を行っています.
add x y
の結果をxPlusY
に束縛し,add xPlusY z
の結果をxPlusYPlusZ
に束縛し,div xPlusYPlusZ 3
の結果をaverage
に束縛し,average
を返し,average3
が返した値を使って,呼び出し側が何かする
順を追って見ていくと,1の実行時点での継続は2, 3, 4, 5となり,2の実行時点での継続は3, 4, 5となり,3の実行時点での継続は4, 5となり,4の実行時点での継続は5となることがわかります.
したがって,1の実行時点での継続を考えると,average3CPS
は以下のような形をしているはずです.
average3CPS x y z cont =
addCPS x y $ \xPlusY ->
...
この「…」の部分には,2, 3, 4, 5の処理が入るはずです.順を追って見ていきましょう.
average3CPS :: Fractional a => a -> a -> (a -> result) -> result
average3CPS x y z cont =
addCPS x y $ \xPlusY ->
addCPS xPlusY z $ \xPlusYPlusZ ->
...
2の処理までを実装した段階です.この時点で「…」には,3, 4, 5の処理が入ります.すなわち,3の部分までの処理を書くと,以下のような形になるはずです.
average3CPS x y z cont =
addCPS x y $ \xPlusY ->
addCPS xPlusY z $ \xPlusYPlusZ ->
divCPS xPlusYPlusZ 3 $ \average ->
...
これでx, y, z
の平均を受け取るところまでは実装できました.最後にやらなければならないことは,もちろん残りの継続cont
に計算結果を渡してやることです.
というわけで,以下がaverage3CPS
の最終的な実装となります.
module Cont where
...
average3CPS :: Fractional a => a -> a -> a -> (a -> result) -> result
average3CPS x y z cont =
addCPS x y $ \xPlusY ->
addCPS xPlusY z $ \xPlusYPlusZ ->
divCPS xPlusYPlusZ 3 $ \average ->
cont average
実際に実行して,average3CPS
が正しく動くことを確認してみましょう.
Prelude> :l Cont
[1 of 1] Compiling Cont ( Cont.hs, interpreted )
Ok, modules loaded: Cont.
*Cont> average3CPS 3 5 7 $ \avg -> show avg
"5.0"
*Cont> average3CPS 3 5 7 $ \avg -> avg -- もちろん,「何もしない」という継続を与えてもOK
5.0
おめでとうございます.これで無事,average3CPS
を実装することができました.これさえ理解できれば,もっと複雑な継続を使った処理も順を追って理解できるはずです.
継続のメリット
継続渡しスタイルを使うことのメリットに,「残りの処理の実行を関数側が制御できる」ことがあります.関数から帰った後の「残りの処理」はあくまでもその関数自体の引数なので,呼ぶも呼ばないも自由ですし,呼んだ後に適当に結果に細工をすることだってできてしまいます.
例えば,ゼロ除算の場合は残りの処理を実行せずに,エラーメッセージを返すような関数safeDivCPS
を以下のように定義することができます.
module Cont where
...
type ErrorMsg = String
safeDivCPS :: (Eq a, Fractional a) =>
a -> a -> (a -> Either ErrorMsg result) -> Either ErrorMsg result
safeDivCPS _ 0 _ = Left "division by zero"
safeDivCPS x y cont = cont (x / y)
都合上継続がresult
ではなくEither ErrorMsg result
になってしまっていますが,とりあえず今は考えないことにしましょう.(まともに説明しようとするとContT
モナド変換子が出てきますが,今回の趣旨から外れるため割愛)
これに適当な継続を与えて,実行してみましょう.継続がEither ErrorMsg result
の型を返さなければならないのに注意です.
*Cont> :l Cont
[1 of 1] Compiling Cont ( Cont.hs, interpreted )
Ok, modules loaded: Cont.
*Cont> safeDivCPS 3 2 $ \d -> Right ("the answer is " ++ show d)
Right "the answer is 1.5"
*Cont> safeDivCPS 3 0 $ \d -> Right ("the answer is " ++ show d)
Left "division by zero"
ゼロ除算の場合は残りの処理を実行せず,直ちにLeft "division by zero"
を返すようになりました.
このように,関数の実行後の振る舞いも含めて制御できてしまうのが,継続のメリットになります.これを利用することで,例えば以下の記事のように,Haskellでループからbreakするといったこともできてしまいます.
今回はモナドを使わない,継続渡しスタイルそのものの説明でした.長くなったので続きは以下に分割します.