haskell - ¿Cuál es la diferencia entre `ioToST` y` insafeIOToST` de GHC.IO
state-monad (2)
Las versiones seguras deben comenzar en la mónada IO (porque no puede obtener un ST RealWorld
desde runST
) y le permiten cambiar entre el contexto IO y un contexto ST RealWorld
. Están seguros porque ST RealWorld
es básicamente lo mismo que IO.
Las versiones inseguras pueden comenzar en cualquier lugar (porque runST
se puede llamar en cualquier lugar) y le permiten cambiar entre una mónada de ST arbitraria y la mónada IO. Utilizar runST
desde un contexto puro y luego hacer unsafeIOToST
dentro de la mónada de estado es básicamente equivalente a usar unsafePerformIO
.
¿Cuáles pueden ser las diferencias y los usos previstos para ioToST
y unsafeSTToIO
definido en GHC.IO ?
-- ---------------------------------------------------------------------------
-- Coercions between IO and ST
-- | A monad transformer embedding strict state transformers in the ''IO''
-- monad. The ''RealWorld'' parameter indicates that the internal state
-- used by the ''ST'' computation is a special one supplied by the ''IO''
-- monad, and thus distinct from those used by invocations of ''runST''.
stToIO :: ST RealWorld a -> IO a
stToIO (ST m) = IO m
ioToST :: IO a -> ST RealWorld a
ioToST (IO m) = (ST m)
-- This relies on IO and ST having the same representation modulo the
-- constraint on the type of the state
--
unsafeIOToST :: IO a -> ST s a
unsafeIOToST (IO io) = ST $ / s -> (unsafeCoerce# io) s
unsafeSTToIO :: ST s a -> IO a
unsafeSTToIO (ST m) = IO (unsafeCoerce# m)
TL; DR . Las cuatro de estas funciones son solo typecasts. Todos son no operativos en el tiempo de ejecución. La única diferencia entre ellos son las firmas de tipo, ¡pero son las firmas de tipo las que hacen cumplir todas las garantías de seguridad en primer lugar!
La mónada ST
y la mónada IO
dan un estado mutable.
Es famoso que es imposible escapar de la mónada IO
. [Bueno, no, puedes si usas unsafePerformIO
. ¡No hagas eso!] Debido a esto, todas las E / S que ejecutará tu programa se agrupan en un solo bloque IO
gigante, lo que impone un ordenamiento global de las operaciones. [Al menos, hasta que llames a forkIO
, pero de todos modos ...]
La razón por la que unsafePerformIO
es tan unsafePerformIO
inseguro es que no hay forma de averiguar exactamente cuándo, si o cuántas veces ocurrirán las operaciones de E / S adjuntas, lo que generalmente es algo muy malo .
La mónada ST
también proporciona un estado mutable, pero tiene un mecanismo de escape: la función runST
. Esto le permite convertir un valor impuro en uno puro. Pero ahora no hay forma de garantizar el orden en que se ejecutarán los bloques ST
separados. Para evitar una devastación total, debemos asegurarnos de que los bloques ST
separados no puedan "interferir" entre sí.
Por esa razón, no puede realizar ninguna operación de E / S en la mónada ST
. Puede acceder al estado mutable, pero ese estado no puede escapar del bloque ST
.
La mónada IO
y la mónada ST
son en realidad la misma mónada . Y un IORef
es en realidad un STRef
, y así sucesivamente. Por lo tanto, sería realmente útil poder escribir código y usarlo en ambas mónadas. Y las cuatro funciones que menciona son tipos que le permiten hacer exactamente eso.
Para entender el peligro, necesitamos entender cómo ST
logra es un pequeño truco. Todo está en el tipo fantasma en las firmas de tipo. Para ejecutar un bloque ST
, debe funcionar para todas las posibles s
:
runST :: (forall s. ST s x) -> x
Todas las cosas mutables tienen s
en el tipo también, y por un accidente feliz, eso significa que cualquier intento de devolver cosas mutables de la mónada ST
estará mal escrito. (Esto es realmente un truco, pero funciona tan bien ...)
Al menos, estará mal escrito si usa runST
. Tenga en cuenta que ioToST
le da un ST RealWorld x
. En términos generales, IO x
& approx; ST RealWorld x
. Pero runST
no lo aceptará como entrada. Entonces no puedes usar runST
para ejecutar I / O.
El ioToST
le proporciona un tipo que no puede usar con runST
. Pero unsafeIOToST
le da un tipo que funciona bien con runST
. En ese punto, has implementado básicamente unsafePerformIO
:
unsafePerformIO = runST . ioToST
El unsafeSTToIO
permite obtener elementos mutables de un bloque ST
, y potencialmente a otro:
foobar = do
v <- unsafeSTToIO (newSTRef 42)
let w = runST (readSTRef v)
let x = runST (writeSTRef v 99)
print w
¿Quieres adivinar qué se va a imprimir? Porque el caso es que tenemos tres acciones de ST
aquí, que pueden suceder en cualquier orden. ¿La readSTRef
ocurrirá antes o después de writeSTRef
?
[En realidad, en este ejemplo, la escritura nunca ocurre, porque no "hacemos" nada con x
. Pero si transfiero x
a una parte del código distante y no relacionada, y ese código pasa a inspeccionarlo, de repente nuestra operación de E / S hace algo diferente. ¡El código puro no debería poder afectar cosas mutables como esa!]
Editar: Parece que fui un poco prematuro. La función unsafeSTToIO
permite tomar un valor mutable de la mónada ST
, pero parece que requiere una segunda llamada para unsafeSTToIO
para volver a poner la cosa mutable en la mónada ST
. (En ese punto, ambas acciones son acciones IO
, por lo que su orden está garantizada).
Por supuesto, también podría mezclar algo unsafeIOToST
con unsafeIOToST
, pero eso realmente no prueba que unsafeSTToIO
por sí mismo no es seguro:
foobar = do
v <- unsafeSTToIO (newSTRef 42)
let w = runST (unsafeIOToST $ unsafeSTToIO $ readSTRef v)
let x = runST (unsafeIOToST $ unsafeSTToIO $ writeSTRef v 99)
print w
He jugado con esto, y todavía no he logrado convencer al verificador de tipos para que me permita hacer algo probablemente inseguro usando soloSTTOIO inseguro. Sigo convencido de que se puede hacer, y los diversos comentarios sobre esta cuestión parecen estar de acuerdo, pero en realidad no puedo construir un ejemplo. Aunque tienes la idea; cambie los tipos, y su seguridad se rompe.