¿Es la mónada IO técnicamente incorrecta?

12

En el wiki de haskell hay el siguiente ejemplo de uso condicional de la mónada IO (ver aquí) .

when :: Bool -> IO () -> IO ()
when condition action world =
    if condition
      then action world
      else ((), world)

Tenga en cuenta que en este ejemplo, la definición de IO ase toma RealWorld -> (a, RealWorld)para hacer que todo sea más comprensible.

Este fragmento ejecuta condicionalmente una acción en la mónada IO. Ahora, suponiendo que conditionsea ​​así False, la acción actionnunca debe ejecutarse. Usando una semántica perezosa, este sería el caso. Sin embargo, se observa aquí que Haskell es técnicamente hablando no estricto. Esto significa que el compilador puede, por ejemplo, ejecutarse de manera preventiva action worlden un subproceso diferente y luego descartar ese cálculo cuando descubre que no lo necesita. Sin embargo, para ese punto los efectos secundarios ya habrán sucedido.

Ahora, uno podría implementar la mónada IO de tal manera que los efectos secundarios solo se propaguen cuando todo el programa haya terminado, y sepamos exactamente qué efectos secundarios deberían ejecutarse. Sin embargo, este no es el caso, porque es posible escribir programas infinitos en Haskell, que claramente tienen efectos secundarios intermedios.

¿Significa esto que la mónada IO está técnicamente equivocada o hay algo más que impide que esto suceda?

Lasse
fuente
Bienvenido a la informática ! Su pregunta está fuera de tema aquí: tratamos las preguntas de ciencias de la computación , no las preguntas de programación (consulte nuestras preguntas frecuentes ). Su pregunta puede ser sobre el tema en Stack Overflow .
dkaeae
2
En mi opinión, esta es una pregunta de informática, porque trata con la semántica teórica de Haskell, no con una pregunta práctica de programación.
Lasse
44
No estoy muy familiarizado con la teoría del lenguaje de programación, pero creo que esta pregunta es sobre el tema aquí. Podría ayudar si aclara qué significa "incorrecto" aquí. ¿Qué propiedad crees que tiene la mónada IO que no debería tener?
Lagarto discreto
1
Este programa no está bien escrito. No estoy seguro de lo que realmente querías escribir. La definición de whenes tipificable, pero no tiene el tipo que usted declara, y no veo qué hace que este código en particular sea interesante.
Gilles 'SO- deja de ser malvado'
2
Este programa se toma textualmente de la página Haskell-wiki vinculada directamente arriba. De hecho, no escribe. Esto se debe a que está escrito bajo el supuesto que IO ase define como RealWorld -> (a, RealWorld), con el fin de hacer que las partes internas de IO sean más legibles.
Lasse

Respuestas:

12

Esta es una "interpretación" sugerida de la IOmónada. Si quiere tomar en serio esta "interpretación", entonces debe tomar en serio "RealWorld". Es irrelevante si action worldse evalúa especulativamente o no, actionno tiene ningún efecto secundario, sus efectos, si los hay, se manejan devolviendo un nuevo estado del universo donde se han producido esos efectos, por ejemplo, se ha enviado un paquete de red. Sin embargo, el resultado de la función es ((),world)y, por lo tanto, el nuevo estado del universo es world. No usamos el nuevo universo que podríamos haber evaluado especulativamente en el lateral. El estado del universo es world.

Probablemente tengas dificultades para tomar eso en serio. Hay muchas maneras en que esto es, en el mejor de los casos, superficialmente paradójico y sin sentido. La concurrencia es especialmente no obvia o loca con esta perspectiva.

"Espera, espera", dices. " RealWorldes solo una 'ficha'. En realidad, no es el estado de todo el universo". Bien, entonces esta "interpretación" no explica nada. Sin embargo, como detalle de implementación , así es como los modelos GHC IO. 1 Sin embargo, esto significa que tenemos "funciones" mágicas que en realidad tienen efectos secundarios y este modelo no proporciona orientación sobre su significado. Y, dado que estas funciones en realidad tienen efectos secundarios, la preocupación que plantea es completamente acertada. GHC no tiene que salir de su manera de asegurarse RealWorldy estas funciones especiales no están optimizados de manera que cambien el comportamiento previsto del programa.

Personalmente (como probablemente es evidente ahora), creo que este modelo de "paso del mundo" IOes simplemente inútil y confuso como herramienta pedagógica. (Si es útil para la implementación, no lo sé. Para GHC, creo que es más un artefacto histórico).

Un enfoque alternativo es ver IOcomo solicitudes descriptivas con manejadores de respuestas. Hay varias maneras de hacer esto. Probablemente lo más accesible es usar una construcción de mónada gratis, específicamente podemos usar:

data IO a = Return a | Request OSRequest (OSResponse -> IO a)

Hay muchas maneras de hacer esto más sofisticado y tener propiedades algo mejores, pero esto ya es una mejora. No requiere suposiciones filosóficas profundas sobre la naturaleza de la realidad para entender. Todo lo que dice es que IOes un programa trivial Returnque no hace nada más que devolver un valor, o es una solicitud al sistema operativo con un controlador para la respuesta. OSRequestpuede ser algo como:

data OSRequest = OpenFile FilePath | PutStr String | ...

Del mismo modo, OSResponsepodría ser algo como:

data OSResponse = Errno Int | OpenSucceeded Handle | ...

(Una de las mejoras que se pueden hacer es hacer que las cosas sean más seguras para que sepa que no recibirá OpenSucceededuna PutStrsolicitud). Este modelo IOdescribe las solicitudes que son interpretadas por algún sistema (para la IOmónada "real" esto es el tiempo de ejecución de Haskell en sí), y luego, tal vez, ese sistema llamará al controlador que hemos proporcionado una respuesta. Esto, por supuesto, tampoco da ninguna indicación de cómo se PutStr "hello world"debe manejar una solicitud como , pero tampoco pretende hacerlo. Hace explícito que esto se está delegando a algún otro sistema. Este modelo también es bastante preciso. Todos los programas de usuario en sistemas operativos modernos necesitan hacer solicitudes al sistema operativo para hacer cualquier cosa.

Este modelo proporciona las intuiciones correctas. Por ejemplo, muchos principiantes ven cosas como el <-operador como "desenvolviendo" IO, o tienen (desafortunadamente reforzado) vistas de que un IO String, por ejemplo, es un "contenedor" que "contiene" Strings (y luego <-los saca). Esta vista de solicitud-respuesta hace que esta perspectiva sea claramente errónea. No hay un identificador de archivo dentro de OpenFile "foo" (\r -> ...). Una analogía común para enfatizar esto es que no hay pastel dentro de una receta para pastel (o tal vez "factura" sería mejor en este caso).

Este modelo también funciona fácilmente con concurrencia. Podemos tener fácilmente un constructor para me OSRequestgusta Fork :: (OSResponse -> IO ()) -> OSRequesty luego el tiempo de ejecución puede intercalar las solicitudes producidas por este controlador adicional con el controlador normal como quiera. Con cierta inteligencia, puede usar esto (o técnicas relacionadas) para modelar cosas como la concurrencia más directamente en lugar de simplemente decir "hacemos una solicitud al sistema operativo y las cosas suceden". Así es como funciona la IOSpecbiblioteca .

1 Hugs utilizó una implementación basada en la continuación de la IOcual es más o menos similar a lo que describo, aunque con funciones opacas en lugar de un tipo de datos explícito. HBC también usó una implementación basada en la continuación en capas sobre el antiguo IO basado en el flujo de solicitud-respuesta. NHC (y, por lo tanto, YHC) usaba thunks, es decir, aproximadamente, IO a = () -> aaunque ()se llamaba World, pero no está haciendo pasar el estado. JHC y UHC utilizaron básicamente el mismo enfoque que GHC.

Derek Elkins dejó SE
fuente
Gracias por su respuesta brillante, realmente ayudó. Su implementación de IO tomó algún tiempo para entenderlo, pero estoy de acuerdo en que es más intuitivo. ¿Está afirmando que esta implementación no sufre problemas potenciales con el ordenamiento de efectos secundarios como lo hace la implementación de RealWorld? No puedo ver ningún problema de inmediato, pero tampoco me queda claro que no existan.
Lasse
Un comentario: parece que en OpenFile "foo" (\r -> ...)realidad debería ser Request (OpenFile "foo") (\r -> ...)?
Lasse
@Lasse Sí, debería haber sido con Request. Para responder a su primera pregunta, esto IOes claramente insensible al orden de evaluación (módulo inferior) porque es un valor inerte. Todos los efectos secundarios (si los hubiera) serían hechos por lo que interpreta este valor. En el whenejemplo, no importaría si actionse evaluara, porque sería un valor como el Request (PutStr "foo") (...)que no le daremos a la cosa que interpreta estas solicitudes de todos modos. Es como el código fuente; no importa si lo reduce ansiosamente o perezosamente, no pasa nada hasta que se lo dé a un intérprete.
Derek Elkins salió del SE
Ah si, ya veo eso. Esta es una definición realmente inteligente. Al principio pensé que todos los efectos secundarios tendrían que suceder necesariamente cuando todo el programa haya terminado de ejecutarse, porque debe construir la estructura de datos antes de poder interpretarla. Pero dado que una solicitud contiene una continuación, solo tiene que construir los datos de la primera Requestpara comenzar a ver los efectos secundarios. Se pueden crear efectos secundarios posteriores al evaluar la continuación. ¡Inteligente!
Lasse