Banner

A newbie in Haskell land or another monad tutorial

Posté par alpheccar le29 Nov 2006 à 20:27 CEST

I am a newbie in the Haskell land. I was lost but found some good maps and discovered there is a tradition in Haskell land : writing a monad tutorial.

There are so many monad tutorials that writing a new one is getting difficult. And writing a good one if even more difficult. So, I am just going to explain my own understanding.

The first thing to note is that monads are EASY !!

What's difficult is trying to understand what they have in common because they can look so different. I have identified three kinds of monads (not exclusive - a monad can belong to more than one kind):

  • Monad as control of the sequencing ;
  • Monad as control of side effects ;
  • Monad as container

Monad as control of the sequencing

In a lazy functional programming language like Haskell, the order of evaluation does not matter. It does not mean you cannot control the order of evaluation. It means you can abstract it and build your own sequencing, your own control.

In imperative languages (like C), you need to extend the language to support new control statements.

In less elegant functional languages (like LISP) you need to have special forms which do not follow the normal rules for evaluation.

In Haskell, you "just" build your own control operators. Let's see some examples:

Control in IO monad

Haskell Code by HsColour
repeatN 0 a = return ()
repeatN n a = a >> repeatN (n-1) a

test = repeatN 3 $ do
         putStrLn "TEST"

And, if you want to pass the loop index to the loop body, you may write:

Haskell Code by HsColour
repeatN 0 a = return ()
repeatN n a = (a n) >> repeatN (n-1) a

test = repeatN 3 $ \i -> do
         putStrLn $ "TEST : " ++ (show i)

Indeterminism monad also known as List monad

Another example of control of the sequencing is the indeterminism monad:

Haskell Code by HsColour
import Control.Monad.List

-- f is a function returning several possible results
f :: Int -> [Int]
f x = [1+x,2*x]

test :: IO ()
test = putStrLn . show $ do 
          a <- return 5
          b <- f a
          return b

Here we apply a function f to the value 5. The function f is returning several possible results.

It is possible to chain indeterminate functions like f:

Haskell Code by HsColour
test2 :: IO ()
test2 = putStrLn . show $ do 
          a <- return 5
          b <- f a
          c <- f b
          return c

but we do not need to give a name to the intermediate results, so let's write it like:

Haskell Code by HsColour
test2 :: IO ()
test2 = putStrLn . show $ return 5 >>= f >>= f

The Maybe and Either monads are special cases

Monad as control of side effects

IO Monad

It is the standard example so I won't write about it

Reader monad

A reader monad is used to maintain an environment.

Haskell Code by HsColour
import Control.Monad.Reader


-- The data type for my environment
data MyState = MyState { vara :: Int
                       , varb :: Int
                       } 

-- The initial environment                       
initState = MyState { vara = 10
                    , varb = 20
                    } 

-- Computation in the initial environment                  
test = do theVarA <- asks vara
          lift . putStrLn $ show theVarA
      `runReaderT` initState 

We create a Reader monad to have access to the environment defined by initState. Then in the monad, we can access the fields of initState.

This state is available whenever we need it in the monad and we do not need to pass it as argument.

runReaderT and lift are explained later. They are not important to understand this example. You just have to know that the line with lift is used to display a value and the runReaderT is used to initialize the environment.

Now, we can temporarily change the value of one variable and work in this modified environment.

Haskell Code by HsColour
-- Increment vara from the environment
incrementVarA ::  Int -> MyState -> MyState              
incrementVarA x p = p {vara = (vara p) + x}
                       
test = do theVarA <- asks vara
          lift . putStrLn $ show theVarA
          -- computation in the new modified environment
          local (incrementVarA 5) $ do 
            theVarA <- asks vara
            lift . putStrLn $ show theVarA
          theVarA <- asks vara
          lift . putStrLn $ show theVarA
      `runReaderT` initState 

We have a side effect since the environment is modified and this change is visible in a non local way. But this change is nevertheless restricted by the local function.

The previous examples are in fact using the Reader monad and the IO monad hence the use of the monad transformer ReaderT and runReaderT.

You may use runReader. With runReader the type of test is no more IO () but Int:

Haskell Code by HsColour
test = do theVarA <- asks vara
          return theVarA
      `runReader` initState 

So, an equivalent code (with IO) is:

Haskell Code by HsColour
test = putStrLn . show $ do theVarA <- asks vara
                            return theVarA
      `runReader` initState 

runReader has type : Reader r a -> r -> a

It is applying a Reader monad to an initial environment (r).

runReaderT is just a bit more complex. It has type: ReaderT r m a -> r -> m a

So, when you're working in ReaderT r IO a, you need to specify if you are working with values of type ReaderT r IO a or IO a. The lift function is used for this. Its type is m a -> t m a. So it will transform IO a values to ReaderT r IO a.

A different way to look at this (probably a wrong way) is:

If you have a value v of type a, you use return v to inject it in the ReaderT r IO a monad.

return v would not work if v was of type IO a since you would get a value of type ReaderT r IO (IO a).

So, lift is used to inject the value in the monad.

Monad as container

In each monad, you have the return function which is injecting an element into the monad. So, any monad can be seen as a kind of container. For the List monad it is obvious. Seeing a monad as a container can be very useful.

Assume you want to add an integer to the result of a computation which could return no result. You may have to do something like that

Haskell Code by HsColour
result = Just 20

test = case result of
         Just a -> Just (a + 10)
         _ -> Nothing

So, you need to extract the value from the container (if there is something to extract), apply your function and package the result in the same container.

Or you can just write:

Haskell Code by HsColour
test = (+10) `fmap` result

fmap is a kind of generalization of map. map is lifting a function a -> b to the container [a] -> [b]

fmap is doing the same for a container m (a monad). So, fmap is transforming the type a -> b to m a -> m b

Deriving monad (you have to use -fglasgow-exts)

In a same code you may have to use different Reader monads even if they have the same type since they may be for different uses.

You may create a type synonym :

Haskell Code by HsColour
type MyEnvironment a = Reader Int a -- (here the environment is just an Int)

But it would not prevent from mixing two different Reader monads if they have the same type.

So, you need to create a new type:

Haskell Code by HsColour
newtype MyEnvironment a = MyEnvironment {runMyEnvironment :: Reader Int a}

Then, you want the same behavior. This is just a reader monad (from a behavior point of view) like newtype Meter = Meter Int is just a number (from a behavior point of view).

So, instead of having to write several instance declarations, you just write:

Haskell Code by HsColour
import Control.Monad.Reader
import Control.Monad.Identity

newtype MyEnvironment a = MyEnvironment {runMyEnvironment :: Reader Int a} deriving (Monad, MonadReader Int)

Then you create an environment . It is just a Reader monad contained in your new type

Haskell Code by HsColour
r :: MyEnvironment Int
r = do 
        r <- MyEnvironment $ ask -- This is packaging the result of ask in MyEnvironment. Hence the work
                                 -- is done in the MyEnvironment monad and not in a simple Reader monad
        return r                 -- r is an Int but return r is a MyEnvironment Int and not a
                                 -- Reader Int Int

Then, you extract the reader monad and apply it to the initial state

Haskell Code by HsColour
test = putStrLn . show $ (runMyEnvironment r) `runReader` 4 

What's common ?

What do the previous monads have in common ? Nothing ! Or not a lot. Indeed, being a monad is a very general concept and focusing on the part they have in common (return, >>=) is not the interesting part nor the difficult one. What is interesting is how different they are : a Reader monad is providing ask and local functions ; an IO monad is providing putStrLn etc...

Each monad has its own personality. Of course, >>= will not be the same in each monad but from a user point of view, it will respect the same monadic laws :

Haskell Code by HsColour
return a >>= k  ==  k a                      -- return is a "neutral element" on left
m >>= return  ==  m                          -- return is a "neutral element" on right
m >>= (\x -> k x >>= h)  ==  (m >>= k) >>= h -- a kind of associativity of >>=

The only things shared by all monads : the monadic laws.

Pas de tags

Commentaires

Ajouter un commentaire...

Posté par Pied le03 Déc 2006 à00:40 CEST

My favourite monad tutorial is the wikibooks one with nuclear waste. I find it easy to understand even for people with no functional knowledge.

For those who may know Caml, I like the State monad and I state it that way : " Imagine you have a sum type like this :


type 'a Stupid = S of 'a

And know, all you have is


let return x = S x;;
let (S x : 'a Stupid) (>>=) (f : 'a -> 'b Stupid ) = f x

You have no right to use a destructor to open a Stupid data. Therefore, all data must be passed through the >>= operator. Therefore, all Stupid data passed to a function returns as stupid.

Provided ones codes without side effects, with the type constraint on >>=, this should run in a perfectly monadic fashion, shouldn't it ?

P!...

Good advices

Posté par alpheccar le01 Déc 2006 à18:02 CEST

Thank you for the advices. It looks better. Too many returns in my code.

let

Posté par Cale Gibbard le30 Nov 2006 à23:20 CEST

Note that you can (and generally should!) use a line of the form

Haskell Code by HsColour
let a = 5

in place of

Haskell Code by HsColour
a <- return 5

The 'in' part of the let is taken to be the remainder of the do-block.

For example:

Haskell Code by HsColour
test2 = putStrLn . show $ do { a <- return 5; b <- f a; c <- f b; return c}

becomes

Haskell Code by HsColour
test2 = putStrLn . show $ do { let a = 5; b <- f a; c <- f b; return c}

Of course, you can also pass the value directly:

Haskell Code by HsColour
test2 = putStrLn . show $ do {b <- f 5; c <- f b; return c}

also, if you're just returning the value of the last computation, there's no need to explicitly use return, as that's the usual semantics (you only need return when you're returning something other than what the last computation in the do-block would have returned), so the above can be rewritten again as:

Haskell Code by HsColour
test2 = putStrLn . show $ do {b <- f 5; f b}

Also, putStrLn . show has a name, it's called print, and so:

Haskell Code by HsColour
test2 = print $ do {b <- f 5; f b}

Posté par alpheccar le29 Nov 2006 à22:48 CEST

I discovered this in the lambdabot source code when trying to build it for windows. It is a very useful feature. There are so many hidden gems in Haskell ...

Useful!

Posté par sigfpe le29 Nov 2006 à21:51 CEST

Hey! I can see a bit of Haskell syntax that slipped through my net. I've never used anything like "deriving (Monad, MonadReader Int)". I will now. Very useful. Thanks!