what traduccion monad maplestory ergo algorithm haskell functional-programming monads continuations

algorithm - traduccion - Control de flujo preciso en Haskell



monads scala (2)

La idea

¡Hola! Estoy tratando de implementar en Haskell una biblioteca de procesamiento de imágenes basada en la ideología del flujo de datos. Tengo un problema relacionado con la forma en que quiero manejar el flujo de control.

La idea principal es introducir un time . El time es un Float , al que se puede acceder desde cualquier parte del código (se puede pensar en Mónada State, pero un poco más divertido). Lo curioso de esto es que podemos usar la operación timeShift en los resultados, afectando el tiempo que las operaciones correspondientes verían.

Un ejemplo sería lo mejor para explicar esta situación. Permite usar el siguiente diagrama de flujo de datos:

-- timeShift(*2) -- -- / / -- readImage -- addImages -> out -- / / -- blur ----------

y su pseudocódigo (que no es typecheck - no es importante si usamos do o let notación aquí, la idea debería ser clara):

test = do f <- frame a <- readImage $ "test" + show f + ".jpg" aBlur <- blur a a'' <- a.timeShift(*2) out <- addImage aBlur a'' main = print =<< runStateT test 5

El 5 es el time que queremos ejecutar la función de test . La función timeShift afecta a todas las operaciones a la izquierda (en el diagrama de flujo de datos); en este caso, la función readImage se ejecutará dos veces para ambas ramas, la inferior usaría la trama 5 y la superior un cuadro 5*2 = 10 .

El problema

Proporciono aquí una implementación muy simple, que funciona muy bien, pero tiene algunas advertencias que quiero resolver. El problema es que quiero mantener el orden de todas las operaciones de IO. Mira la parte inferior, por ejemplo, lo que aclarará a qué me refiero.

Implementación de muestra

A continuación se muestra una implementación de ejemplo del algoritmo y un código, que construye el siguiente gráfico de flujo de datos:

-- A --- blur --- timeShift(*2) -- -- / -- addImages -> out -- / -- B --- blur --------------------

el código:

import Control.Monad.State -- for simplicity, lets assume an Image is just a String type Image = String imagesStr = ["a0","b1","c2","d3","e4","f5","g6","h7","i8","j9","k10","l11","m12","n13","o14","p15","q16","r17","s18","t19","u20","v21","w22","x23","y24","z25"] images = "abcdefghjiklmnoprstuwxyz" -------------------------------- -- Ordinary Image processing functions blurImg'' :: Image -> Image blurImg'' img = "(blur " ++ img ++ ")" addImage'' :: Image -> Image -> Image addImage'' img1 img2 = "(add " ++ img1 ++ " " ++ img2 ++ ")" -------------------------------- -- Functions processing Images in States readImage1 :: StateT Int IO Image readImage1 = do t <- get liftIO . putStrLn $ "[1] reading image with time: " ++ show t return $ imagesStr !! t readImage2 :: StateT Int IO Image readImage2 = do t <- get liftIO . putStrLn $ "[2] reading image with time: " ++ show t return $ imagesStr !! t blurImg :: StateT Int IO Image -> StateT Int IO Image blurImg img = do i <- img liftIO $ putStrLn "blurring" return $ blurImg'' i addImage :: StateT Int IO Image -> StateT Int IO Image -> StateT Int IO Image addImage img1 img2 = do i1 <- img1 i2 <- img2 liftIO $ putStrLn "adding images" return $ addImage'' i1 i2 timeShift :: StateT Int IO Image -> (Int -> Int) -> StateT Int IO Image timeShift img f = do t <- get put (f t) i <- img put t return i test = out where i1 = readImage1 j1 = readImage2 i2 = blurImg i1 j2 = blurImg j1 i3 = timeShift i2 (*2) out = addImage i3 j2 main = do print =<< runStateT test 5 print "end"

El resultado es:

[1] reading image with time: 10 blurring [2] reading image with time: 5 blurring adding images ("(add (blur k10) (blur f5))",5) "end"

y debería ser:

[1] reading image with time: 10 [2] reading image with time: 5 blurring blurring adding images ("(add (blur k10) (blur f5))",5) "end"

Tenga en cuenta que la salida correcta es ("(add (blur k10) (blur f5))",5) - lo que significa que hemos agregado la imagen k10 a f5 - del fotograma 10º y 5º respectivamente.

Más requisitos Estoy buscando una solución que permita a los usuarios escribir código simple (como en la función de test , por supuesto podría estar en una Monad), pero no quiero que manejen la lógica de desplazamiento de tiempo a mano.

Conclusiones

La única diferencia es el orden de ejecución de las acciones de IO. Me encantaría preservar el orden de las acciones de IO tal como están escritas en la función de test . Estaba intentando implementar la idea usando Countinuations , Arrows y algunos estados divertidos, pero sin éxito.


Una Monad no puede reordenar los pasos del componente que componen img1 e img2 en

addImage :: (Monad m) => m [i] -> m [i] -> m [i] addImage img1 img2 = do i1 <- img1 i2 <- img2 return $ i1 ++ i2

si existe alguna m [i] cuyo resultado dependa de un efecto secundario. Cualquier MonadIO m tiene un m [i] cuyo resultado depende de un efecto secundario, por lo tanto, no puede reordenar los pasos del componente de img1 e img2 .

Los azúcares anteriores a

addImage :: (Monad m) => m [i] -> m [i] -> m [i] addImage img1 img2 = img1 >>= (/i1 -> img2 >>= (/i2 -> return (i1 ++ i2) ) )

Centrémonos en el primero >>= (recordando que (>>=) :: forall a b. ma -> (a -> mb) -> mb ). Especializado para nuestro tipo, esto es (>>=) :: m [i] -> ([i] -> m [i]) -> m [i] . Si vamos a implementarlo, tendríamos que escribir algo como

(img1 :: m [i]) >>= (f :: [i] -> m [i]) = ...

Para hacer algo con f , necesitamos pasarlo por [i] . El único [i] correcto que tenemos está atrapado dentro de img1 :: m [i] . Necesitamos el resultado de img1 para hacer cualquier cosa con f . Ahora hay dos posibilidades. O bien podemos o no podemos determinar el resultado de img1 sin ejecutar sus efectos secundarios. Examinaremos ambos casos, comenzando con cuando no podamos.

no poder

Cuando no podemos determinar el resultado de img1 sin ejecutar sus efectos secundarios, solo tenemos una opción: debemos ejecutar img1 y todos sus efectos secundarios. Ahora tenemos un [i] , pero todos los efectos secundarios de img1 ya se han ejecutado. No hay manera de que podamos ejecutar ninguno de los efectos secundarios de img2 antes de algunos de los efectos secundarios de img1 porque los efectos secundarios de img1 ya han sucedido.

poder

Si podemos determinar el resultado de img1 sin ejecutar sus efectos secundarios, estamos de suerte. Hallamos el resultado de img1 y pasamos eso a f , obteniendo un nuevo m [i] contiene el resultado que queremos. Ahora podemos examinar los efectos secundarios de ambos img1 y el nuevo m [i] y reordenarlos (aunque aquí hay una gran advertencia sobre la ley asociativa para >>= ).

el problema a mano

Como esto se aplica a nuestro caso, para cualquier MonadIO , existe lo siguiente, cuyo resultado no puede determinarse sin ejecutar sus efectos secundarios, colocándonos firmemente en el caso imposible en el que no podamos reordenar los efectos secundarios.

counterExample :: (MonadIO m) => m String counterExample = liftIO getLine

También hay muchos otros ejemplos de contador, como readImage1 o readImage2 que realmente deben leer la imagen de IO .


Las bibliotecas de Dataflow y de programación reactiva funcional en Haskell generalmente se escriben en términos de Applicative o Arrow . Estas son abstracciones para cálculos que son menos generales que los de Monad: las clases de tipos Aplicable y Arrow no exponen una manera para que la estructura de los cálculos dependa de los resultados de otros cálculos. Como resultado, las bibliotecas que exponen solo estas clases de tipos pueden razonar sobre la estructura de los cálculos en la biblioteca independientemente de realizar esos cálculos. Resolveremos su problema en términos de la clase de tipo Applicative

class Functor f => Applicative f where -- | Lift a value. pure :: a -> f a -- | Sequential application. (<*>) :: f (a -> b) -> f a -> f b

Applicative permite a un usuario de la biblioteca realizar nuevos cálculos con pure , operar en cómputos existentes con fmap (de Functor ) y componer cálculos junto con <*> , usando el resultado de un cálculo como entrada para otro. No permite que un usuario de la biblioteca haga un cálculo que hace otro cálculo y luego usa el resultado de ese cálculo directamente; no hay forma de que un usuario pueda escribir join :: f (fa) -> fa . Esta restricción evitará que nuestra biblioteca se encuentre con el problema que describí en mi otra respuesta .

Transformadores, libres, y el transformador ApT

Tu problema de ejemplo es bastante complicado, por lo que vamos a sacar un montón de trucos Haskell de alto nivel y crear algunos nuevos. Los primeros dos trucos que vamos a sacar son transformadores y tipos de datos libres . Los transformadores son tipos que toman tipos con un tipo como el de Functor , Applicative o Monad y producen nuevos tipos del mismo tipo.

Los transformadores suelen tener el siguiente Double ejemplo. Double puede tomar cualquier Functor o Applicative o Monad y crear una versión que siempre contenga dos valores en lugar de uno

newtype Double f a = Double {runDouble :: f (a, a)}

Los tipos de datos libres son transformadores que hacen dos cosas. En primer lugar, dada alguna propiedad más simple del tipo subyacente, se obtienen nuevas propiedades interesantes para el tipo transformado. La Monad Free proporciona una Monad con cualquier Functor , y la Applicative gratuita, Ap , hace un Applicative de cualquier Functor . Lo otro que hacen los tipos "libres" es que "liberan" la implementación del intérprete tanto como sea posible . Aquí están los tipos para el FreeT gratuito, Ap , el Monad Free , Free , y el transfomer gratuito para mónadas, FreeT . El transformador de mónada gratuito proporciona un transformador de mónada para "libre" dado un Functor

-- Free Applicative data Ap f a where Pure :: a -> Ap f a Ap :: f a -> Ap f (a -> b) -> Ap f b -- Base functor of the free monad transformer data FreeF f a b = Pure a | Free (f b) -- Free monad transformer newtype FreeT f m a = FreeT {runFreeT :: m (FreeF f a (FreeT f m a)} -- The free monad is the free monad transformer applied to the Identity monad type Free f = FreeT f Identity

Aquí hay un boceto de nuestro objetivo: queremos proporcionar una interfaz Applicative para combinar cálculos, que, en la parte inferior, permita los cómputos de Monad ic. Queremos "liberar" al intérprete tanto como sea posible para que pueda reordenar los cálculos. Para hacer esto, combinaremos tanto el transformador de mónadas gratuito Applicative como el libre.

Queremos una interfaz Applicative , y la más fácil de hacer es la que podemos obtener para "gratis", que se alinea muy bien con nuestra meta de "liberar al interpetador" tanto como sea posible. Esto sugiere que nuestro tipo se verá como

Ap f a

para algunos Functor f y cualquier a . Nos gustaría que el cómputo subyacente sea sobre alguna Monad , y las Monad son funcionadoras, pero nos gustaría "liberar" al intérprete tanto como sea posible. Cogeremos el transformador de mónada gratuito como el functor subyacente para Ap , dándonos

Ap (FreeT f m) a

para algunos Functor f , algunos Monad m , y cualquier a . Sabemos que la Monad probablemente sea IO , pero dejaremos nuestro código lo más genérico posible. Solo tenemos que proporcionar el Functor para FreeT . Todos los Functors son Functors , por lo que Ap podría usarse para f , escribiríamos algo como

type ApT m a = Ap (FreeT (ApT m) m) a

Esto le da al compilador ajustes, así que en su lugar moveremos el Ap dentro y definiremos

newtype ApT m a = ApT {unApT :: FreeT (Ap (ApT m)) m a}

Obtendremos algunas instancias para esto y discutiremos su motivación real después de un interludio.

Interludio

Para ejecutar todo este código, necesitarás lo siguiente. El Map y el Control.Concurrent solo son necesarios para compartir cálculos, más sobre eso mucho más tarde.

{-# LANGUAGE GADTs #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} module Main where import Control.Monad.Trans.Class import Control.Monad.IO.Class import Control.Monad.Trans.Reader import Control.Applicative import Control.Applicative.Free hiding (Pure) import qualified Control.Applicative.Free as Ap (Ap(Pure)) import Control.Monad.Trans.Free import qualified Data.Map as Map import Control.Concurrent

Rellenándolo

Te engañé en la sección anterior y fingí descubrir que ApT no podía entender el problema. De hecho, descubrí ApT probando cualquier cosa para tratar de ApT de ApT en un Applicative y poder controlar su orden cuando salió. Durante mucho tiempo, estaba tratando de resolver cómo implementar mapApM (a continuación) para escribir flipImage (mi reemplazo para su blur ). Aquí está el transformador ApT en todo su esplendor. Está destinado a ser utilizado como el Functor para un Ap , y, al utilizar Ap como su propio Functor para FreeT , puede mágicamente introducir valores en un Applicative que no debería parecer posible.

newtype ApT m a = ApT {unApT :: FreeT (Ap (ApT m)) m a} deriving (Functor, Applicative, Monad, MonadIO)

Podría derivar aún más instancias de FreeT , estas son solo las que necesitamos. No puede derivar MonadTrans , pero podemos hacerlo nosotros mismos:

instance MonadTrans ApT where lift = ApT . lift runApT :: ApT m a -> m (FreeF (Ap (ApT m)) a (FreeT (Ap (ApT m)) m a)) runApT = runFreeT . unApT

La verdadera belleza de ApT es que podemos escribir algunos códigos aparentemente imposibles como

stuffM :: (Functor m, Monad m) => m (ApT m a) -> ApT m a stuffMAp :: (Functor m, Monad m) => m (ApT m a) -> Ap (ApT m) a

El m en el exterior desaparece, incluso en Ap que es meramente Applicative .

Esto funciona debido al siguiente ciclo de funciones, cada una de las cuales puede rellenar la salida de la función que está arriba en la entrada de la función debajo de ella. La primera función comienza con una ApT ma , y la última termina con una. (Estas definiciones no son parte del programa)

liftAp'' :: ApT m a -> Ap (ApT m) a liftAp'' = liftAp fmapReturn :: (Monad m) => Ap (ApT m) a -> Ap (ApT m) (FreeT (Ap (ApT m)) m a) fmapReturn = fmap return free'' :: Ap (ApT m) (FreeT (Ap (ApT m)) m a) -> FreeF (Ap (ApT m)) a (FreeT (Ap (ApT m)) m a) free'' = Free pure'' :: a -> FreeF (Ap (ApT m)) a (FreeT (Ap (ApT m)) m a) pure'' = Pure return'' :: (Monad m) => FreeF (Ap (ApT m)) a (FreeT (Ap (ApT m)) m a) -> m (FreeF (Ap (ApT m)) a (FreeT (Ap (ApT m)) m a)) return'' = return freeT :: m (FreeF (Ap (ApT m)) a (FreeT (Ap (ApT m)) m a)) -> FreeT (Ap (ApT m)) m a freeT = FreeT apT :: FreeT (Ap (ApT m)) m a -> ApT m a apT = ApT

Esto nos permite escribir

-- Get rid of an Ap by stuffing it into an ApT. stuffAp :: (Monad m) => Ap (ApT m) a -> ApT m a stuffAp = ApT . FreeT . return . Free . fmap return -- Stuff ApT into Free stuffApTFree :: (Monad m) => ApT m a -> FreeF (Ap (ApT m)) a (FreeT (Ap (ApT m)) m a) stuffApTFree = Free . fmap return . liftAp -- Get rid of an m by stuffing it into an ApT stuffM :: (Functor m, Monad m) => m (ApT m a) -> ApT m a stuffM = ApT . FreeT . fmap stuffApTFree -- Get rid of an m by stuffing it into an Ap stuffMAp :: (Functor m, Monad m) => m (ApT m a) -> Ap (ApT m) a stuffMAp = liftAp . stuffM

Y algunas funciones de utilidad para trabajar en una pila de transformadores

mapFreeT :: (Functor f, Functor m, Monad m) => (m a -> m b) -> FreeT f m a -> FreeT f m b mapFreeT f fa = do a <- fa FreeT . fmap Pure . f . return $ a mapApT :: (Functor m, Monad m) => (m a -> m b) -> ApT m a -> ApT m b mapApT f = ApT . mapFreeT f . unApT mapApM :: (Functor m, Monad m) => (m a -> m b) -> Ap (ApT m) a -> Ap (ApT m) b mapApM f = liftAp . mapApT f . stuffAp

Nos gustaría comenzar a escribir nuestros procesadores de imagen de ejemplo, pero primero tenemos que tomar otra diversión para abordar un requisito difícil.

Un requisito difícil: compartir entradas

Tu primer ejemplo muestra

-- timeShift(*2) -- -- / / -- readImage -- addImages -> out -- / / -- blur ----------

lo que implica que el resultado de readImage debe compartir entre blur y timeShift(*2) . Considero que esto significa que los resultados de readImage solo se deben calcular una vez para cada momento.

Applicative no es lo suficientemente poderoso como para capturar esto. Crearemos una nueva clase de tipos para representar cómputos cuyo resultado se puede dividir en múltiples flujos idénticos.

-- The class of things where input can be shared and divided among multiple parts class Applicative f => Divisible f where (</>) :: (f a -> f b) -> f a -> f b

Vamos a hacer un transformador que agregue esta capacidad a las Applicative existentes.

-- A transformer that adds input sharing data LetT f a where NoLet :: f a -> LetT f a Let :: LetT f b -> (LetT f b -> LetT f a) -> LetT f a

Y proporcione algunas funciones de utilidad e instancias para ello

-- A transformer that adds input sharing data LetT f a where NoLet :: f a -> LetT f a Let :: LetT f b -> (LetT f b -> LetT f a) -> LetT f a liftLetT :: f a -> LetT f a liftLetT = NoLet mapLetT :: (f a -> f b) -> LetT f a -> LetT f b mapLetT f = go where go (NoLet a) = NoLet (f a) go (Let b g) = Let b (go . g) instance (Applicative f) => Functor (LetT f) where fmap f = mapLetT (fmap f) -- I haven''t checked that these obey the Applicative laws. instance (Applicative f) => Applicative (LetT f) where pure = NoLet . pure NoLet f <*> a = mapLetT (f <*>) a Let c h <*> a = Let c ((<*> a) . h) instance (Applicative f) => Divisible (LetT f) where (</>) = flip Let

Procesadores de imagen

Con todos nuestros transformadores en su lugar, podemos comenzar a escribir nuestros procesadores de imágenes. En la parte inferior de nuestra pila tenemos nuestra ApT de una sección anterior

Ap (ApT IO)

Los cálculos deben poder leer el tiempo del entorno, por lo que agregaremos un ReaderT para ese

ReaderT Int (Ap (ApT IO))

Finalmente, nos gustaría poder compartir los cálculos, por lo que LetT transformador LetT en la parte superior, proporcionando todo el tipo de IP para nuestros procesadores de imágenes

type Image = String type IP = LetT (ReaderT Int (Ap (ApT IO)))

Leeremos imágenes de IO . getLine hace divertidos ejemplos interactivos.

readImage :: Int -> IP Image readImage n = liftLetT $ ReaderT (/t -> liftAp . liftIO $ do putStrLn $ "[" ++ show n ++ "] reading image for time: " ++ show t --getLine return $ "|image [" ++ show n ++ "] for time: " ++ show t ++ "|" )

Podemos cambiar el tiempo de las entradas

timeShift :: (Int -> Int) -> IP a -> IP a timeShift f = mapLetT shift where shift (ReaderT g) = ReaderT (g . f)

Añadir varias imágenes juntas

addImages :: Applicative f => [f Image] -> f Image addImages = foldl (liftA2 (++)) (pure [])

Y voltear imágenes que pretenden usar alguna biblioteca que está atascada en IO . No pude imaginar cómo blur una cuerda ...

inIO :: (IO a -> IO b) -> IP a -> IP b inIO = mapLetT . mapReaderT . mapApM flipImage :: IP [a] -> IP [a] flipImage = inIO flip'' where flip'' ma = do a <- ma putStrLn "flipping" return . reverse $ a

Interpretación LetT

Nuestra LetT para compartir resultados se encuentra en la parte superior de nuestra pila de transformadores. Tendremos que interpretarlo para obtener los cálculos debajo de él. Para interpretar LetT necesitaremos una forma de compartir resultados en IO , que proporciona memoize , y un interpeter que elimina el transformador LetT de la parte superior de la pila.

Para compartir cálculos, necesitamos almacenarlos en alguna parte, esto memoize un cómputo de IO en IO , asegurándose de que ocurra solo una vez, incluso a través de múltiples hilos.

memoize :: (Ord k) => (k -> IO a) -> IO (k -> IO a) memoize definition = do cache <- newMVar Map.empty let populateCache k map = do case Map.lookup k map of Just a -> return (map, a) Nothing -> do a <- definition k return (Map.insert k a map, a) let fromCache k = do map <- readMVar cache case Map.lookup k map of Just a -> return a Nothing -> modifyMVar cache (populateCache k) return fromCache

Para interpretar un Let , necesitamos un evaluador para el ApT IO subyacente para incorporarlo a las definiciones de los enlaces de Let . Dado que el resultado de los cálculos depende del entorno leído por ReaderT , incorporaremos tratar con ReaderT en este paso. Un enfoque más sofisticado usaría clases de transformadores, pero las clases de transformadores para Applicative es un tema para una pregunta diferente.

compileIP :: (forall x. ApT IO x -> IO x) -> IP a -> IO (Int -> ApT IO a) compileIP eval (NoLet (ReaderT f)) = return (stuffAp . f) compileIP eval (Let b lf) = do cb <- compileIP eval b mb <- memoize (eval . cb) compileIP eval . lf . NoLet $ ReaderT (liftAp . lift . mb)

Interpretando ApT

Nuestro intérprete utiliza el siguiente State para evitar la necesidad de mirar dentro de AsT , FreeT y FreeF todo el tiempo.

data State m a where InPure :: a -> State m a InAp :: State m b -> State m (b -> State m a) -> State m a InM :: m a -> State m a instance Functor m => Functor (State m) where fmap f (InPure a) = InPure (f a) fmap f (InAp b sa) = InAp b (fmap (fmap (fmap f)) sa) fmap f (InM m) = InM (fmap f m)

Interpereting Ap es más difícil de lo que parece. El objetivo es tomar los datos que están en Ap.Pure y ponerlos en InPure y los datos que están en Ap y ponerlos en InAp . interpretAp realidad necesita llamarse a sí mismo con un tipo más grande cada vez que entra en un Ap más profundo; la función sigue captando otro argumento. El primer argumento t proporciona una forma de simplificar estos tipos que de otra manera explotarían.

interpretAp :: (Functor m) => (a -> State m b) -> Ap m a -> State m b interpretAp t (Ap.Pure a) = t a interpretAp t (Ap mb ap) = InAp sb sf where sb = InM mb sf = interpretAp (InPure . (t .)) $ ap

interperetApT obtiene datos de ApT , FreeT , y FreeF y en State m

interpretApT :: (Functor m, Monad m) => ApT m a -> m (State (ApT m) a) interpretApT = (fmap inAp) . runApT where inAp (Pure a) = InPure a inAp (Free ap) = interpretAp (InM . ApT) $ ap

Con estas simples piezas de interpretación podemos hacer estrategias para interpretar los resultados. Cada estrategia es una función del State del intérprete a un nuevo State , con un posible efecto secundario en el camino. El orden que la estrategia elige para ejecutar los efectos secundarios determina el orden de los efectos secundarios. Haremos dos estrategias de ejemplo.

La primera estrategia realiza solo un paso en todo lo que está listo para ser calculado, y combina resultados cuando están listos. Esta es probablemente la estrategia que quieres.

stepFB :: (Functor m, Monad m) => State (ApT m) a -> m (State (ApT m) a) stepFB (InM ma) = interpretApT ma stepFB (InPure a) = return (InPure a) stepFB (InAp b f) = do sf <- stepFB f sb <- stepFB b case (sf, sb) of (InPure f, InPure b) -> return (f b) otherwise -> return (InAp sb sf)

Esta otra estrategia realiza todos los cálculos tan pronto como los conoce. Los ejecuta a todos en un solo pase.

allFB :: (Functor m, Monad m) => State (ApT m) a -> m (State (ApT m) a) allFB (InM ma) = interpretApT ma allFB (InPure a) = return (InPure a) allFB (InAp b f) = do sf <- allFB f sb <- allFB b case (sf, sb) of (InPure f, InPure b) -> return (f b) otherwise -> allFB (InAp sb sf)

Muchas, muchas otras estrategias son posibles.

Podemos evaluar una estrategia ejecutándola hasta que produzca un único resultado.

untilPure :: (Monad m) => ((State f a) -> m (State f a)) -> State f a -> m a untilPure s = go where go state = case state of (InPure a) -> return a otherwise -> s state >>= go

Ejecutando el intepreter

Para ejecutar el intérprete, necesitamos algunos datos de ejemplo. Aquí hay algunos ejemplos interesantes.

example1 = (/i -> addImages [timeShift (*2) i, flipImage i]) </> readImage 1 example1'' = (/i -> addImages [timeShift (*2) i, flipImage i, flipImage . timeShift (*2) $ i]) </> readImage 1 example1'''' = (/i -> readImage 2) </> readImage 1 example2 = addImages [timeShift (*2) . flipImage $ readImage 1, flipImage $ readImage 2]

El intérprete de LetT necesita saber qué evaluador usar para los valores encuadernados, por lo que definiremos nuestro evaluador solo una vez. Un solo interpretApT inicia la evaluación al encontrar el State inicial del intérprete.

evaluator :: ApT IO x -> IO x evaluator = (>>= untilPure stepFB) . interpretApT

Compilaremos example2 , que es esencialmente su ejemplo, y lo ejecutaremos por tiempo 5.

main = do f <- compileIP evaluator example2 a <- evaluator . f $ 5 print a

Lo cual produce casi el resultado deseado, con todas las lecturas sucediendo antes de cualquier volteo.

[2] reading image for time: 5 [1] reading image for time: 10 flipping flipping "|01 :emit rof ]1[ egami||5 :emit rof ]2[ egami|"