Goto in Haskell: Кто-нибудь может объяснить этот, казалось бы, безумный эффект от использования монады продолжения? - PullRequest
37 голосов
/ 04 марта 2011

Из этой темы (Control.Monad.Cont fun, 2005) Томаш Зелонка представил функцию (прокомментированную в ясной и приятной форме Томасом Йегером).Томаш берет аргумент (функцию) тела callCC и возвращает его для последующего использования со следующими двумя определениями:

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 .Используя их, вы можете напомнить семантику goto в haskell, которая выглядит действительно круто:

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.

Здесь возникает интересный момент.Я использовал это вместе с Writer Monad для решения определенной проблемы.Мой код выглядит следующим образом:

{-# 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

Числа от нуля до трех проглатываются где-то в глубокой темнотеэтот пример.

Теперь в «реальном мире Хаскелла» состояния О'Салливана, Гоерцена и Стюарта

"Преобразование в монадные стекирования аналогично составлению функций. Если мы изменим порядок вмы применяем функции, а затем получаем разные результаты, мы не удивимся. Так же и с монадными трансформаторами. " (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 (именно поэтому я недавно спросил thisвопрос .)

Мы добавляем экземпляр, оставляя listen и pass неопределенным:

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

Добавьте эти строки, скомпилируйте и запустите.Все числа напечатаны.

Что произошло в предыдущем примере?

Ответы [ 2 ]

34 голосов
/ 05 марта 2011

Вот несколько неформальный ответ, но, надеюсь, полезный.getCC' возвращает продолжение к текущей точке исполнения;Вы можете думать об этом как о сохранении фрейма стека.Продолжение, возвращаемое getCC', имеет не только состояние ContT в точке вызова, но также состояние любой монады выше ContT в стеке.Когда вы восстанавливаете это состояние, вызывая продолжение, все монады, построенные выше ContT, возвращаются в свое состояние в точке вызова getCC'.

В первом примере вы используете type APP= WriterT [String] (ContT () IO), сIO в качестве базовой монады, затем ContT и, наконец, WriterT.Поэтому, когда вы вызываете loop, состояние записывающего устройства восстанавливается до того, которое было при вызове getCC', потому что записывающее устройство находится выше ContT в стеке монад.Когда вы переключаете ContT и WriterT, теперь продолжение раскручивает только монаду ContT, потому что она выше, чем пишущая.

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..."

Несмотря на то, что монаде писателя сообщают значения, все они отбрасываются при запуске внутренней монады 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 и ContT заключается в том, что тип ErrorT дает понять, что любые частичные вычисления будут отброшены, если выдается ошибка.

Определенно проще рассуждать о ContTкогда он находится на вершине стека, но иногда полезно иметь возможность развернуть монаду до известной точки.Тип транзакции может быть реализован таким образом, например.

11 голосов
/ 05 марта 2011

Я провел некоторое время, отслеживая это в исчислении λ.Он генерировал страницы и страницы дериваций, которые я не буду пытаться воспроизвести здесь, но я немного понял, как работает стек монад.Ваш тип расширяется следующим образом:

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

Вы также можете расширить * Writer return, >>= и tell, наряду с Cont's return, >>= и callCC.Отслеживать его крайне утомительно.

Эффект вызова loop в драйвере состоит в том, чтобы отказаться от нормального продолжения и вместо этого снова вернуться из вызова к getCC'.Это заброшенное продолжение содержало код, который добавил бы текущий x в список.Таким образом, вместо этого мы повторяем цикл, но теперь x является следующим числом, и только когда мы нажмем последнее число (и, таким образом, не оставляем продолжение), мы составим список из ["The quick brown fox"] и ["4"].

Точно так же, как в «Real World Haskell» подчеркивается, что монада ввода-вывода должна оставаться в нижней части стека, также важно, чтобы монада продолжения оставалась сверху.

...