Since the early 2000s, the RebindableSyntax
language extension
was our only choice if we wanted to use do-notation with anything
other than instances of the Monad
type class. This design became
particularly unwieldy a few years ago, when we started introducing
linear monads in our experiments with
-XLinearTypes. In this post
we will discuss how QualifiedDo
, a new language extension
in the upcoming 8.12 release of GHC, improves the experience of
writing do-notation with monad-like types.
Do-notation
All Haskellers are accustomed to do-notation. Mark Jones introduced it in the Gofer compiler in 1994, and from there it made it into the Haskell language report in 1996. Since then it has become popular, and for a good reason: it makes easy to read a sequence of statements describing an effectful computation.
f :: T String
f = do h <- openAFile
s <- readTheFile h
sendAMessage s
closeTheFile h
waitForAMessage
One just reads it top-to-bottom. There are no parentheses and no operator precedences to figure out where statements begin and end.
Because monads are the main abstraction for dealing with effectful
computations in Haskell, it made sense to support do-notation for
instances of the Monad
type class, which means that the type T
above needs to have a Monad
instance.
class Applicative m => Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
instance Monad T where
...
Monad-like
In the new century, new ideas appeared about how to represent
effectful programs. A growing collection of monad-like types
accumulated for which the Monad
type class was an inadequate
abstraction. Such has been the case of indexed monads, graded
monads, constrained monads and, lately, linear monads.
In the case of linear monads, the operations use linear arrows,
which prevents us from using them to implement the standard
Monad
type class.
{-# LANGUAGE LinearTypes #-}
module Control.Monad.Linear where
-- constraints elided for the sake of discussion
class (...) => Monad m where
return :: a #-> m a
(>>=) :: m a #-> (a #-> m b) -> m b
(>>) :: Monad m => m () #-> m b #-> m b
m0 >> m1 = m0 >>= \() -> m1
Until now, the way to use do-notation with
not-quite-monads has been to use the RebindableSyntax
language
extension. With RebindableSyntax
, we need only to bring into scope
the operations that do-notation should use.
{-# LANGUAGE RebindableSyntax #-}
import Control.Monad.Linear as Linear
instance Linear.Monad LinearT where
...
f :: LinearT String
f = do h1 <- linearOpenAFile
(h2, Unrestricted s) <- linearReadTheFile h1
linearLiftIO (sendAMessage s)
linearCloseTheFile h2
linearWaitForAMessage
Now the compiler will desugar the do
block as we want.
f :: LinearT String
f = linearOpenAFile Control.Monad.Linear.>>= \h1 ->
linearReadTheFile h1 Control.Monad.Linear.>>= \(h2, Unrestricted s) ->
linearLiftIO (sendAMessage s) Control.Monad.Linear.>>
linearCloseTheFile h2 Control.Monad.Linear.>>
linearWaitForAMessage
RebindableSyntax
, however, has other effects over the whole
module. Does your module have another do
block on a regular monad?
sendAMessage s =
do putStr "Sending message..."
r <- post "https://tweag.io/post" s
if statusCode r == 200
then putStrLn "Done!"
else putStrLn "Request failed!"
The compiler will complain that there is no instance of Linear.Monad IO
.
And indeed, there is not supposed to be. We want the operations of Monad IO
here! But aren’t Prelude.>>
and Prelude.>>=
in scope anymore? No, they’re
not in scope, because RebindableSyntax
also has the effect of not importing
the Prelude
module implicitly.
If function sendAMessage
had a type signature, GHC would complain that
IO
is not in scope.
sendAMessage :: String -> IO ()
It gets worse:
...
if statusCode r == 200
then putStrLn "Done!"
else putStrLn "Request failed!"
...
There would be:
- no
putStrLn
in scope - no
fromInteger
to interpret the literal200
- no
ifThenElse
function that-XRebindableSyntax
mandates to desugar anif
expression
To add insult to injury, in the particular case of linear types, there
is not even a correct way to define an ifThenElse
function to desugar if
expressions. So enabling -XRebindableSyntax
together with
-XLinearTypes
deprives us from if
expressions in linear contexts.
The list of problems does not end here, but the remaining issues are already
illustrated. Each issue has a simple fix, all of which add up to an annoying bunch
the next time we are faced with the decision to introduce RebindableSyntax
or do away with do-notation.
But despair no more, dear reader. The days of agony are a thing of the past.
Qualified do
By enabling the QualifiedDo
language extension, we can qualify the
do
keyword with a module alias to tell which operations to use in
the desugaring.
{-# LANGUAGE QualifiedDo #-}
import qualified Control.Monad.Linear as Linear
instance Linear.Monad LinearT where
...
f :: LinearT String
f = Linear.do -- Desugars to:
h1 <- linearOpenAFile -- Linear.>>=
(h2, Unrestricted s) <- linearReadTheFile h1 -- Linear.>>=
linearLiftIO (sendAMessage s) -- Linear.>>
linearCloseTheFile h2 -- Linear.>>
linearWaitForAMessage -- Linear.>>
sendAMessage :: String -> IO ()
sendAMessage s = do -- Desugars to:
putStr "Sending message..." -- Prelude.>>
r <- post "https://tweag.io/post" s -- Prelude.>>=
if statusCode r == 200
then putStrLn "Done!"
else putStrLn "Something went wrong!"
This has the compiler desugar the Linear.do
block with any
operations Linear.>>=
and Linear.>>
which happen to be in scope.
The net result is that it ends up using Control.Monad.Linear.>>=
and Control.Monad.Linear.>>
. The unqualified do
still uses the
>>=
and >>
operations from the Prelude
, allowing different
desugarings of do-notation in a module with ease.
Is that it? Yes! Really? No extra effects on the module!
One can combine QualifiedDo
with ApplicativeDo
, or RecursiveDo
and
even RebindableSyntax
. Only the qualified do
blocks will be affected.
Every time that a do
block would need to reach out for mfix
, fail
,
<$>
, <*>
, join
, (>>)
or (>>=)
, Linear.do
will use
Linear.mfix
, Linear.fail
, Linear.<$>
, etc.
The proposal
An extension being this short to explain is the merit of more than a year-long GHC proposal to nail each aspect that could be changed in the design.
We had to consider all sorts of things we could possibly use to qualify
a do
block. How about qualifying with a type class name? Or a value
of a type defined with record syntax? Or an expression? And expressions
of what types anyway? And what names should be used for operations in the
desugaring? And should the operations really be imported? And what if
we wanted to pass additional parameters to all the operations introduced
by the desugaring of a given do
block?
There is an answer to each of these questions and more in the proposal. We thank our reviewers, with a special mention to Iavor Diatchki, Joachim Breitner, fellow Tweager Richard Eisenberg, and Simon Peyton Jones, who devoted their energy and insights to refine the design.
What’s next?
Reviewers have suggested during the discussion that other syntactic sugar
could be isolated with qualified syntax as well: list comprehensions, monad
comprehensions, literals, if
expressions or arrow notation are candidates
for this.
For now, though, we are eager to see how much adoption QualifiedDo
has. The
implementation has recently been merged into GHC,
and we will be able to assess
how well it does in practice. The extension is now yours to criticize or improve.
Happy hacking!