La mónada de la Pausa
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. Pueden permitirle escribir actualizaciones destructivas, pero no realmente. ¡Y luego la mónada de continuación te permite romper las mentes de la gente! Normalmente tu propia. ;-)
Pero aquí hay un desafío: ¿Puedes hacer una mónada que pueda ser pausada?
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 Pause
mónada es una especie de estado mónada (de ahí mutate
, con la semántica obvia). Normalmente una mónada como esta tiene algún tipo de función" run", que ejecuta el cálculo y le devuelve el estado final. Pero Pause
es diferente: Proporciona una función step
, que ejecuta el cálculo hasta que llama a la función mágica yield
. Aquí el cómputo se detiene, devolviendo a la persona que llama suficiente información para reanudar el cómputo más tarde.
Para mayor asombro: Permita al llamante modificar el estado entre step
llamada. (Las firmas de tipo anteriores 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 a salida los estados intermedios en su operación. Si quieres que el usuario sea capaz de cambiar algo a mitad de la ejecución, las cosas se complican muy rápido.
Ideas de implementación:
Obviamente se puede hacer con hilos, cerraduras y
IO
. Pero, ¿podemos hacerlo mejor? ;-)Algo loco con una continuación mónada?
Tal vez algún tipo de mónada de escritor, donde
yield
simplemente registra el estado actual, y luego podemos "fingir"step
iterando sobre los estados en el registro. (Obviamente esto impide alterar el estado entre pasos, ya que realmente no estamos "pausando" nada ahora.)
6 answers
Claro; simplemente deja que cualquier cálculo termine con un resultado, o se suspenda, dando una acción para ser utilizada en el curriculum vitae, 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 Monad
simplemente secuencia las cosas de la manera normal, pasando el resultado final a la continuación k
, o agregando el resto del cómputo que se hará en suspensión.
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
2012-04-19 21:46:57
Nota: que usted mismo no proporcionó acceso directo al estado actual s
en esta mónada.
Pause s
es solo una mónada libre sobre las operaciones mutate
y yield
. Implementado directamente se 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 step 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 podría 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 hay que definir los constructores inteligentes y paso a paso.
Haciéndolo más rápido.
Ahora, 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 código asintóticamente más lento.
Para evitar que se puede aplicar la Codensity mónada a su mónada libre existente, o simplemente utilizar la 'Church free' mónada directamente, los cuales 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 Libre es que obtienes un modelo fácil de razonar para el tipo de datos, y aún obtienes un modelo de evaluación rápido.
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
2012-04-19 21:57:18
Así es como lo haría, usando mónadas libres. ¿Qué son? Son árboles con acciones en los nodos y valores en las hojas, con >>=
actuando como injerto de árbol.
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 de infijo malhumorado. Para crear una instancia, solo necesitas f
que sea algo sobre lo que puedas mapear: cualquier Functor
servirá.
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 los árboles resultantes". Estos árboles pueden representar estrategias para la computación interactiva: todo el árbol cubre todas las posibles interacciones con el entorno, y el entorno elige qué camino en el árbol seguir. ¿Por qué son libres ? Son solo árboles, sin una teoría ecuacional interesante en ellos, diciendo qué estrategias son equivalentes a qué otras estrategias.
Ahora vamos a tener un kit para hacer Funtores que corresponden a un montón de comandos que tal vez quiera ser capaz de hacerlo. 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 el tipo de entrada s
y el tipo de salida t
. Para hacer eso, debe elegir una entrada en s
y explicar cómo continuar con el valor en x
dada la salida del comando en t
. Para mapear una función a través de tal cosa, la viras 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 acabo de escribir los tipos de valor para los comandos que queremos ser capaces de hacer!
Ahora asegurémonos de que podemos ofrecer una opción entre esos comandos. Podemos demostrar que una elección entre funtores produce un funtor. Más equipamiento 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)
Entonces, Modify s :+: Yield
representa la elección entre modificar y ceder. Cualquier firma de comandos simples (comunicarse con el mundo en términos de valores en lugar de manipular cálculos) se puede convertir en un funtor de esta manera. Es una molestia que tengo 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 modify y yield como one-do-then-return. Aparte de negociar la entrada ficticia para yield
, eso es solo mecánico.
mutate :: (s -> s) -> Pause s ()
mutate f = Do (L (f :? Ret))
yield :: Pause s ()
yield = Do (R (() :? Ret))
La función step
entonces da un significado a los árboles de estrategia. Es un operador de control , construyendo un cálculo (tal vez) desde 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 step
ejecuta la estrategia hasta que termina con un Ret
, o cede, mutando el estado a medida que avanza.
El método general es así: separe los comandos (interactuando en términos de valores) de los operadores de control (manipulando cálculos); construya la mónada libre de" árboles de estrategia " sobre la firma de los comandos (manejando el controlador); implemente los operadores de control por recursión sobre la estrategia arbol.
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
2012-04-19 22:15:53
No coincide exactamente con sus firmas de tipo, 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
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
2012-04-20 02:13:25
{-# 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 escribiría esto. Di step
una definición un poco más general, también podría llamarse runPause
. De hecho, pensar en el tipo de step
me lleva a la definición de Pause
.
En el paquete monad-coroutine encontrará un transformador monad general. La Pause s
mónada es la misma que Coroutine (State s) Id
. Puedes combinar las corrutinas con otras mónadas.
Relacionado: la mónada Prompt en http://themonadreader.files.wordpress.com/2010/01/issue15.pdf
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
2012-04-19 22:18:19
Nota: Esta respuesta está disponible como un archivo de Haskell alfabetizado en Gist.
Disfruté bastante este ejercicio. Traté de hacerlo sin mirar las respuestas, y valió la pena. Me llevó un tiempo considerable, pero el resultado es sorprendentemente cercano a dos de las otras respuestas, así como a la biblioteca monad-coroutine. Así que supongo que esta es una solución algo natural a este problema. Sin este ejercicio, no entendería cómo monad-coroutine realmente obrar.
Para agregar algún valor adicional, explicaré los pasos que finalmente me llevaron a la solución.
Reconociendo la mónada del estado
Ya que estamos tratando con estados, buscamos patrones que puedan ser efectivamente descritos por la mónada de estado. 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 es en realidad x -> State s y
. Esto nos lleva a actualizar firmas
mutate :: State s () - Pause s ()
step :: Pause s () - State s (Maybe (Pause s ()))
Generalización
Nuestra Pause
mónada está actualmente parametrizada por el estado. Sin embargo, ahora vemos que realmente no necesitamos al estado para nada, ni usamos ningún detalle de la mónada estatal. Así que podríamos intentar hacer una solución más general que sea 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 ()))
También, podríamos tratar de hacer mutate
y step
más generales permitiendo cualquier tipo de valor, no solo ()
. Y al darse cuenta de que Maybe a
es isomorfo a Either a ()
finalmente podemos generalizar nuestras firmas a
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 step
devuelve el valor intermedio del cálculo.
Monad transformer
Ahora, vemos que en realidad estamos tratando de hacer una mónada a partir de una mónada - añadir alguna funcionalidad adicional. Esto es lo que generalmente se llama un transformador de mónada . Además, la firma de mutate
es exactamente la misma que lift de MonadTrans
. Lo más probable es que estemos a la derecha. pista.
La mónada final
La función 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? Probemos:
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
es Right
, es solo un valor monádico, sin ningún
suspensiones. Esto nos lleva a cómo implementar lo más fácil - el lift
función 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 el Either
part is Left
, it represents the continued computation after a suspension. 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 vacío
cálculo:
yield :: (Monad m) => Pause m ()
yield = suspend (return ())
Aún así, nos estamos perdiendo la parte más importante. La instancia Monad
. Vamos a arreglar
se. Implementar return
es simple, solo levantamos la mónada interna. Implementar >>=
es un poco más complicado. Si el valor original Pause
era solo un valor simple (Right y
), entonces simplemente envolver f y
como resultado. Si es un cómputo pausado que puede ser continuado (Left p
), descendemos recursivamente a é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
Tratemos de hacer alguna función de modelo que use y actualice el estado, produciendo mientras que 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 a 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.
Corrutinas y mónada-corutina
Lo que hemos implementado es una solución monádica bastante general que implementa Corrutinas. Tal vez no es sorprendente que alguien tuviera la idea antes: -), y creó 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 suspender muchas variaciones cómo para trabajar con cálculos suspendidos. Por ejemplo, para pasar un valoral llamador de resume (que llamamos step
), o para esperar a que se proporcione un valor para continuar, etc.
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
2012-08-31 15:42:54