mónada monadologia monadas leibniz las filosofía filosofia educatina doctrina haskell monads monad-transformers

haskell - monadologia - ¿Cuál es la diferencia entre los diferentes ordenamientos de los mismos transformadores de mónada?



monadas filosofia (5)

Resumen: Diferentes órdenes de pila producen diferente lógica de negocios

Es decir, diferentes órdenes de transformador de mónada de la pila no solo afectan a los pedidos de evaluación, sino también a las funcionalidades de los programas.

Al demostrar el impacto de los pedidos, las personas usualmente usan los transformadores más simples como ReaderT , WriterT , StateT , MaybeT , ExceptT . Diferentes órdenes de ellos no dan una lógica de negocios dramáticamente diferente, por lo que es difícil entender el impacto claramente. Además, algunos subconjuntos de ellos son conmutativos, es decir, no hay diferencias de funcionalidad.

Para fines de demostración, sugiero usar StateT y ListT , que revelan la dramática diferencia entre los pedidos de transformadores en las pilas de mónadas.

Antecedentes: StateT y ListT

  • State : State mónada State está bien explicada en Para unas cuantas mónadas más . StateT solo le da un poco más de poder: usar las operaciones monádicas de su m subyacente. Es suficiente si conoce evalStateT , put , get y evalStateT , que se explican en muchos tutoriales de mónadas State .
  • ListT : List , aka, [] , es una mónada (explicada en Un puñado de mónadas ). ListT ma (en el paquete list-t ) le ofrece algo similar a [a] más todas las operaciones monádicas de la mónada subyacente m . La parte difícil es la ejecución de ListT (algo comparable a evalStateT ): hay muchas formas de ejecución. Piense en los diferentes resultados que le interesan al utilizar evalStateT , runStateT y execState , el contexto de la List de la List tiene muchos consumidores potenciales, como simplemente execState , es decir, traverse_ , plegarlos , es decir, fold y más.

Experimento: comprender el impacto de la orden de transformador de mónada

Construiremos una simple pila de StateT mónadas de dos capas utilizando StateT y ListT encima de IO para cumplir algunas funcionalidades de demostración.

Descripción de la tarea

Resumen de números en una secuencia

El flujo se ListT como una lista de ListT Integer , por lo que nuestro ListT . Para resumirlos, debemos mantener un estado de la suma mientras procesamos cada elemento en el flujo, donde viene nuestro StateT .

Dos pilas

Tenemos un estado simple como Int para mantener la suma.

  • ListT (StateT Int IO) a
  • StateT Int (ListT IO) a

Programa completo

#!/usr/bin/env stack -- stack script --resolver lts-11.14 --package list-t --package transformers import ListT (ListT, traverse_, fromFoldable) import Control.Monad.Trans.Class (lift) import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.State (StateT, evalStateT, get, modify) main :: IO() main = putStrLn "#### Task: summing up numbers in a stream" >> putStrLn "#### stateful (StateT) stream (ListT) processing" >> putStrLn "#### StateT at the base: expected result" >> ltst >> putStrLn "#### ListT at the base: broken states" >> stlt -- (ListT (StateT IO)) stack ltst :: IO () ltst = evalStateT (traverse_ (/_ -> return ()) ltstOps) 10 ltstOps :: ListT (StateT Int IO) () ltstOps = genLTST >>= processLTST >>= printLTST genLTST :: ListT (StateT Int IO) Int genLTST = fromFoldable [6,7,8] processLTST :: Int -> ListT (StateT Int IO) Int processLTST x = do liftIO $ putStrLn "process iteration LTST" lift $ modify (+x) lift get printLTST :: Int -> ListT (StateT Int IO) () printLTST = liftIO . print -- (StateT (ListT IO)) stack stlt :: IO () stlt = traverse_ (/_ -> return ()) $ evalStateT (genSTLT >>= processSTLT >>= printSTLT) 10 genSTLT :: StateT Int (ListT IO) Int genSTLT = lift $ fromFoldable [6,7,8] processSTLT :: Int -> StateT Int (ListT IO) Int processSTLT x = do liftIO $ putStrLn "process iteration STLT" modify (+x) get printSTLT :: Int -> StateT Int (ListT IO) () printSTLT = liftIO . print

Resultados y explicación

$ ./order.hs #### Task: summing up numbers in a stream #### stateful (StateT) stream (ListT) processing #### StateT at the base: expected result process iteration LTST 16 process iteration LTST 23 process iteration LTST 31 #### ListT at the base: broken states process iteration STLT 16 process iteration STLT 17 process iteration STLT 18

La primera pila ListT (StateT Int IO) a produce el resultado correcto, ya que StateT se evalúa después de ListT . Al evaluar StateT , el sistema de tiempo de ejecución ya evaluó todas las operaciones de ListT : alimentar a la pila con un flujo [6,7,8] , pasando por ellas con traverse_ . La palabra que se evalúa aquí significa que los efectos de ListT se han ido y ListT es transparente para StateT .

La segunda pila de StateT Int (ListT IO) a no tiene el resultado correcto ya que StateT tiene una vida demasiado corta. En cada iteración de la evaluación de ListT , aka, traverse_ , el estado se crea, evalúa y desaparece. El StateT en esta estructura de pila no logra su propósito de mantener los estados entre las operaciones de elementos de lista / flujo.

Estoy intentando definir una API para expresar un tipo particular de procedimiento en mi programa.

newtype Procedure a = { runProcedure :: ? }

Hay estado, que consiste en una asignación de ID a registros:

type ID = Int data Record = { ... } type ProcedureState = Map ID Record

Hay tres operaciones básicas:

-- Declare the current procedure invalid and bail (similar to some definitions of fail for class Monad) abort :: Procedure () -- Get a record from the shared state; abort if the record does not exist. retrieve :: ID -> Procedure Record -- Store (or overwrite) a record in the shared state. store :: ID -> Record -> Procedure ()

Tengo algunas metas con estas operaciones:

  • Los procedimientos pueden hacer suposiciones (a diferencia de una llamada en bruto de Map.lookup ) acerca de qué registros están disponibles, y si alguna de sus suposiciones es incorrecta, el Procedimiento en su totalidad devuelve un error.
  • Se puede encadenar una serie de Procedimientos usando <|> (de la clase Alternativa) para recurrir a Procedimientos que hacen diferentes suposiciones. (Similar a STM''s o orElse )

Teniendo en cuenta estos objetivos, creo que quiero una combinación de las mónadas State y Maybe .

-- Which to choose? type Procedure a = StateT ProcedureState Maybe a type Procedure a = MaybeT (State ProcedureState) a

No puedo entender cómo los dos ordenamientos de Maybe y State se comportarán de manera diferente. ¿Alguien puede explicar la diferencia de comportamiento entre los dos ordenamientos?

Además, si ves un problema con mi pensamiento original (quizás estoy sobre ingeniería), siéntete libre de señalarlo.

Conclusión: las tres respuestas fueron útiles, pero había una idea común que me ayudó a decidir qué orden quería. Al observar el tipo de retorno de runMaybeT / runStateT , fue fácil ver qué combinación tenía el comportamiento que estaba buscando. (En mi caso, quiero el tipo de devolución Maybe (ProcedureState, a) ).


Edit: originalmente obtuve los casos al revés. Arreglado ahora.

La diferencia entre los ordenamientos de las pilas de transformadores de mónada realmente solo importa cuando se están despegando capas de la pila.

type Procedure a = MaybeT (State ProcedureState) a

En este caso, primero ejecuta MaybeT, lo que da como resultado un cálculo con estado que devuelve un Maybe a .

type Procedure a = StateT ProcedureState Maybe a

Aquí, StateT es la mónada externa, lo que significa que después de ejecutar StateT con un estado inicial, se le otorgará un Maybe (a, ProcedureState) . Es decir, el cálculo puede haber tenido éxito o puede no haberlo tenido.

Entonces, lo que elija dependerá de cómo quiera manejar los cálculos parciales. Con MaybeT en el exterior, siempre obtendrá algún tipo de estado devuelto independientemente del éxito del cálculo, que puede o no ser útil. Con StateT en el exterior, garantiza que todas las transacciones con estado son válidas. Por lo que describe, probablemente yo usaría la variante StateT , pero espero que cualquiera de los dos pueda funcionar.

La única regla para el ordenamiento de transformadores de mónada es que si IO (u otra mónada sin transformador) está involucrada, debe ser la parte inferior de la pila. Normalmente, las personas usarán ErrorT como el siguiente nivel más bajo si es necesario.


Para complementar las otras respuestas, me gustaría describir cómo resolver esto en el caso general. Es decir, dados dos transformadores, ¿cuáles son las semánticas de sus dos combinaciones?

Tuve muchos problemas con esta pregunta cuando comencé a usar transformadores de mónada en un proyecto de análisis la semana pasada. Mi enfoque fue crear una tabla de tipos transformados que consulto cuando no estoy seguro. Así es como lo hice:

Paso 1 : crear una tabla de los tipos de mónada básicos y sus tipos de transformadores correspondientes:

transformer type base type (+ parameter order) --------------------------------------------------------------- MaybeT m a m (Maybe a) b. Maybe b StateT s m a s -> m (a, s) t b. t -> (b, t) ListT m a m [a] b. [] b ErrorT e m a m (Either e a) f b. Either f b ... etc. ...

Paso 2: aplique cada transformador de mónada a cada una de las mónadas base, sustituyendo el parámetro de tipo m :

inner outer combined type Maybe MaybeT Maybe (Maybe a) Maybe StateT s -> Maybe (a, s) -- <== this !! ... etc. ... State MaybeT t -> (Maybe a, t) -- <== and this !! State StateT s -> t -> ((a, s), t) ... etc. ...

(Este paso es un poco doloroso, ya que hay un número cuadrático de combinaciones ... pero fue un buen ejercicio para mí, y solo tuve que hacerlo una vez). La clave para mí aquí es que escribí los tipos combinados sin envolver. - Sin todos esos molestos envoltorios MaybeT, StateT, etc. Es mucho más fácil para mí mirar y pensar en los tipos sin el repetitivo.

Para responder a su pregunta original, esta tabla muestra que:

  • MaybeT + State :: t -> (Maybe a, t) un cálculo con estado donde puede que no haya un valor, pero siempre habrá una salida de estado (posiblemente modificada)

  • StateT + Maybe :: s -> Maybe (a, s) un cálculo en el que tanto el estado como el valor pueden estar ausentes


Podrá responder la pregunta usted mismo si intenta escribir funciones de "ejecución" para ambas versiones. No tengo instalados los transformadores MTL +, por lo que no puedo hacerlo yo mismo. Uno devolverá (Tal vez a, estado) El otro Tal vez (a, estado) .

Editar: he truncado mi respuesta, ya que agrega detalles que pueden ser confusos. La respuesta de John golpea el clavo en la cabeza.


Supongamos que en lugar de usar State / StateT para almacenar el estado de sus procedimientos, estaba usando un IORef en la mónada IO .

A priori, hay dos formas en que puede querer que mzero (o fail ) se comporte en una combinación de las mónadas IO y Maybe :

  • o bien mzero borra todo el cálculo, de modo que mzero <|> x = x ; o
  • mzero hace que el cálculo actual no devuelva un valor, pero los efectos de tipo IO se conservan.

Parece que desea el primero, de modo que el estado establecido por un procedimiento se "desenrolla" para el siguiente procedimiento en una cadena de <|> s.

Por supuesto, esta semántica es imposible de implementar. No sabemos si un cálculo invocará mzero hasta que lo mzero , pero hacerlo puede tener efectos de IO arbitrarios como launchTheMissiles , que no podemos revertir.

Ahora, tratemos de construir dos pilas de transformadores de mónada diferentes de Maybe y IO :

  • IOT Maybe - oops, esto no existe!
  • MaybeT IO

El que existe ( MaybeT IO ) proporciona el comportamiento mzero que es posible, y el inexistente IOT Maybe corresponde al otro comportamiento.

Afortunadamente, está utilizando State ProcedureState , cuyos efectos pueden revertirse, en lugar de IO ; la pila de transformadores de la mónada que desea es StateT ProcedureState Maybe una.