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 a
se 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 condition
sea así False
, la acción action
nunca 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 world
en 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?
when
es tipificable, pero no tiene el tipo que usted declara, y no veo qué hace que este código en particular sea interesante.IO a
se define comoRealWorld -> (a, RealWorld)
, con el fin de hacer que las partes internas de IO sean más legibles.Respuestas:
Esta es una "interpretación" sugerida de la
IO
mónada. Si quiere tomar en serio esta "interpretación", entonces debe tomar en serio "RealWorld". Es irrelevante siaction world
se evalúa especulativamente o no,action
no 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 esworld
. No usamos el nuevo universo que podríamos haber evaluado especulativamente en el lateral. El estado del universo esworld
.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. "
RealWorld
es 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 GHCIO
. 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 asegurarseRealWorld
y 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"
IO
es 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
IO
como 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: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
IO
es un programa trivialReturn
que no hace nada más que devolver un valor, o es una solicitud al sistema operativo con un controlador para la respuesta.OSRequest
puede ser algo como:Del mismo modo,
OSResponse
podría ser algo como:(Una de las mejoras que se pueden hacer es hacer que las cosas sean más seguras para que sepa que no recibirá
OpenSucceeded
unaPutStr
solicitud). Este modeloIO
describe las solicitudes que son interpretadas por algún sistema (para laIO
mó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 sePutStr "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 unIO String
, por ejemplo, es un "contenedor" que "contiene"String
s (y luego<-
los saca). Esta vista de solicitud-respuesta hace que esta perspectiva sea claramente errónea. No hay un identificador de archivo dentro deOpenFile "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
OSRequest
gustaFork :: (OSResponse -> IO ()) -> OSRequest
y 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 laIOSpec
biblioteca .1 Hugs utilizó una implementación basada en la continuación de la
IO
cual 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 = () -> a
aunque()
se llamabaWorld
, pero no está haciendo pasar el estado. JHC y UHC utilizaron básicamente el mismo enfoque que GHC.fuente
OpenFile "foo" (\r -> ...)
realidad debería serRequest (OpenFile "foo") (\r -> ...)
?Request
. Para responder a su primera pregunta, estoIO
es 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 elwhen
ejemplo, no importaría siaction
se evaluara, porque sería un valor como elRequest (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.Request
para comenzar a ver los efectos secundarios. Se pueden crear efectos secundarios posteriores al evaluar la continuación. ¡Inteligente!