haskell - La mónada Pausa
monads coroutine (6)
Las mónadas pueden hacer muchas cosas increíbles y locas. Pueden crear variables que contienen una superposición de valores. Pueden permitirle acceder a los datos del futuro antes de calcularlos. Te pueden permitir escribir actualizaciones destructivas, pero no realmente. ¡Y luego la mónada de continuación te permite romper las mentes de las personas! Ususalmente tuyo. ;-)
Pero aquí hay un desafío: ¿puedes hacer una mónada que se pueda pausar ?
data Pause s x instance Monad (Pause s) mutate :: (s -> s) -> Pause s () yield :: Pause s () step :: s -> Pause s () -> (s, Maybe (Pause s ()))
La mónada Pause
es un tipo de mónada de estado (por lo tanto, mutate
, con la semántica obvia). Normalmente, una mónada como esta tiene algún tipo de función "ejecutar", que ejecuta el cálculo y te devuelve el estado final. Pero Pause
es diferente: proporciona una función de step
, que ejecuta el cálculo hasta que llama a la función de yield
mágico. Aquí el cálculo se pausa, volviendo a la persona que llama la información suficiente para reanudar el cálculo más tarde.
Para mayor asombro: permita que la persona que llama modifique el estado entre llamadas de step
. (Las firmas tipo arriba deberían permitir esto, por ejemplo).
Caso de uso: a menudo es fácil escribir código que hace algo complejo, pero un PITA total para transformarlo también muestra los estados intermedios en su operación. Si desea que el usuario pueda cambiar algo a mitad de la ejecución, las cosas se vuelven complejas muy rápido.
Ideas de implementación:
Obviamente se puede hacer con hilos, bloqueos e
IO
. ¿Pero podemos hacerlo mejor? ;-)¿Algo loco con una mónada de continuación?
Tal vez algún tipo de mónada de escritor, donde el
yield
simplemente registra el estado actual, y luego podemos "pretender" que lo pase iterando sobre los estados en el registro. (Obviamente esto impide modificar el estado entre pasos, ya que en realidad no estamos "pausando" nada ahora).
Así es como lo haría, usando mónadas libres . Er, um, ¿qué son? Son árboles con acciones en los nodos y valores en las hojas, con >>=
actuando como injertos de árboles.
data f :^* x
= Ret x
| Do (f (f :^* x))
No es inusual escribir F * X para tal cosa en las matemáticas, de ahí mi nombre de tipo infijo malhumorado. Para hacer una instancia, solo necesitas que f
sea algo sobre lo que puedas mapear: cualquier Functor
.
instance Functor f => Monad ((:^*) f) where
return = Ret
Ret x >>= k = k x
Do ffx >>= k = Do (fmap (>>= k) ffx)
Eso es solo "aplicar k
en todas las hojas e injertar en los árboles resultantes". Estos árboles pueden representar estrategias de computación interactiva: el árbol completo cubre todas las interacciones posibles con el entorno, y el entorno elige qué camino seguir en el árbol. ¿Por qué son libres ? Son solo árboles, sin una teoría ecuacional interesante sobre ellos, que dicen qué estrategias son equivalentes a qué otras estrategias.
Ahora tengamos un kit para hacer Functors que corresponda a un conjunto de comandos que podríamos querer hacer. Esta cosa
data (:>>:) s t x = s :? (t -> x)
instance Functor (s :>>: t) where
fmap k (s :? f) = s :? (k . f)
captura la idea de obtener un valor en x
después de un comando con tipo de entrada s
y tipo de salida t
. Para hacer eso, debe elegir una entrada en s
explicar cómo continuar con el valor en x
dado el resultado del comando en t
. Para asignar una función a través de tal cosa, vuélvala a la continuación. Hasta ahora, equipo estándar. Para nuestro problema, ahora podemos definir dos funtores:
type Modify s = (s -> s) :>>: ()
type Yield = () :>>: ()
¡Es como si acabara de escribir los tipos de valores para los comandos que queremos poder hacer!
Ahora asegurémonos de que podemos ofrecer una opción entre esos comandos. Podemos demostrar que una elección entre funtores produce un functor. Equipamiento más estándar.
data (:+:) f g x = L (f x) | R (g x)
instance (Functor f, Functor g) => Functor (f :+: g) where
fmap k (L fx) = L (fmap k fx)
fmap k (R gx) = R (fmap k gx)
Por lo tanto, Modify s :+: Yield
representa la elección entre modificar y ceder. Cualquier firma de comandos simples (comunicándose con el mundo en términos de valores en lugar de manipular los cálculos) se puede convertir en un functor de esta manera. ¡Es una molestia tener que hacerlo a mano!
Eso me da tu mónada: la mónada libre sobre la firma de modificar y ceder.
type Pause s = (:^*) (Modify s :+: Yield)
Puedo definir los comandos modificar y ceder como one-do-then-return. Además de negociar la entrada ficticia para el yield
, eso es simplemente mecánico.
mutate :: (s -> s) -> Pause s ()
mutate f = Do (L (f :? Ret))
yield :: Pause s ()
yield = Do (R (() :? Ret))
La función de step
le da un significado a los árboles de estrategia. Es un operador de control , construyendo un cálculo (tal vez) de otro.
step :: s -> Pause s () -> (s, Maybe (Pause s ()))
step s (Ret ()) = (s, Nothing)
step s (Do (L (f :? k))) = step (f s) (k ())
step s (Do (R (() :? k))) = (s, Just (k ()))
La función de step
ejecuta la estrategia hasta que finaliza con Ret
, o cede, mutando el estado a medida que avanza.
El método general es el siguiente: separar los comandos (que interactúan en términos de valores) de los operadores de control (manipulación de cálculos); construir la mónada libre de "árboles de estrategia" sobre la firma de comandos (poner el mango); implementar los operadores de control por recursión en los árboles de estrategia.
No coincide con sus firmas de tipo exactamente, pero ciertamente simple:
{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses, UndecidableInstances #-}
import Control.Monad.State
newtype ContinuableT m a = Continuable { runContinuable :: m (Either a (ContinuableT m a)) }
instance Monad m => Monad (ContinuableT m) where
return = Continuable . return . Left
Continuable m >>= f = Continuable $ do
v <- m
case v of
Left a -> runContinuable (f a)
Right b -> return (Right (b >>= f))
instance MonadTrans ContinuableT where
lift m = Continuable (liftM Left m)
instance MonadState s m => MonadState s (ContinuableT m) where
get = lift get
put = lift . put
yield :: Monad m => ContinuableT m a -> ContinuableT m a
yield = Continuable . return . Right
step :: ContinuableT (State s) a -> s -> (Either a (ContinuableT (State s) a), s)
step = runState . runContinuable
-- mutate unnecessary, just use modify
Nota: Esta respuesta está available como un archivo Haskell literario en Gist.
Disfruté bastante este ejercicio. Intenté hacerlo sin mirar las respuestas, y valió la pena. Me tomó un tiempo considerable, pero el resultado es sorprendentemente cercano a dos de las otras respuestas, así como a la biblioteca de monad-coroutine . Así que supongo que esta es una solución algo natural a este problema. Sin este ejercicio, no entendería cómo funciona realmente la monad-coroutine .
Para agregar algún valor adicional, explicaré los pasos que finalmente me llevaron a la solución.
Reconociendo la mónada del estado
Como estamos tratando con estados, buscamos patrones que puedan ser descritos efectivamente por la mónada estatal. En particular, s - s
es isomorfo a s -> (s, ())
, por lo que podría ser reemplazado por State s ()
. Y la función del tipo s -> x -> (s, y)
se puede voltear a x -> (s -> (s, y))
, que en realidad es x -> State sy
. Esto nos lleva a firmas actualizadas
mutate :: State s () - Pause s ()
step :: Pause s () - State s (Maybe (Pause s ()))
Generalización
Nuestra mónada de Pause
está actualmente parametrizada por el estado. Sin embargo, ahora vemos que realmente no necesitamos el estado para nada, ni usamos ninguna especificación de la mónada estatal. Entonces podríamos tratar de hacer una solución más general que esté parametrizada por cualquier mónada:
mutate :: (Monad m) = m () -> Pause m ()
yield :: (Monad m) = Pause m ()
step :: (Monad m) = Pause m () -> m (Maybe (Pause m ()))
Además, podríamos tratar de hacer mutate
y step
un step
más general al permitir cualquier tipo de valor, no solo ()
. Y al darnos cuenta de que Maybe a
es isomorfo a Either a ()
podemos finalmente generalizar nuestras firmas para
mutate :: (Monad m) = m a -> Pause m a
yield :: (Monad m) = Pause m ()
step :: (Monad m) = Pause m a -> m (Either (Pause m a) a)
de modo que ese step
devuelve el valor intermedio del cálculo.
Transformador de mónada
Ahora, vemos que en realidad estamos tratando de hacer una mónada a partir de una mónada: agregue alguna funcionalidad adicional. Esto es lo que generalmente se llama un transformador de mónada . Además, la firma de MonadTrans
es exactamente igual a la lift desde MonadTrans
. Lo más probable es que estemos en el camino correcto.
La mónada final
La función de step
parece ser la parte más importante de nuestra mónada, define exactamente lo que necesitamos. Tal vez, esta podría ser la nueva estructura de datos? Intentemos:
import Control.Monad
import Control.Monad.Cont
import Control.Monad.State
import Control.Monad.Trans
data Pause m a
= Pause { step :: m (Either (Pause m a) a) }
Si la parte Either
está en lo cierto, es solo un valor monádico, sin ninguna suspensión. Esto nos lleva a implementar lo fácil: la función de lift
de MonadTrans
:
instance MonadTrans Pause where
lift k = Pause (liftM Right k)
y mutate
es simplemente una especialización:
mutate :: (Monad m) => m () -> Pause m ()
mutate = lift
Si la parte Either
deja, representa el cálculo continuo después de una suspensión. Así que vamos a crear una función para eso:
suspend :: (Monad m) => Pause m a -> Pause m a
suspend = Pause . return . Left
Ahora, yield
un cálculo es simple, simplemente suspendemos con un cálculo vacío:
yield :: (Monad m) => Pause m ()
yield = suspend (return ())
Aún así, nos falta la parte más importante. La instancia de Monad
. Vamos a arreglarlo Implementar el return
es simple, simplemente levantamos la mónada interna. Implementar >>=
es un poco más complicado. Si el valor de Pause
original fue solo un valor simple ( Right y
), entonces simplemente envolvemos fy
como resultado. Si se trata de un cálculo detenido que se puede continuar ( Left p
), descendemos recursivamente en él.
instance (Monad m) => Monad (Pause m) where
return x = lift (return x) -- Pause (return (Right x))
(Pause s) >>= f
= Pause $ s >>= /x -> case x of
Right y -> step (f y)
Left p -> return (Left (p >>= f))
Pruebas
Probemos hacer alguna función de modelo que use y actualice el estado, cediendo mientras se está dentro del cálculo:
test1 :: Int -> Pause (State Int) Int
test1 y = do
x <- lift get
lift $ put (x * 2)
yield
return (y + x)
Y una función auxiliar que depura la mónada: imprime sus pasos intermedios en la consola:
debug :: Show s => s -> Pause (State s) a -> IO (s, a)
debug s p = case runState (step p) s of
(Left next, s'') -> print s'' >> debug s'' next
(Right r, s'') -> return (s'', r)
main :: IO ()
main = do
debug 1000 (test1 1 >>= test1 >>= test1) >>= print
El resultado es
2000
4000
8000
(8000,7001)
como se esperaba.
Coroutines y monad-coroutine
Lo que hemos implementado es una solución monádica bastante general que implementa Coroutines . Tal vez no sea sorprendente que alguien haya tenido la idea antes :-), y haya creado el paquete monad-coroutine . Menos sorprendente, es bastante similar a lo que creamos.
El paquete generaliza la idea aún más. El cálculo continuo se almacena dentro de un funtor arbitrario. Esto permite suspend muchas variaciones de cómo trabajar con cálculos suspendidos. Por ejemplo, pasar un valor a la persona que llama de la resume (que llamamos step
), o esperar a que se proporcione un valor para continuar, etc.
Nota: que no proporcionó acceso directo al estado actual de esta mónada.
Pause s
es solo una mónada libre sobre las operaciones de mutate
y yield
. Implementado directamente usted obtiene:
data Pause s a
= Return a
| Mutate (s -> s) (Pause s a)
| Yield (Pause s a)
instance Monad (Pause s) where
return = Return
Return a >>= k = k a
Mutate f p >>= k = Mutate f (p >>= k)
Yield p >>= k = Yield (p >>= k)
con un par de constructores inteligentes para darle la API deseada:
mutate :: (s -> s) -> Pause s ()
mutate f = Mutate f (return ())
yield :: Pause s ()
yield = Yield (return ())
y la función de paso para conducirlo
step :: s -> Pause s () -> (s, Maybe (Pause s ()))
step s (Mutate f k) = step (f s) k
step s (Return ()) = (s, Nothing)
step s (Yield k) = (s, Just k)
También puedes definir esto directamente usando
data Free f a = Pure a | Free (f (Free f a))
(de mi paquete ''gratis'') con
data Op s a = Mutate (s -> s) a | Yield a
entonces ya tenemos una mónada para Pausa
type Pause s = Free (Op s)
y solo necesita definir los constructores inteligentes y el paso a paso.
Haciéndolo más rápido.
Ahora bien, estas implementaciones son fáciles de razonar, pero no tienen el modelo operativo más rápido. En particular, los usos asociados a la izquierda de (>> =) producen un código asintóticamente más lento.
Para Codensity puedes aplicar la mónada Codensity a tu mónada libre existente, o simplemente usar directamente la mónada ''Church free'' , que describo en profundidad en mi blog.
http://comonad.com/reader/2011/free-monads-for-less/
http://comonad.com/reader/2011/free-monads-for-less-2/
http://comonad.com/reader/2011/free-monads-for-less-3/
El resultado de aplicar la versión codificada por Church de la mónada Free es que obtiene un modelo fácil de razonar para el tipo de datos, y aún obtiene un modelo de evaluación rápido.
Por supuesto; simplemente deja que cualquier cálculo termine con un resultado, o se suspenda, dando una acción para usar en el currículum, junto con el estado en el momento:
data Pause s a = Pause { runPause :: s -> (PauseResult s a, s) }
data PauseResult s a
= Done a
| Suspend (Pause s a)
instance Monad (Pause s) where
return a = Pause (/s -> (Done a, s))
m >>= k = Pause $ /s ->
case runPause m s of
(Done a, s'') -> runPause (k a) s''
(Suspend m'', s'') -> (Suspend (m'' >>= k), s'')
get :: Pause s s
get = Pause (/s -> (Done s, s))
put :: s -> Pause s ()
put s = Pause (/_ -> (Done (), s))
yield :: Pause s ()
yield = Pause (/s -> (Suspend (return ()), s))
step :: Pause s () -> s -> (Maybe (Pause s ()), s)
step m s =
case runPause m s of
(Done _, s'') -> (Nothing, s'')
(Suspend m'', s'') -> (Just m'', s'')
La instancia de Monad
simplemente secuencia las cosas de la manera normal, pasando el resultado final a la continuación k
, o agregando el resto del cálculo que se realizará en suspensión.
{-# LANGUAGE TupleSections #-}
newtype Pause s x = Pause (s -> (s, Either x (Pause s x)))
instance Monad (Pause s) where
return x = Pause (, Left x)
Pause k >>= f = Pause $ /s -> let (s'', v) = k s in
case v of
Left x -> step (f x) s''
Right x -> (s'', Right (x >>= f))
mutate :: (s -> s) -> Pause s ()
mutate f = Pause (/s -> (f s, Left ()))
yield :: Pause s ()
yield = Pause (, Right (return ()))
step :: Pause s x -> s -> (s, Either x (Pause s x))
step (Pause x) = x
Así es como yo escribí esto. Le di un step
más de definición general, podría llamarse también runPause
. De hecho, pensar en el tipo de step
me lleva a la definición de Pause
.
En el paquete de monad-coroutine encontrará un transformador monad-coroutine general. La mónada Pause s
es la misma que Coroutine (State s) Id
. Puede combinar corutinas con otras mónadas.
Relacionado: la mónada Prompt en http://themonadreader.files.wordpress.com/2010/01/issue15.pdf