Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the difference between `ioToST` and `unsafeIOToST` from GHC.IO

What can the differences and intended uses be for ioToST and unsafeSTToIO defined in 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)
like image 871
imz -- Ivan Zakharyaschev Avatar asked Feb 27 '15 16:02

imz -- Ivan Zakharyaschev


2 Answers

The safe versions must start in the IO monad (because you cannot obtain an ST RealWorld from runST) and allow you to switch between the IO context and a ST RealWorld context. They are safe because ST RealWorld is basically the same thing as IO.

The unsafe versions can start anywhere (because runST can be called anywhere) and allow you to switch between an arbitrary ST monad and the IO monad. Using runST from a pure context and then doing a unsafeIOToST within the state monad is basically equivalent to using unsafePerformIO.

like image 193
Sebastian Redl Avatar answered Oct 20 '22 09:10

Sebastian Redl


TL;DR. All four of these functions are just typecasts. They are all no-op at run-time. The only difference between them is the type signatures — but it's the type signatures that enforce all the safety guarantees in the first place!


The ST monad and the IO monad both give you mutable state.

It is famously impossible to escape the IO monad. [Well, no, you can if you use unsafePerformIO. Don't do that!] Because of this, all the I/O that your program will ever perform gets bundled up into a single giant IO block, thus enforcing a global ordering on the operations. [At least, until you call forkIO, but anyway...]

The reason unsafePerformIO is so damned unsafe is that there is no way to figure out exactly when, if, or how many times the enclosed I/O operations will happen — which is typically a very bad thing.

The ST monad also provides mutable state, but it does have an escape mechanism — the runST function. This lets you turn an impure value into a pure one. But now there is no way to guarantee what order separate ST blocks will run in. In order to prevent complete devastation, we need to ensure that separate ST blocks can't "interfere" with each other.

For that reason, you can't perform any I/O operations in the ST monad. You can access mutable state, but that state isn't allowed to escape the ST block.

The IO monad and the ST monad are actually the same monad. And an IORef is actually an STRef, and so on. So it would really by jolly useful to be able to write code and use it in both monads. And all four of the functions you mention are type-casts that let you do exactly that.

To understand the danger, we need to understand how ST achieves it's little trick. It's all in the phantom s type in the type signatures. To run an ST block, it needs to work for all possible s:

runST :: (forall s. ST s x) -> x

All the mutable stuff has s in the type as well, and by a happy accident, that means that any attempt to return mutable stuff out of the ST monad will be ill-typed. (This is really a bit of a hack, but it works so perfectly...)

At least, it will be ill-typed if you use runST. Notice that ioToST gives you an ST RealWorld x. Roughly speaking, IO x ≈ ST RealWorld x. But runST won't accept that as input. So you can't use runST to run I/O.

The ioToST gives you a type that you can't use with runST. But unsafeIOToST gives you a type that works just fine with runST. At that point, you have basically implemented unsafePerformIO:

unsafePerformIO = runST . ioToST

The unsafeSTToIO allows you to get mutable stuff out of one ST block, and potentially into another:

foobar = do
  v <- unsafeSTToIO (newSTRef 42)
  let w = runST (readSTRef v)
  let x = runST (writeSTRef v 99)
  print w

Wanna take a guess what is going to get printed? Because the thing is, we've got three ST actions here, which can happen in absolutely any order. Will the readSTRef happen before or after the writeSTRef?

[Actually, in this example, the write never happens, because we don't "do" anything with x. But if I pass x to some distant, unrelated part of the code, and that code happens to inspect it, suddenly our I/O operation does something different. Pure code shouldn't be able to affect mutable stuff like that!]


Edit: It appears I was slightly premature. The unsafeSTToIO function allows you to take a mutable value out of the ST monad, but it appears it requires a second call to unsafeSTToIO to put the mutable thing back into the ST monad again. (At that point, both actions are IO actions, so their ordering is guaranteed.)

You could of course mix in some unsafeIOToST as well, but that doesn't really prove that unsafeSTToIO by itself is unsafe:

foobar = do
  v <- unsafeSTToIO (newSTRef 42)
  let w = runST (unsafeIOToST $ unsafeSTToIO $ readSTRef v)
  let x = runST (unsafeIOToST $ unsafeSTToIO $ writeSTRef v 99)
  print w

I've played around with this, and I haven't yet managed to convince the type checker to let me do something provably unsafe using only unsafeSTToIO. I remain convinced it can be done, and the various comments on this question seem to agree, but I can't actually construct an example. You get the idea though; change the types, and your safety gets broken.

like image 38
MathematicalOrchid Avatar answered Oct 20 '22 10:10

MathematicalOrchid