haskell-effectful/effectful

Add more docs on reallyUnsafeUnliftIO

schernichkin opened this issue · 4 comments

Please provide more documentation on reallyUnsafeUnliftIO. It used in Effectful.Concurrent.MVar, however in Effectful.FileSystem.IO.File used unsafeSeqUnliftIO. The current description looks scarry - it is quite difficult to cause a segmentation fault in Haskell, even using unsafePerformIO, but the description states that this can happen if computation run "in a thread distinct from the caller of reallyUnsafeUnliftIO".

I think there needs to be more clarity at this point. It would be nice to have an example of incorrect use, an example of safe use and some guidelines when which of these two functions should be used.

Yeah, will do. FWIW I was thinking recently of removing the "segmentation fault" bits from the docs as was unable to think of scenarios in which it would happen. It might've been possible pre 2.0.0.0, but I don't really remember, maybe I just imagined things.

the description states that this can happen if computation run "in a thread distinct from the caller of reallyUnsafeUnliftIO".

Yeah, it amounts to more than one thread using EnvS that reference the same Storage at the same time. If you do this, you will get data races, weird bugs (like local in one thread affecting the return value of ask in the other thread) and perhaps internal consistency check fails. I don't think getting a segfault is currently possible, even if you put some random functions from Effectful.Internal.Env together in a way that doesn't make sense.

Apart from the segmentation fault bit, is there anything in the docs that's unclear? Specifically, in this bit:

Unlifted 'Eff' computations must be run in a way that's perceived as sequential to the outside observer, e.g. in the same thread as the caller of 'reallyUnsafeUnliftIO' or in a worker thread that finishes before another unlifted computation is run.

I think the best way would be to give an example of correct and incorrect usage. If I understand correctly, the root of the problem is that unlift functions ((forall a. m a -> IO a) -> IO b) -> m b may capture m-s underlying state and this can lead to various errors in case of using mutable state. That is why such libraries as unliftio limit m to reader-like things and it's pretty safe regardless of what we do in the unlifted computation.

However, if I understand correctly, this is not generally true for effectful. We can end up with incorrect behavior even all effects are reader-like, so additional measures should be taken. Moreover, effectful libraries trend to reexport functions with callbacks that use unlifting under the hood (e.g. withMVar :: Concurrent :> es => MVar a -> (a -> Eff es b) -> Eff es b allows to pass callback of type (a -> Eff es b)). I'm wondering what the requirements for such callback function are to guarantee correct behavior if any.

I think you're approaching it from a weird angle. First, forget about constraints of unliftio, they aren't relevant here.

I'm wondering what the requirements for such callback function are to guarantee correct behavior if any.

There aren't any.

The public API, unless described otherwise (Effectful.Dispatch.Static.Primitive and Effectful.Dispatch.Static.Unsafe) is safe to use and there are no hidden gotchas (if there are, it's most likely a bug).

For higher order effects (i.e. the ones that execute callbacks), if you use the sequential unlift where the concurrent one is expected, you'll get a descriptive runtime error and can then switch to appropriate concurrent strategy or modify the handler so it works with sequential unlifts.

FWIW, lifted MVar functions use the unsafe variants because realistically the callback will never be executed in a different thread as this would made MVar unusably slow. File operations use the safe variant because while these functions will also most likely never use a different thread to run a callback, in principle they could and the cost of safety check is irrelevant anyway when compared to the cost of opening a file.