Simpler `liftWith` type?
mitchellwrosen opened this issue · comments
The type is more general in order to allow you to run multiple actions in sequence. E.g. if you have:
m :: m a
k :: a -> m b
then you need to handle both, with the latter fmap
’d into the context resulting from the former:
liftWith $ \ ctx hdl ->
hdl (m <$ ctx) -- ctx is only used to lift the initial action
>>= hdl . fmap k -- subsequent actions must use the derived context
The simpler type signature you suggest makes the correct implementation of this behaviour much harder, requiring a liftWith
call per action, and makes it much easier to accidentally reset the context to its original value at every bind. Edit: Indeed, you might not have any control over the bind at all, because something in another library might be performing it instead.
For this simple example, we could have done hdl ((m >>= k) <$ ctx)
instead, which is indeed amenable to a simpler liftWith
. However, that isn’t generally the case, e.g. if you need to run IO
actions in between m
and k
to e.g. time or log something.
Part of my goal with the design of liftWith
is to make it a little easier to avoid, or at least to notice, the typical MonadBaseControl
problem where you restore the wrong state, likely without even realizing. But without linear types I can’t enforce that you only use the input context once, and even if I could, it would be wrong; consider Control.Exception.catch
which needs to use that context for both the initial action and the error handler, but then use the derived context for the continuation. (Exactly the same thing applies to thread
, incidentally, as can be seen in Control.Effect.Catch
’s Effect
instance.)
Thinking a little further, I don’t want to include another function with the simplified type, since the same argument about the potential for misuse applies.
Therefore, I’m going to close this out for now.