Goto en Haskell: ¿Puede alguien explicar este efecto aparentemente loco del uso de la mónada de continuación?


Desde este hilo (Control.Mónada.Cont fun, 2005), Tomasz Zielonka introdujo una función (comentada de una manera clara y agradable por Thomas Jäger). Tomasz toma el argumento (una función) de un cuerpo callCC y lo devuelve para su uso posterior con las siguientes dos definiciones:

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))

También se mencionan en Haskellwiki. Usándolos, puede parecerse a la semántica de goto en haskell, que se ve muy bien:

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

Esto imprime los números 0 a 10.

Aquí viene el punto interesante. Usé esto junto con el escritor Mónada para resolver un cierto problema. Mi código se ve como el siguiente:

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

Cuando compila y ejecuta este código, la salida es:

The quick brown fox ...
4

Los números del cero al tres son tragados en algún lugar en la profunda oscuridad de este ejemplo.

Ahora, en "Real World Haskell" O'Sullivan, Goerzen y Stewart estados

"El apilamiento de transformadores de mónada es análogo a las funciones de composición. Si cambiamos el orden en el que aplicamos funciones y luego obtenemos diferentes resultados, no nos sorprenderemos. Lo mismo ocurre con los transformadores de mónada." (Mundo Real Haskell, 2008, P. 442)

Se me ocurrió la idea de intercambiar los transformadores de arriba:

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

Sin embargo, esto no se compilará porque no hay una definición de instancia para MonadWriter en Control.Mónada.Cont (que es por lo que hace poco hice esta pregunta .)

Agregamos una instancia dejando escuchar y pasar undefined:

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

Agrega esas líneas, compila y ejecuta. Todos los números están impresos.

¿Qué ha pasado en el ejemplo anterior?

Author: Community, 2011-03-04

2 answers

Aquí hay una respuesta algo informal, pero espero que sea útil. getCC' devuelve una continuación al punto actual de ejecución; se puede pensar en ello como guardar un marco de pila. La continuación devuelta por getCC' no solo tiene el estado de ContT en el punto de la llamada, sino también el estado de cualquier mónada por encima de ContT en la pila. Al restaurar ese estado llamando a la continuación, todas las mónadas construido sobre ContT devolver a su estado en el punto de la getCC' llamar.

En la primera ejemplo se usa type APP= WriterT [String] (ContT () IO), con IO como mónada base, luego ContT, y finalmente WriterT. Así que cuando llamas loop, el estado del escritor se desenrolla a lo que era en la llamada getCC' porque el escritor está por encima de ContT en la pila de mónadas. Cuando cambias ContT y WriterT, ahora la continuación solo desenrolla la mónada ContT porque es más alta que el escritor.

ContT no es el único transformador de mónada que puede causar problemas como este. Este es un ejemplo de una situación similar con 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..."

A pesar de que a la mónada escritora se le estaban diciendo valores, todos son descartados cuando se ejecuta la mónada interior ErrorT. Pero si cambiamos el orden de los transformadores:

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"])

Aquí se conserva el estado interno de la mónada writer, porque es inferior a ErrorT en la pila de mónadas. La gran diferencia entre ErrorT y ContT es que el tipo de ErrorT deja claro que cualquier cálculo parcial será descartado si se lanza un error.

Es definitivamente es más fácil razonar sobre ContT cuando está en la parte superior de la pila, pero en ocasiones es útil poder desenrollar una mónada a un punto conocido. Un tipo de transacción podría llevarse a cabo de esta manera, por ejemplo.

 33
Author: John L,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-03-05 11:46:22

Pasé algún tiempo rastreando esto en el cálculo λ. Generó páginas y páginas de derivaciones que no intentaré reproducir aquí, pero obtuve un poco de información sobre cómo funciona la pila de mónadas. Su tipo se expande de la siguiente manera:

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

De manera similar, puede expandir el return, >>=, y tell, junto con Cont return, >>=, y callCC. Sin embargo, rastrearlo es extremadamente tedioso.

El efecto de llamar a loop en el controlador es que abandona la normalidad continuación y en su lugar volver, de nuevo, de la llamada a getCC'. Esa continuación abandonada contenía el código que habría agregado el x actual a la lista. Por lo tanto, repetimos el bucle, pero ahora x es el siguiente número, y solo cuando pulsamos el último número (y por lo tanto no abandone la continuación) reconstruimos la lista de ["The quick brown fox"] y ["4"].

Así como "Real World Haskell" enfatiza que la mónada IO debe permanecer en la parte inferior de la pila, también parece importante que la mónada de continuación se mantenga en la cima.

 10
Author: chrisleague,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-03-04 23:55:17