haskell monad-transformers

haskell - Monad Transformers vs Pasar parámetros a funciones



monad-transformers (1)

Soy nuevo en Haskell pero entiendo cómo se pueden usar los Transformadores de Monad. Sin embargo, todavía tengo dificultades para aprovechar su supuesta ventaja sobre pasar los parámetros a las llamadas de función.

Basado en la wiki Monad Transformers Explained , básicamente tenemos un Objeto Config definido como

data Config = Config Foo Bar Baz

y pasarlo, en lugar de escribir funciones con esta firma

client_func :: Config -> IO ()

usamos un transformador ReaderT Monad y cambiamos la firma a

client_func :: ReaderT Config IO ()

jalar la configuración es solo una llamada para ask .

La llamada de función cambia de client_func c a runReaderT client_func c

Multa.

¿Pero por qué esto hace que mi aplicación sea más simple?

1- Sospecho que Monad Transformers tiene un interés cuando unes muchas funciones / módulos para formar una aplicación. Pero aquí es donde se detiene mi entendimiento. ¿Podría alguien por favor arrojar algo de luz?

2- No pude encontrar ninguna documentación sobre cómo se escribe una gran aplicación modular en Haskell, donde los módulos exponen alguna forma de API y ocultan sus implementaciones, así como (parcialmente) ocultan sus propios estados y entornos de los otros módulos. ¿Alguna sugerencia, por favor?

(Edit: Real World Haskell afirma que "... este enfoque [Monad Transformers] ... se adapta a programas más grandes.", Pero no hay un ejemplo claro que demuestre esa afirmación)

EDITAR Siguiente Chris Taylor Respuesta a continuación

Chris explica perfectamente por qué encapsular Config, State, etc ... en una Transformer Monad proporciona dos beneficios:

  1. Impide que una función de nivel superior tenga que mantener en su firma de tipo todos los parámetros requeridos por las (sub) funciones que llama pero que no son necesarias para su propio uso (consulte la función getUserInput )
  2. y, en consecuencia, hace que las funciones de nivel superior sean más resistentes a un cambio del contenido de la Mónada de Transformador (digamos que desea agregar un Writer para proporcionar el Registro en una función de nivel inferior)

Esto tiene el costo de cambiar la firma de todas las funciones para que se ejecuten "en" Transformer Monad.

Entonces la pregunta 1 está completamente cubierta. Gracias Chris.

La pregunta 2 ahora se responde en esta publicación SO


Digamos que estamos escribiendo un programa que necesita cierta información de configuración de la siguiente forma:

data Config = C { logFile :: FileName }

Una forma de escribir el programa es pasar explícitamente la configuración entre las funciones. Sería bueno si solo tuviéramos que pasarlo a las funciones que lo usan explícitamente, pero lamentablemente no estamos seguros de si una función podría necesitar llamar a otra función que usa la configuración, por lo que estamos obligados a pasarla como una parámetro en todas partes (de hecho, tiende a ser las funciones de bajo nivel que necesitan usar la configuración, lo que nos obliga a pasarlo a todas las funciones de alto nivel también).

Escribamos el programa de esa manera, y luego lo volveremos a escribir usando la mónada Reader y veremos qué beneficio obtenemos.

Opción 1. Paso de configuración explícito

Terminamos con algo como esto:

readLog :: Config -> IO String readLog (C logFile) = readFile logFile writeLog :: Config -> String -> IO () writeLog (C logFile) message = do x <- readFile logFile writeFile logFile $ x ++ message getUserInput :: Config -> IO String getUserInput config = do input <- getLine writeLog config $ "Input: " ++ input return input runProgram :: Config -> IO () runProgram config = do input <- getUserInput config putStrLn $ "You wrote: " ++ input

Tenga en cuenta que en las funciones de alto nivel tenemos que pasar la configuración todo el tiempo.

Opción 2. Mónada de lector

Una alternativa es reescribir usando la mónada Reader . Esto complica un poco las funciones de bajo nivel:

type Program = ReaderT Config IO readLog :: Program String readLog = do C logFile <- ask readFile logFile writeLog :: String -> Program () writeLog message = do C logFile <- ask x <- readFile logFile writeFile logFile $ x ++ message

Pero como recompensa, las funciones de alto nivel son más simples, porque nunca necesitamos consultar el archivo de configuración.

getUserInput :: Program String getUserInput = do input <- getLine writeLog $ "Input: " ++ input return input runProgram :: Program () runProgram = do input <- getUserInput putStrLn $ "You wrote: " ++ input

Tomando más lejos

Podríamos volver a escribir las firmas de tipo de getUserInput y runProgram para que sean

getUserInput :: (MonadReader Config m, MonadIO m) => m String runProgram :: (MonadReader Config m, MonadIO m) => m ()

lo que nos da mucha flexibilidad para más adelante, si decidimos que queremos cambiar el tipo de Program subyacente por cualquier motivo. Por ejemplo, si queremos agregar un estado modificable a nuestro programa, podemos redefinir

data ProgramState = PS Int Int Int type Program a = StateT ProgramState (ReaderT Config IO) a

y no tenemos que modificar getUserInput o runProgram en absoluto; seguirán funcionando bien.

NB: No he marcado esta publicación, y mucho menos he intentado ejecutarla. Puede haber errores!