36

このスレッド(Control.Monad.Cont fun、2005)から、Tomasz Zielonkaが関数を導入しました(ThomasJägerによって明確で素晴らしい方法でコメントされています)。Tomaszは、callCC本体の引数(関数)を受け取り、次の2つの定義を使用して後で使用できるように返します。

import Control.Monad.Cont
...
getCC :: MonadCont m => m (m a)
getCC = callCC (\c -> let x = c x in return x)

getCC' :: MonadCont m => a -> m (a, a -> m b)
getCC' x0 = callCC (\c -> let f x = c (x, f) in return (x0, f))

それらはHaskellwikiでも言及されています。それらを使用すると、haskellのgotoセマンティクスに似たものになります。これは本当にクールに見えます。

import Control.Monad.Cont

getCC' :: MonadCont m => a -> m (a, a -> m b)
getCC' x0 = callCC (\c -> let f x = c (x, f) in return (x0, f))

main :: IO ()
main = (`runContT` return) $ do
    (x, loopBack) <- getCC' 0
    lift (print x)
    when (x < 10) (loopBack (x + 1))
    lift (putStrLn "finish")

これにより、0〜10の数値が出力されます。

ここに興味深い点があります。これをライターモナドと一緒に使って、ある問題を解決しました。私のコードは次のようになります。

{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, UndecidableInstances #-}

import Control.Monad.Cont
import Control.Monad.Writer

getCC :: MonadCont m => m (m a)
getCC = callCC (\c -> let x = c x in return x)

getCC' :: MonadCont m => a -> m (a, a -> m b)
getCC' x0 = callCC (\c -> let f x = c (x, f) in return (x0, f))

-- a simple monad transformer stack involving MonadCont and MonadWriter
type APP= WriterT [String] (ContT () IO)

runAPP :: APP a -> IO ()
runAPP a= runContT (runWriterT a) process
      where process (_,w)= do
               putStrLn $ unlines w
               return ()

driver :: Int -> APP ()
driver k = do
   tell [ "The quick brown fox ..." ]
   (x,loop) <- getCC' 0
   collect x
   when (x<k) $ loop (x+1) 

collect :: Int -> APP ()
collect n= tell [ (show n) ] 

main :: IO ()
main = do
   runAPP $ driver 4

このコードをコンパイルして実行すると、出力は次のようになります。

The quick brown fox ...
4

この例の深遠な暗闇のどこかで、0から3までの数字が飲み込まれています。

現在、「Real World Haskell」では、オサリバン、ゲルゼン、スチュワートの各州が

「モナド変換子の積み重ねは、関数の作成に似ています。関数を適用する順序を変更して、異なる結果が得られても、驚くことはありません。したがって、モナド変換子も同様です。」(Real World Haskell、2008、P.442)

上記のトランスを交換するというアイデアを思いつきました。

--replace in the above example
type APP= ContT () (WriterT [String] IO)
...
runAPP a = do
    (_,w) <- runWriterT $ runContT a (return . const ())
    putStrLn $ unlines w

ただし、Control.Monad.ContにMonadWriterのインスタンス定義がないため、これはコンパイルされません(これが、最近この質問をした理由です)。

リッスンとパスを未定義のままにしてインスタンスを追加します。

instance (MonadWriter w m) => MonadWriter w (ContT r m) where
  tell = lift . tell
  listen = undefined
  pass = undefined

それらの行を追加し、コンパイルして実行します。すべての数字が印刷されます。

前の例で何が起こったのですか?

4

2 に答える 2

33

これはやや非公式な回答ですが、うまくいけば便利です。 getCC'現在の実行ポイントへの継続を返します。スタック フレームの保存と考えることができます。によって返される継続には、呼び出し時点での の状態getCC'だけでなく、スタック上の任意のモナドの状態も含まれます。継続を呼び出してその状態を復元すると、上記で構築されたすべてのモナドが呼び出し時点の状態に戻ります。ContTContTContTgetCC'

最初の例では、基本モナドとしてtype APP= WriterT [String] (ContT () IO)with 、 then 、および finallyを使用しています。したがって、 を呼び出すと、ライターはモナドスタックの上にあるため、ライターの状態は呼び出し時の状態に巻き戻されます。と を切り替えると、ライターよりも高いため、継続はモナドを巻き戻すだけになりました。IOContTWriterTloopgetCC'ContTContTWriterTContT

ContTこのような問題を引き起こす可能性がある唯一のモナド変換子ではありません。同様の状況の例を次に示しますErrorT

func :: Int -> WriterT [String] (ErrorT String IO) Int
func x = do
  liftIO $ print "start loop"
  tell [show x]
  if x < 4 then func (x+1)
    else throwError "aborted..."

*Main> runErrorT $ runWriterT $ func 0
"start loop"
"start loop"
"start loop"
"start loop"
"start loop"
Left "aborted..."

writer モナドに値が伝えられていたとしても、内部ErrorTモナドが実行されるとそれらはすべて破棄されます。しかし、トランスフォーマーの順序を入れ替えると:

switch :: Int -> ErrorT String (WriterT [String] IO) () 
switch x = do
  liftIO $ print "start loop"
  tell [show x]
  if x < 4 then switch (x+1)
    else throwError "aborted..."

*Main> runWriterT $ runErrorT $ switch 0
"start loop"
"start loop"
"start loop"
"start loop"
"start loop"
(Left "aborted...",["0","1","2","3","4"])

ErrorTここでは、ライター モナドの内部状態が保持されます。これは、モナド スタックよりも低いためです。との大きな違いは、ErrorTの型により、エラーがスローされた場合に部分的な計算が破棄されることが明確になることです。ContTErrorT

スタックの一番上にある場合を判断するのは間違いなく簡単ContTですが、モナドを既知のポイントまで巻き戻すことができると便利な場合があります。たとえば、あるタイプのトランザクションは、この方法で実装できます。

于 2011-03-05T11:46:22.880 に答える
11

これを λ 計算でトレースするのに時間を費やしました。ここでは再現しようとはしませんが、モナドスタックがどのように機能するかについて少し洞察を得ることができました。タイプは次のように展開されます。

type APP a = WriterT [String] (ContT () IO) a
           = ContT () IO (a,[String])
           = ((a,[String]) -> IO()) -> IO()

同様に、Writer のreturn>>=、および を、Cont の、、およびtellとともに展開できます。ただし、それをトレースするのは非常に面倒です。return>>=callCC

loopドライバを呼び出すと、通常の継続が中止され、代わりに呼び出しから に戻りgetCC'ます。xその放棄された継続には、リストに current を追加するコードが含まれていました。その代わりに、ループを繰り返しますが、今度xは次の番号であり、最後の番号に到達したとき (したがって継続を放棄しない["The quick brown fox"]とき) にのみ、とからリストをつなぎ合わせ["4"]ます。

「Real World Haskell」が IO モナドがスタックの一番下にとどまる必要があることを強調しているように、継続モナドが一番上にとどまることが重要であるようにも思われます。

于 2011-03-04T23:55:17.690 に答える