una que programacion mónadas monadas monada funtores functores funcional ejemplos dividir definir datos aplicativos haskell

que - mónadas haskell



¿Es la construcción de IO monádica en Haskell simplemente una convención? (4)

¿Es la construcción de IO monádica en Haskell simplemente una convención o hay una razón de implementación para ello?

¿Podría no solo FFI en libc.so en lugar de hacer su IO, y omitir el IO Monad pedazo?

¿Funcionaría de todos modos, o es el resultado indeterminista debido a Haskell evaluando perezoso o algo más, como que el GHC es coincidencia de patrones para IO Monad y luego manejarlo de una manera especial u otra cosa.

¿Cuál es la verdadera razón? Al final terminas en un efecto secundario. Entonces, ¿por qué no hacerlo de la manera más simple?


¿Podría simplemente FFI en libc.so en su lugar para hacer IO y omitir la cosa IO Monad?

Tomando desde https://en.wikibooks.org/wiki/Haskell/FFI#Impure_C_Functions , si declara una función FFI como pura (por lo tanto, sin referencia a IO), entonces

GHC no tiene sentido al calcular dos veces el resultado de una función pura

lo que significa que el resultado de la llamada a la función está efectivamente en caché. Por ejemplo, un programa donde se declara un generador de número pseudoaleatorio impuro extranjero para devolver un CUInt

{-# LANGUAGE ForeignFunctionInterface #-} import Foreign import Foreign.C.Types foreign import ccall unsafe "stdlib.h rand" c_rand :: CUInt main = putStrLn (show c_rand) >> putStrLn (show c_rand)

devuelve lo mismo en cada llamada, al menos en mi compilador / sistema:

16807 16807

Si cambiamos la declaración para devolver un IO CUInt

{-# LANGUAGE ForeignFunctionInterface #-} import Foreign import Foreign.C.Types foreign import ccall unsafe "stdlib.h rand" c_rand :: IO CUInt main = c_rand >>= putStrLn . show >> c_rand >>= putStrLn . show

luego, esto da como resultado (probablemente) un número diferente devuelto cada llamada, ya que el compilador sabe que es impuro:

16807 282475249

Por lo tanto, vuelve a tener que usar IO para las llamadas a las bibliotecas estándar de todos modos.


Digamos que usando FFI definimos una función

c_write :: String -> ()

que miente sobre su pureza, en que cada vez que su resultado es forzado, imprime la cuerda. Para que no tengamos problemas de almacenamiento en caché en la respuesta de Michal, podemos definir estas funciones para tomar un argumento extra () .

c_write :: String -> () -> () c_rand :: () -> CUInt

En un nivel de implementación, esto funcionará siempre que CSE no sea demasiado agresivo (lo que no está en GHC porque puede provocar fugas inesperadas de memoria, resulta). Ahora que tenemos las cosas definidas de esta manera, hay muchas preguntas de uso incómodas que señala Alexis, pero podemos resolverlas usando una mónada:

newtype IO a = IO { runIO :: () -> a } instance Monad IO where return = IO . const m >>= f = IO $ /() -> let x = runIO m () in x `seq` f x rand :: IO CUInt rand = IO c_rand

Básicamente, simplemente rellenamos todas las incómodas preguntas de uso de Alexis en una mónada, y mientras usemos la interfaz monádica, todo permanece predecible. En este sentido, IO es solo una convención, porque podemos implementarlo en Haskell, no hay nada fundamental al respecto.

Eso es desde el punto de vista operacional.

Por otro lado, la semántica de Haskell en el informe se especifica usando solo la semántica denotacional . Y, en mi opinión, el hecho de que Haskell tenga una semántica denotacional precisa es una de las cualidades más bellas y útiles del lenguaje, lo que me permite un marco preciso para pensar en las abstracciones y así administrar la complejidad con precisión. Y aunque la mónada IO abstracta usual no tiene una semántica denotacional aceptada ( para lamentación de algunos de nosotros ), es al menos concebible que podamos crear un modelo denotacional para ella, conservando así algunos de los beneficios del modelo denotacional de Haskell. Sin embargo, la forma de E / S que acabamos de dar es completamente incompatible con la semántica denotacional de Haskell.

En pocas palabras, solo se supone que hay dos valores distinguibles (mensajes de error modulo fatales) de tipo () : () y ⊥. Si tratamos a FFI como los fundamentos de I / O y usamos la mónada IO solo "como una convención", entonces efectivamente agregamos un jillion de valores a cada tipo: para continuar teniendo una semántica denotacional, cada valor debe estar junto con la posibilidad de realizar E / S antes de su evaluación, y con la complejidad adicional que esto introduce, esencialmente perdemos toda nuestra capacidad para considerar dos programas distintos equivalentes excepto en los casos más triviales, es decir, perdemos nuestra capacidad de refactorización.

Por supuesto, debido a la unsafePerformIO de unsafePerformIO esto ya es técnicamente el caso, y los programadores Haskell avanzados también deben pensar en la semántica operacional. Pero la mayoría de las veces, incluso cuando trabajamos con E / S, podemos olvidarnos de todo eso y refactorizar con confianza, precisamente porque hemos aprendido que cuando usamos unsafePerformIO , debemos ser muy cuidadosos para asegurarnos de que funciona bien, que sigue siendo nos proporciona el mayor razonamiento denotacional posible. Si una función tiene unsafePerformIO , automáticamente le doy 5 o 10 veces más atención que las funciones normales, porque necesito entender los patrones de uso válidos (generalmente la firma tipo me dice todo lo que necesito saber), necesito pensar en el almacenamiento en caché y las condiciones de carrera, necesito pensar qué tan profundo necesito forzar sus resultados, etc. Es horrible [1]. El mismo cuidado sería necesario para FFI I / O.

En conclusión: sí, es una convención, pero si no la sigues, entonces no podemos tener cosas bonitas.

[1] Bueno, en realidad, creo que es bastante divertido, pero seguramente no es práctico pensar en todas esas complejidades todo el tiempo.


Eso depende de lo que sea el significado de "es" o, al menos, cuál es el significado de "convención".

Si una "convención" significa "la forma en que normalmente se hacen las cosas" o "un acuerdo entre las partes que cubren un asunto en particular", entonces es fácil dar una respuesta aburrida: sí, la mónada IO es una convención. Es la forma en que los diseñadores del lenguaje acordaron manejar las operaciones de IO y la forma en que los usuarios del lenguaje suelen realizar operaciones de IO.

Si se nos permite elegir una definición más interesante de "convención", entonces podemos obtener una respuesta más interesante. Si una "convención" es una disciplina impuesta en un idioma por sus usuarios para lograr un objetivo particular sin asistencia del idioma en sí, entonces la respuesta es no: la mónada IO es lo opuesto a una convención. Es una disciplina impuesta por el lenguaje que ayuda a los usuarios a construir y razonar sobre los programas.

El objetivo del tipo IO es crear una distinción clara entre los tipos de valores "puros" y los tipos de valores que requieren ejecución por parte del sistema de tiempo de ejecución para generar un resultado significativo. El sistema de tipo Haskell impone esta separación estricta, evitando que un usuario (por ejemplo) cree un valor de tipo Int que lanza los misiles proverbiales. Esta no es una convención en el segundo sentido: su objetivo es mover la disciplina requerida para realizar los efectos secundarios de forma segura y consistente desde el usuario hasta el lenguaje y su compilador.

¿Podría simplemente FFI en libc.so en su lugar para hacer IO y omitir la cosa IO Monad?

Por supuesto, es posible hacer IO sin una món IO: ver casi cualquier otro lenguaje de programación existente.

¿Funcionaría de todos modos o es el resultado indeterminista debido a Haskell evaluando perezoso o algo más, como que el GHC es coincidencia de patrones para IO Monad y luego manejarlo de una manera especial u otra cosa.

No hay tal cosa como un almuerzo gratis. Si Haskell permitiera que cualquier valor requiriera ejecución que implique a IO, entonces tendría que perder otras cosas que nosotros valoramos. Probablemente la más importante sea la transparencia referencial : si myInt pudiera ser a veces 1 y a veces 5 dependiendo de factores externos, perderíamos la mayor parte de nuestra capacidad de razonar sobre nuestros programas de manera rigurosa (lo que se conoce como razonamiento ecuacional ).

La pereza se mencionó en otras respuestas, pero el problema con la pereza sería específicamente que el intercambio ya no sería seguro. Si x en let x = someExpensiveComputationOf y in x * x no era referencialmente transparente, GHC no podría compartir el trabajo y tendría que calcularlo dos veces.

¿Cuál es la verdadera razón?

Sin la separación estricta de los valores efectivos de los valores no efectivos proporcionados por IO e impuestos por el compilador, Haskell dejaría efectivamente de ser Haskell. Hay muchos idiomas que no hacen cumplir esta disciplina. Sería bueno tener al menos uno alrededor de eso.

Al final terminas en un efecto lateral. Entonces, ¿por qué no hacerlo de la manera más simple?

Sí, al final su programa está representado por un valor llamado main con un tipo IO. Pero la pregunta no es dónde terminas, sino dónde comienzas : si comienzas por diferenciar entre valores efectivos y no efectivos de una manera rigurosa, entonces obtienes muchas ventajas al construir ese programa.


Sí, la E / S monádica es una consecuencia de que Haskell sea flojo. Específicamente, sin embargo, la E / S monádica es una consecuencia de que Haskell sea puro , lo cual es efectivamente necesario para que un lenguaje perezoso sea predecible.

Esto es fácil de ilustrar con un ejemplo. Imagine por un momento que Haskell no era puro, pero todavía era flojo. En lugar de putStrLn tener el tipo String -> IO () , simplemente tendría el tipo String -> () , e imprimiría una cadena a stdout como un efecto secundario. El problema con esto es que esto solo ocurrirá cuando se llame realmente a putStrLn , y en un lenguaje lento, las funciones solo se invocan cuando se necesitan sus resultados.

Aquí está el problema: putStrLn produces () . Ver el valor de tipo () es inútil, porque () significa "aburrido" . Eso significa que este programa haría lo que usted espera:

main :: () main = case putStr "Hello, " of () -> putStrLn " world!" -- prints “Hello, world!/n”

Pero creo que puedes estar de acuerdo en que el estilo de programación es bastante extraño. El case ... of es necesario, sin embargo, porque fuerza la evaluación de la llamada a putStr haciendo coincidir contra () . Si modifica el programa un poco:

main :: () main = case putStr "Hello, " of _ -> putStrLn " world!"

... ahora solo imprime world!/n , y la primera llamada no se evalúa en absoluto.

En realidad, esto empeora aún más, porque es aún más difícil de predecir tan pronto como comiences a intentar hacer una programación real. Considera este programa:

printAndAdd :: String -> Integer -> Integer -> Integer printAndAdd msg x y = putStrLn msg `seq` (x + y) main :: () main = let x = printAndAdd "first" 1 2 y = printAndAdd "second" 3 4 in (y + x) `seq` ()

¿Este programa imprime first/nsecond/n o second/nfirst/n ? Sin saber el orden en el cual (+) evalúa sus argumentos, no lo sabemos . Y en Haskell, el orden de evaluación ni siquiera está bien definido, por lo que es muy posible que el orden en el que se ejecutan los dos efectos sea completamente imposible de determinar.

Este problema no surge en los lenguajes estrictos con un orden de evaluación bien definido, pero en un lenguaje lento como Haskell, necesitamos alguna estructura adicional para garantizar que los efectos secundarios sean (a) realmente evaluados y (b) ejecutados en el orden correcto . Las mónadas son una interfaz que proporciona elegantemente la estructura necesaria para hacer cumplir ese orden.

¿Porqué es eso? ¿Y cómo es eso posible? Bueno, la interfaz monádica proporciona una noción de dependencia de datos en la firma para >>= , que impone un orden de evaluación bien definido. La implementación de IO de Haskell es "mágica", en el sentido de que se implementa en el tiempo de ejecución, pero la elección de la interfaz monádica está lejos de ser arbitraria. Parece ser una buena manera de codificar la noción de acciones secuenciales en un lenguaje puro, y hace posible que Haskell sea perezoso y referencialmente transparente sin sacrificar la secuencia predecible de los efectos.

Vale la pena señalar que las mónadas no son la única forma de codificar los efectos secundarios de una manera pura; de hecho, históricamente, ni siquiera son la única forma en que Haskell manejó los efectos secundarios . No se deje engañar por pensar que las mónadas son solo para I / O (no lo son), solo útiles en un lenguaje perezoso (son muy útiles para mantener la pureza incluso en un lenguaje estricto), solo útiles en un lenguaje puro (muchas cosas son mónadas útiles que no son solo para imponer la pureza), o que necesita mónadas para hacer E / S (no lo hace). Sin embargo, parecen haber funcionado bastante bien en Haskell para esos fines.

† Con respecto a esto, Simon Peyton Jones una vez notó que "la pereza lo mantiene honesto" con respecto a la pureza .