Monadic “do” block, yet again

What should get into your “ do" block?

(This article has undergone some reviews by quite some people via this reddit post. Thanks to all those there to have helped me get my personal best article so far on Haskell!)

Whether you have hit this article, either by chance or by intention, one thing is clear: you know atleast an overview of what a “monad” is, if not mastered them.

Most tutorials or books that introduce monads explains use of the “bind” operator (>>=) and the sequencing operator (>>) and then introduce do as a syntactic sugar for both of these.

Although the intentions of this ordering of explanations is good, and that is the best way to introduce do syntactic sugar, I had a problem with that: the real-world code always is seen with do blocks wherever possible. When I read such code, I seem to understand the code well, but when I write my own do blocks, I get into trouble. Simply because the concepts of do were not so clear to me — and for those of you who hit this problem, we’ll try to demystify do here.

Your function’s type defines also the type of action in the do block

Well, it must be ensured that every action within the do block is of the same type of the function itself: only that, it can be complied to its “generic” form. To put this in a perspective:

IO String is a concrete form, IO a is its generic form

is a concrete form, is its generic form Same with IO () , which is the concrete form of the generic IO a

, which is the concrete form of the generic Maybe Int is the concrete form, Maybe a is its generic form

is the concrete form, is its generic form [Char] is the concrete form, while [a] is its generic form

is the concrete form, while is its generic form and so on…

This is not a rocket-science finding: if you just look at function definitions that start with do , such as the main function, it must be clear that do happens to be only encompassing statement in that function, and so must have the same type as the function itself.

The fact that each action must be of the same type of the function is also not a surprise, considering that do is a syntactic sugar for (>>) and (>>=) — we’ll come back to this later.

Explaining “Actions”

Taking a slight detour, we’ll try to understand what an “action” means. Expressions that are to be evaluated to get a result is an “action”. For example, an expression that contains a function: the result of which is obtained only when the function executes. When an expression, such as a let binding, which just “binds” a name to an expression is used — it is not an action. Even if binding happens to be to an expression that contains a function. Examples:

Example-1 (putStrLn :: String->IO (), generic: IO a):

λ> putStrLn "Hello world!" -- Is an IO action, which is executed

Hello world! Example-2 (getLine :: IO String, generic: IO a):

λ> let str = getLine -- Is just a let-binding to an action

-- i.e., an action is bound to a name

-- "str", which can execute.

λ> str -- "str" can be executed

test -- Input we gave "getLine"

"test" -- Results of action automatically printed by ghci Example-3 (getLine :: IO String, generic: IO a):

λ> res <- getLine -- Is an action, which is executed.

test -- results of which is bound to the

-- variable "res". Here, "test" is the

-- input we gave getLine

λ> res -- Value of "res" being printed

"test" Example-4 (Just 12 :: Maybe Int, generic: Maybe a):

val <- Just 12 -- Is an action, where the Just data

-- constructor is executed, and the

-- result (i.e., value contained by

-- this Maybe type, i.e., 12) is

-- bound to the variable val.

Examples above must have clarified what an action is. The last example is just to also bring to your notice that data constructors are also functions that are executed, and so, are actions too.

Before we move on, here’s a summary of what an action is and what is not:

Expressions that need to be evaluated to get the results are actions. These are mainly functions to be executed.

Binding of a value extracted from a monad using <- triggers an action. If you understand that a partially applied function (i.e., a function that is awaiting an argument) is also a monad, the value of which is the result of its execution, then, the example-3 makes sense for you. Example-4 gives a case when a value from a Maybe is extracted using <- .

triggers an action. If you understand that a partially applied function (i.e., a function that is awaiting an argument) is also a monad, the value of which is the result of its execution, then, the example-3 makes sense for you. Example-4 gives a case when a value from a Maybe is extracted using . The type of the action must comply to the generic forms of the type of the do block: IO a , Maybe a , [a] , etc.

block: , , , etc. A let -binding is NOT an action. It just binds an expression to a name, and the name will have to be evaluated later to trigger an action. It should be noted that a let is not specific for monadic code: it is simply a binding for associating a name to any type, and in non-monadic contexts, is used in the construct such as “ let…in ”.

Back to the Monadic do

Let us start from the main function:

main :: IO ()

main = do

...

The type of the function main is IO () , generic form being IO a . This means, every action within main should be of type IO () . So, here are some actions that will work and some that will not:

main :: IO ()

main = do

let s1 = "Hello " -- Ok: let-binding just binds s1 to "Hello" s2 <- getLine -- Ok: type of getLine is "IO String", its

-- generic type being "IO a". Since

-- the generic type of main is also

-- "IO a", this works. val <- Just 12 -- Not Ok: The type of "Just 12" is

-- "Maybe Int", generic form being

-- "Maybe a". This is incompatible

-- with the generic type of main. let val' = Just 12 -- Ok: Although we have a Maybe Int value,

-- we are only binding a name to this

-- expression using let. It must be

-- remembered however that we have bound

-- val' to the complete Maybe monad, and

-- and not to the value 12 alone. To get

-- that value out, we must do something

-- else (see next section below) putStrLn (s1 ++ s2) -- Ok: putStrLn :: IO (), generic: IO a.

-- Since this is compatible with generic

-- type of main, we are good to go.

The above examples give a general idea of what can get into a do block, and what cannot. The statement “ val <- Just 12 ”, although does not work here, does bring something to mind: how then, can we mix various monadic types? Since Haskell code always starts from the main , and the main ’s type is always IO () , it must obviously be possible to bring in values from other monads in main — otherwise, there is little reason to have other monads. Which brings us to the next topic.

Bringing in values out of other monads: mixing monads

While using <- to extract values out of a monadic context is the easiest way, unfortunately, this does not help if we have a monad that is not an IO a inside main . Of course, for the same reason, it is not possible to have an IO a inside a do block of another type of monad such as Maybe a (i.e., a function of type Maybe a having a do block).

What we then have to do is to manually pull out the values we want from their monadic contexts. For example, a function such as maybeval below could be used to get a value from a Maybe monad:

-- Pull out value of Maybe. We use "0" as a default value

-- if we get a Nothing in the maybe.

maybeval :: Maybe String -> String

maybeval (Just x) = x

maybeval Nothing = "0"

If you want to be a little fancy, the below code also does the same:

-- Pull out value of Maybe. We use "0" as a default value

-- if we get a Nothing in the maybe.

maybeval' :: Maybe String -> String

maybeval' = maybe "0" id

Now, using this in a main ’s do block is easy:

main :: IO ()

main = do

let ms = Just "Hello World" -- Ok: A let-bind is ok within IO a

putStrLn $ maybeval ms -- Ok: We first extract the value

-- from the Maybe monad (ms)

-- using the function maybeval

-- we wrote before, and then

-- putStrLn (of type IO String)

-- to display this value

Again, to summarise what we’ve learnt here, to mix monadic values:

Write a function that manually “extracts” a value from a monadic context

Use this function in another monad’s do block to get the value out

Obviously, this is a small pain to take considering the fact that Haskell is a statically typed language, which makes our life much better!

Finally, we explain why of all this

Almost all Haskell books (rightfully) start from this explanation, we just put that in the tail for completeness.

As mentioned before, >> and >>= are the operators (functions) for which do is a syntactic sugar. If we understand the types of these functions, we understand why we need to ensure that the lines of do are to be of the same type.

λ> :t (>>)

(>>) :: Monad m => m a -> m b -> m b

λ> :t (>>=)

(>>=) :: Monad m => m a -> (a -> m b) -> m b

We notice above that the types of both these functions (i.e, their outputs) are m b . This means, when these are used in a sequence (as they are normally used), their outputs must be of the same type. Now, lets get to an example. These two are equivalent:

Printing “Hello” and “world” with >> :

λ> putStrLn "Hello " >> putStrLn "world!"

Hello

world!

Printing and equivalent with do , which is just a syntactic sugar. Note that in order to experiment directly on GHCi, we need to use :{ and :} to make sure that GHCi allows for multi-line expression such as do :

λ> :{

λ| do

λ| putStrLn "Hello"

λ| putStrLn "world"

λ| :}

Hello

world

Just so that we see this clearly, here’s only the do block:

do

putStrLn "Hello"

putStrLn "world"

Basically, we notice that each of the parts of >> are of the type IO () — i.e., the output type m b is IO () . A code such as this will not work:

λ> Just 12 >> putStrLn "Hello" <interactive>:10:12: error:

* Couldn't match type `IO' with `Maybe'

Expected type: Maybe ()

Actual type: IO ()

* In the second argument of `(>>)', namely `putStrLn "Hello"'

In the expression: Just 12 >> putStrLn "Hello"

In an equation for `it': it = Just 12 >> putStrLn "Hello"

This did not work because the constituent parts Just 12 and putStrLn are of different types. And this will work because its constituent parts of the same type Maybe Int .

λ> Just 12 >> Just 15

Just 15

Going to their equivalent in do blocks, this will not work:

do

Just 12 -- Type: Maybe Int

putStrLn "Hello" -- Type: String -> IO ()

This will work:

do

Just 12 -- Type: Maybe Int

Just 15 -- Type: Maybe Int

Now, lets turn our attention to the operator >>= : without much explanation, here are the equivalents of what will work and what will not:

These are equivalents, and will not work:

λ> getLine >>= Just <interactive>:13:13: error:

* Couldn't match type `Maybe' with `IO'

Expected type: String -> IO String

Actual type: String -> Maybe String

* In the second argument of `(>>=)', namely `Just'

In the expression: getLine >>= Just

In an equation for `it': it = getLine >>= Just

Equivalent, with do block, will not work:

do

s <- getLine -- Type: IO String

Just s -- Type: Maybe String

These are equivalents, and will work:

λ> getLine >>= putStrLn

Hello

Hello

Here’s the equivalent with the do block, which will work:

do

s <- getLine -- Type: IO String, generic IO a

putStrLn s -- Type: IO (), generic IO a

Ok, now we are experts in this, and lets go to…

One code to rule it all!

Ok, we’ve learnt so much so far. Here’s a code that puts all of this in one show. As a bonus, the code also has an example for usage of a Monad transformer: MaybeT IO String . Here goes, and follow the comments to understand.

module Main where import Control.Monad.Trans.Maybe main :: IO ()

main = do

-- We use "let n1 = giveVal 3" here because

-- we are running in an IO monad (i.e., the return

-- type of main happens to be "IO a" - 'a' here is just

-- fixed to be unit ()). Every expression in a monadic

-- "do" block should have the type of the monad that it

-- is running in: in this case, must be "IO a".

-- Since "giveVal n" is of type "Maybe String", we cannot

-- use "<-" to "retrieve" value from within Maybe returned

-- by giveVal. Instead, we use the "let" syntax to just assign

-- the maybe value to n1 and n2, and manually pull out

-- the String values using a function maybeval.

-- Both s1 and s2 are of type "Maybe String" because that is

-- the type of the function "giveVal" let s1 = giveVal "3" -- Ok: Binding within monadic code

let s2 = giveVal "6" -- Ok: Binding within monadic code -- getIfString is of type MaybeT IO String. To understand what is

-- happening here, we will break this type into its constituent

-- type. i.e., MaybeT IO String = MaybeT IO (Maybe String).

-- Doing a <- from a monad "pulls out" the value from its context.

-- In this case, the overall context of MaybeT is an IO context,

-- and hence the pulled-out value (here s3) is a Maybe String.

-- To get the actual String value contained within this maybe value,

-- we need to pass it to the function maybeval, as is done with s1

-- and s2 too.

s3 <- runMaybeT $ getIfString

putStrLn $ maybeval s1

++ maybeval s2

++ maybeval s3 -- This function has a Maybe Int as the return value, and hence

-- any "do" monadic code within this function should also

-- have Maybe as the type.

giveVal :: String -> Maybe String

giveVal s = do

s1 <- createVal s -- Ok: createVal has Maybe Int

-- Value returned by createVal is

-- already automatically "pulled out".

let s2 = createVal s -- Ok: let-binding within monadic code

-- But s2 is now a Maybe type, and hence

-- value needs to be "pulled out"

let s2' = maybeval s2 -- Additional step needed to "pull out"

-- value from s2

return (s1 ++ s2') -- Creates a Maybe String from a String

createVal :: String -> Maybe String

createVal s = Just s -- Pull out value of Maybe. We use '0' as a default value

-- if we get a Nothing in the maybe.

maybeval :: Maybe String -> String

maybeval = maybe "0" id -- Here, we experiment with a monadic transformer, MaybeT IO String.

-- Since getIfString is a function of this type, it expects

-- every line to be of this type. We construct this type in every

-- line below.

getIfString :: MaybeT IO String

getIfString = do

-- Since MaybeT value is constructed as follows:

--

-- newtype MaybeT m a {

-- runMaybeT :: m (Maybe a)

-- }

--

-- in our case, we need a "MaybeT IO String" constructed as

-- follows: MaybeT IO (Maybe String)

-- Going from the last:

--

-- λ> :t getLine

-- getLine :: IO String

--

-- We have a "String" in an IO monad to get a "IO String". To

-- insert a "Maybe" between "IO" and "String", we just map a

-- "Just" to this value. This mapping is done by <$> which is

-- nothing but an alias for "fmap"

-- Hence, we have:

--

-- λ> :t Just <$> getLine

-- Just <$> getLine :: IO (Maybe String)

--

-- Finally, we just construct this MaybeT by applyingthe

-- Data constructor MaybeT to the whole thing. We now get:

--

-- λ> :t MaybeT $ Just <$> getLine

-- MaybeT $ Just <$> getLine :: MaybeT IO String

--

-- which is what should be the type of each line within this

-- function.

MaybeT $ Just <$> getLine

Hmm… quite a code there! Feel free to comment below if you want to know more, or, generally on how you liked (or disliked) this article!!

And, don’t forget to CLAP!