Now that we have seen some elementary examples of Monads, it is the best time to introduce you to the do notation in Haskell.
One of my tweets reads: “In Haskell, the do notation is syntactic sugar for monadic operations.” What did you say? You don’t follow me on Twitter? Tsk, tsk 🙂
Well, anyway, what does this statement mean? Fortunately, it is easy to explain its meaning. I will begin by explaining what “syntactic sugar” is. “Syntactic sugar” is a way to syntax, to write something such that it makes it more elegant, obvious, or neat, without adding to it any more detail or information.
Did you like my definition of the term “syntactic sugar”? Doesn’t it seem to have come out of a well-respected dictionary? Well, it has come out of my brain, just now, for the purpose of this blog post. So you might want to search the Internet for a better definition.
Thus, the do notation is a notation that is used in Haskell in order to clarify the writing of monadic operations. But what are “monadic operations”? Well, to put it simply, it is the use of the bind operator (>>=). Remember that we define and use the bind operator for each Monadic instance, so that we can compose functions of type a -> M b, where M is a type constructor/Monad.
The following program uses a trivially simple type constructor, the one I introduced in my previous blog post, in order to explain the do notation. I just elevate the type constructor to a Monad, without first elevating to a Functor/Applicative, in order to show only the concepts of the do notation and nothing more.
module Main where data OurTC a = OurType a deriving (Show) instance Monad OurTC where return = OurType OurType x >>= f = f x main :: IO () main = do putStrLn "Program begins." print (myFunction1 100) print (myFunction1' 100) print (myFunction2 100) print (myFunction2' 100) putStrLn "Program ends." myFunction1 :: Int -> OurTC Int myFunction1 x = (OurType x) >>= (\x -> OurType (2*x)) >>= (\x -> OurType (x+1)) myFunction1' :: Int -> OurTC Int myFunction1' x = do a <- (OurType x) b <- (\x -> OurType (2*x)) a c <- (\x -> OurType (x+1)) b return c myFunction2 :: Int -> OurTC [Int] myFunction2 x = (OurType x) >>= (\x -> OurType [x, 2*x, 3*x]) >>= (\xs -> OurType (reverse xs)) >>= (\xs -> OurType (map (+1) xs)) myFunction2' :: Int -> OurTC [Int] myFunction2' x = do a <- (OurType x) b <- (\x -> OurType [x, 2*x, 3*x]) a c <- (\xs -> OurType (reverse xs)) b d <- (\xs -> OurType (map (+1) xs)) c return d
The output of the previous program is:
Program begins. OurType 201 OurType 201 OurType [301,201,101] OurType [301,201,101] Program ends.
In the previous program, myFunction1 and myFunction1′ are equivalent. The do exactly the same thing, which is to compose a few simpler functions of type a -> OurTC b, where OurTC is a type constructor/Monad and a and b can be any Haskell types. myFunction1 uses the bind operator (>>=) to compose these functions, whereas myFunction1′ uses the do notation and the “variables” a, b, and c to pass the output of one function into the next function.
Similarly, in the previous program, myFunction2 and myFunction2′ are equivalent. myFunction2 uses the bind operator, whereas myFunction2′ uses do notation.
As you can see, there is also a do block in the main program. In that do block, we should imagine that each statement is bound to the next by the >> operator, instead of the >>= operator. The use of the >> operator means that each statement is executed without passing anything to the next statement.
Most people find the do notation simpler. Also the do notation produces programs with more lines, but also lines of smaller length. This is important. In the previous program, if we keep composing functions, myFunction1 and myFunction2 will become a really really long line, whereas myFunction1′ and myFunction2′ will have many lines but will keep the length of each line to a very small length. The same applies to the do block in the main program.
You can use whichever notation you like and you can also mix and match.