UPDATE 2021-01-02: I have since written a GHC compiler plugin to
implement an alternative ?
-based syntax for early
return. I prefer that one than use of Try.do
, because
it doesn’t require any type magic or special instances, and the
?
is more readable.
UPDATE: I’ve added a follow-up post to this here, where I address some criticisms of this post.
The first half of this post is here. Please read that for context.
One thing that struck me was that our earlier
Make the IO action return Either
approach produced code
that was still perfectly satisfying the unliftio laws. Perhaps, like in
Rust, we need a syntactic solution to the problem.
Enter QualifiedDo,
which will be available on the 9.0.1 version of GHC. What this would
allow us to do is rebind (>>=)
to mean what we’d
like:
module Try ((Try.>>=)) where
(>>=) :: IO (Either e a) -> (a -> IO (Either e b)) -> IO (Either e b)
>>=) m f = do
(<- m
result case result of
Left e -> pure (Left e)
Right a -> f a
We put this in a module called Try
and import it with
QualifiedDo
enabled.
Now our code becomes:
<- constrainRenamed renamed
Try.do constraints <- solveConstraints constraints
solved <- generaliseSolved solved
generalised resolveGeneralised generalised
where each action’s type is
SomeThing -> IO (Either ErrorType OtherThing)
.
Full working example:
{-# LANGUAGE QualifiedDo #-}
import Try
data ErrorType = OhNo deriving (Show)
action1 :: IO (Either ErrorType Int)
= pure (Right 10)
action1 action2 :: Int -> IO (Either ErrorType Int)
= pure (Left OhNo)
action2 x action3 :: Int -> IO (Either ErrorType Int)
= pure (Right (x+30))
action3 x = do
main <-
result
Try.do<- action1
output <- action2 output
output2 <- action3 output2
output3 print result
If you want a final return, you need to wrap it up in
Either
, as:
= do
main <-
result
Try.do<- action1
output <- action2 output
output2 <- action3 output2
output3 pure (Right output3)
print result
Otherwise it won’t match our type:
:16:7: error:
Main.hsCouldn't match type ‘Int’ with ‘Either ErrorType b0’
• Expected: IO (Either ErrorType b0)
Actual: IO Int
People who know a bit of Rust will see this as a familiar pattern;
putting Ok(output3)
at the end of your function.
What did we gain? We can have our cake and eat it too. We get a trivial, syntactically-lightweight, way to string possibly-failing actions together, while retaining all the benefits of being an unliftio-able monad.
Verdict: Best of all worlds.
Unfortunately, it’ll be a while before I’ll be upgrading to this version of GHC, but I look forward to being able to use this time saver.