¿Cómo realizaría el código Haskell alguna acción de E / S (por ejemplo, getline o putStr) si el tipo de E / S no fuera una instancia de mónada?

Simplemente habría funciones específicas de IO para encadenar acciones de IO . Si bien en principio los tipos podrían ser diferentes a las funciones de Monad , no puedo pensar en alternativas razonables de antemano.

Quizás el equivalente de >>= se llamaría then , similar a las promesas de JavaScript¹. El análogo de return podría llamarse wrap . La biblioteca estándar expondría estas dos funciones, junto con cualquier otra función específica de IO que sea útil:

entonces :: IO a -> (a -> IO b) -> IO b
wrap :: a -> IO a

Cuando vamos a escribir funciones usando IO , combinamos nuestras piezas individuales con then y lambdas, tal vez sufriendo terribles recuerdos de Node.

getLine `then` \ line -> putStr line

Casi idéntico a lo que podríamos escribir en JavaScript con la nueva sintaxis lambda:

getLine (). then (line => putStr (line))

Después de hacer esto lo suficiente, nos hartaríamos y agregaríamos algo de azúcar de sintaxis al idioma, específico de IO , al igual que otros idiomas tienen azúcar de sintaxis para promesas (es decir, async y await ). El azúcar de sintaxis puede parecerse mucho a la notación do (o las comprensiones de Scala, que son una versión ligeramente diferente de la notación do), pero también puede parecer expresiones let con alguna palabra clave.

OCaml en realidad tiene una sintaxis de azúcar como esta específica para su biblioteca de promesa monádica (LWT), implementada como una macro. En OCaml, se ve así:

let% lwt s = getLine en putStr s

lo que desearía usar la función de bind de LWT (es decir, nuestra función específica de IO):

LWT.bind getLine (diversión s -> putStr s)

Nuestro hipotético azúcar de sintaxis específica de IO en Haskell podría verse así:

let-io s = getLine en putStr s

y desugaría a

getLine `then` \ s -> putStr s

En definitiva, ¿qué hemos hecho? Hemos tomado nuestro lenguaje y agregado las funciones básicas que necesitamos para componer los valores de IO ( wrap , then … etc.) y algo de azúcar de sintaxis para hacerlo más aceptable. Es una repetición de la historia experimentada con promesas en todo, desde JavaScript hasta C # y OCaml.

También es probable que agreguemos muchas funciones de conveniencia específicas de IO y tal vez incluso más azúcar de sintaxis, pero lo que describí sería el núcleo de IO en Haskell.

Excepto por el azúcar de sintaxis, todo esto se puede hacer en el idioma existente. Solo necesitaría aprovechar las primitivas del compilador para operar con valores de IO , es decir, la implementación de wrap y then dependería de la funcionalidad primitiva expuesta por el compilador y el tiempo de ejecución en lugar de Haskell puro.

Y todo lo que terminamos haciendo es reinventar mónadas pero mucho más estrechas, especializadas solo para IO una historia repetida en muchos otros idiomas con promesas. Mirando wrap y then y let-io , no es obvio que podría ser útil para muchas otras cosas: programación probabilística, programación no determinista, manejo de errores, concurrencia, continuaciones, registro, gestión de estado, programación reactiva …

Si alguien se diera cuenta de esto, ¿qué harían? Acabarían wrap y then en una clase:

clase Thennable t donde
wrap :: a -> ta
entonces :: ta -> (a -> tb) -> tb

y piratearían una extensión OverloadedIOLet que vincularía el azúcar de sintaxis let-io a la clase Thennable lugar de específicamente a IO .

¿Y dónde estaríamos? Tendríamos Monad pero más feo, sin referencia a la teoría subyacente, sin leyes claras, un nombre horrible y sintaxis azucarada robada poco a poco de instalaciones específicas de IO . Probablemente habría llegado una década más tarde que la programación monádica general de Haskell, con Haskell en el ínterin siendo como otros lenguajes con promesas, sin aprovechar la generalidad de una abstracción elegante y simple.

notas al pie
¹ Las promesas naturalmente forman una mónada, pero la API de JavaScript no refleja esto. En cambio, el método de JavaScript combinado ambos >>= y fmap en una función despachando dinámicamente en el tipo de resultado. Si la función a la que pasa devuelve un valor normal, se convierte en una promesa, pero si devuelve una promesa, la promesa se aplana.

Esto hace que toda la API sea menos elegante y regular, y le impide producir una promesa de una promesa, que es una limitación real para cualquier código que se supone que es completamente genérico sobre lo que funciona.

Si todas las demás cosas fueran iguales y simplemente no hubiera una definición de instancia de mónada para IO , aún podría escribir un programa Haskell que solo haga una acción de IO, es decir, getLine o putStr . También podría escribir un programa que haga tanto entrada como salida con interact , pero no puede encadenar múltiples acciones de E / S sin la instancia de mónada.

Podría usar unsafePerformIO para realizar múltiples acciones de E / S dentro de un solo programa, pero en ese caso, no habría garantía sobre el orden en que se realizarían, porque imponer un orden en las acciones es una de las cosas principales de la instancia de mónada. definición de IO hace.

Ya no podría encadenarlos, ya que los operadores (>> =) y (>>) no estarían disponibles, sin embargo, una sola acción de E / S seguiría funcionando.

No es que getContents, por ejemplo, NO sea una sola acción de E / S, ya que internamente realiza múltiples acciones de E / S.

Además, la sintaxis `do` no funcionaría.