Introducing Qualified Do/Ado
Possible Readability Issue with Rebindable Do/Ado Notation
When using Rebindable do/ado notation, I'd recommend using the let ... in do/ado
aproach for rebinding function names. Let me give an example why. If we used the 'where' clause approach, it isn't immediately clear whether do/ado notation
desugars to the standard functions or to some remapped version until the very end. For example,
-- Reader thinks, "Oh hey! It's do notation.
-- It's just standard `bind` desugaring."
comp3 :: Box Int
comp3 = do
a <- Box 1
b <- Box 1
c <- Box 1
d <- Box 1
e <- Box 1
f <- Box 1
g <- Box 1
h <- Box 1
i <- Box 1
j <- Box 1
k <- Box 1
l <- Box 1
m <- Box 1
n <- Box 1
o <- Box 1
p <- Box 1
q <- Box 1
r <- Box 1
pure 5
where
someValue = "some really long boilerplate-y string..."
anotherComputation = case _ of
Just x -> Right $ foldl ((:)) Nil x
Nothing -> Left "Not sure what went wrong here..."
-- Reader now thinks, "Oh crap. My understanding is completely off
-- now that I know `bind` really means the below definition..."
bind = -- my custom bind definition...
The above problem can be alleviated by bumping bind
to the top using a let binding.
-- Reader thinks, "Oh hey! It's do notation.
-- It's just standard `bind` desugaring."
comp3 :: Box Int
comp3 = do
-- Reader thinks, "Oh wait. It's using a custom bind definition.
-- I'll need to read through this next part carefully..."
let bind = -- my custom bind definition...
in do
a <- Box 1
b <- Box 1
-- the rest of the code in the example above...
Problems with Rebindable Do/Ado Notation
There are generally two problems with Rebindable do/ado notation.
First, each function that uses this feature must rebind do/ado notation to the correct definition. If one was building a library where each function used this, it would get very tedious.
For example,
comp1 :: Box Int
comp1 = let bind = NormalBind.bind in do
three <- Box 3
Box unit
two <- Box 2
pure (three + two)
comp2 :: Box Int
comp2 = let bind = NormalBind.bind in do
three <- Box 3
pure (three + two)
-- ok, this is really getting tedious...
comp3 :: Box Int
comp3 = let bind = NormalBind.bind in do
three <- Box 3
Box unit
two <- Box 2
pure (three + two)
Second, rebindable do/ado notation might not be easily redable when running computations in various monadic contexts. For example
someComputation :: Box Int
someComputation = let bind = NormalBind.bind in do
-- Box monadic context... use standard bind here
value1 <- takesMonad1Argument (let bind = customBind in do
-- Monad1 monadic context... use custom bind here
value2 <- runMonad1Computation
takesMonad2Argument (let bind = NormalBind.bind in do
-- Monad2 monadic context... use a different custom bind here...
value3 <- runMonad2Computation)
pure (value3 + 5))
pure (value1 + 8)
As can be seen, "rebindable" do/ado notation is good when functions do not use many lines and one is not switching back and forth between monadic contexts.
Still, Qualified Do/Ado helps "solve" each of these problems. What follows is the requirements one needs to implement before this feature will work. In this example, we'll use a more complicated example: IndexedMonad/IxMonad.