Monad

Welcome back to the final lesson in the Beginner Crash Course!

We’ll pick up where we left off with the code that we wrote in the last lesson on Functor and also the rejectNonalphabetic function that we wrote a few lessons ago when we discussed folds. (We have replaced myAll with the regular Prelude function all.)

import Data.Char

database :: [(Integer, String)]
database = [(1, "Julie"),
            (2, "Chris"),
            (3, "Alonzo"),
            (4, "Melman")]

greetUser :: Integer -> Maybe String
greetUser record =
    fmap ("Hello, " ++) (lookup record database)

rejectNonalphabetic :: String -> Maybe String
rejectNonalphabetic string =
    case (all isAlpha string) of
        False -> Nothing
        True -> Just string

We wrote this rejectNonalphabetic function enforce a rule on some input – let’s say it’s username that someone entered while trying to sign up for our wonderful website – to ensure that the string contains only letters. If our input contains any other kind of character we don’t like, we’ll turn the whole thing into Nothing.

More validation functions

Let’s say we have another one that removes spaces, without rejecting the input. We’ll assume that if the user typed in any spaces, then it was just a mistake, so we can pull out the spaces. But we do want to reject the input if there’s nothing left once the spaces are gone. This function will have the same type: String to Maybe String.

removeSpaces :: String -> Maybe String
removeSpaces string =
    case (filter (\x -> not (x == ' ')) string) of
        "" -> Nothing
        result -> Just result

We start with a filter to remove spaces, and put that into a case expression to further inspect the results after the filtering. If we’re left with the empty string, we’ll return a Nothing; if we’re left with any other result, then we’ll return Just that result.

Finally, we could add a check to make sure usernames aren’t unreasonably long.

validateLength :: String -> Maybe String
validateLength string =
    case (length string > 25) of
        True -> Nothing
        False -> Just string

If the number of characters in the string is more than 25, we’ll call that too long. Otherwise, we’ll return just the string.

Composition

So now we have three rules that we want our inputs to pass. We’re going to reject anything nonalphabetic, remove spaces, and check the length. What we need to do is compose these three rules so that they are all applied the input string, and a failure at any of the three steps gives us a failure of the overall process.

The order here is going to matter, for a few reasons.

  • Spaces are not alphabetic characters, so we want to remove those before we reject nonalphabetic characters. If the input has some combination of spaces and alphabetic characters, we want to consider that valid input.
  • Space removal might bring a long string under the 25-character limit, so we’ll also want it to come before the length check.

We could write one long function that nests all the various case expressions that we’re using:

makeUsername :: String -> Maybe String
makeUsername string =
    case removeSpaces string of
        Nothing -> Nothing
        Just xs ->
            case (rejectNonalphabetic xs) of
                Nothing -> Nothing
                Just xs' ->
                    ...

This is valid Haskell, but these can get quite long and hard to read and think about, especially if we need to add more steps later (or remove some). And you sometimes have to rename arguments to avoid shadowing (that’s what xs and xs' are: new names).

We might initially be tempted to try just composing them in some way, perhaps:

makeUsername :: String -> Maybe String
makeUsername string =
    validateLength (rejectNonalphabetic (removeSpaces string))

Unfortunately, the compiler will reject this and chastise you with a big intimidating type error. Each one of these functions returns Maybe String. We want to be able to pass the String output as the input to the next rule, as if the Maybe weren’t there.

You may remember that we did something like this earlier using the <- arrow with do syntax before when we wrote our first main action.

main :: IO ()
main =
  do
    word <- getLine
    print (isPalindrome word)

When we had an IO String action from getLine and what we wanted was the String that the action produces, this <- form allowed us to sort of “get the String out of the IO String”. But Haskell also has an operator that does this. It is so important and beloved that it is, in fact, part of the Haskell logo.

In a bind

This is called the bind operator: (>>=).There is also the Kleisli composition operator (>=>) that you could use equally well for this example, but we will focus on (>>=).

We will discuss the type in a moment, but we’ll start by showing how it’s used. Here is the concise way to write makeUsername instead of the big nested case expression above:

makeUsername :: String -> Maybe String
makeUsername string =
    removeSpaces string >>= rejectNonalphabetic >>= validateLength

The result from removeSpaces is going to effect the whole rest of the computation, just like the case expressions. The first time we hit a Nothing, then the whole evaluation is going to stop. But if it’s a Just, then the string will get passed as the input to the next function. In this way, we can keep passing Strings out of Maybe Strings into the next function – almost like magic. But it’s not magic.

The type signature of bind looks like this:

m a -> (a -> m b) -> m b

In the context of our makeUsername function, m is Maybe. The first argument (Maybe a) is the output from applying a validation rule, and the second argument (a -> Maybe b) is the next validation rule to feed the result into.

Let’s look at the type of this operator in GHCi to make sure we got it exactly right. To query the type of an infix operator, you need to write it with parentheses around it:

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

In the previous lesson we saw the type of fmap written using a type variable f, where the letter F was chosen as an abbreviation for Functor. Here the letter m was chosen because this type contructor must be a Monad (Maybe is one such type constructor).

We are going to use a language extension called type applicationsWe use the :set GHCi command here to enable this language extension. Type applications is a very nice feature, and we have it enabled by default in our GHCi configuration because we use it in the REPL often. that allows us to see the result of applying a function to a type. First we’ll query for the type of (>>=) applied to Maybe:

λ> :set -XTypeApplications

λ> :type (>>=) @Maybe
(>>=) @Maybe :: Maybe a -> (a -> Maybe b) -> Maybe b

The type parameter m is allowed to turn into Maybe because Maybe belongs to the Monad class. You can check to see if some of your favorite type constructors are also monads. Recall that list is a Functor; does it also belong to the class of Monad? Try applying (>>=) to [] to find out:

λ> :type (>>=) @[]
(>>=) @[] :: [a] -> (a -> [b]) -> [b]

It is!

Let’s ask GHCi what it knows about Monad.

λ> :info Monad
class Applicative m => Monad (m :: * -> *) where
  (>>=) :: m a -> (a -> m b) -> m b
  (>>) :: m a -> m b -> m b
  return :: a -> m a
  {-# MINIMAL (>>=) #-}
        -- Defined in ‘GHC.Base’
instance Monad (Either e) -- Defined in ‘Data.Either’
instance Monad [] -- Defined in ‘GHC.Base’
instance Monad Maybe -- Defined in ‘GHC.Base’
instance Monad IO -- Defined in ‘GHC.Base’
instance Monad ((->) r) -- Defined in ‘GHC.Base’
instance Monoid a => Monad ((,) a) -- Defined in ‘GHC.Base’

This class has the constraint Applicative m =>Applicative is a lovely class which unfortunately did not fit into the time alotted for this course. We treat it extensively in Functortown. – a constraint on a class is called a superclass. This means that every m that belongs Monad class must also belong to the Applicative class.

Like Functor, the kind signature is (* -> *) – So, Monad and Functor are classes for the same kindedness of things: type constructors that have exactly one type parameter.

The type signature for bind (>>=) tells us that if m is a monad, then there is a function m a -> (a -> m b) -> m b. So what does it mean for a type m to be a monad? We’ve been talking a lot about what typeclasses mean and their relationship with type; a class is something that many types have in common. There are many types that support this bind operation; if a type does, then it belongs to the Monad class.

You’ll sometimes hear us speak of something as being “in a monad”. In this program, we have a lot of Maybe String values, so we have Strings in the Maybe monad.

Lookup, then validate

Let’s see, as a final move, if we can bring together our greetUser function and our database with our validation function that we have now written. Let’s say when we greet a user that has logged into our website, we will validate the name before we greet them, and we won’t greet them if they have broken our rules.

(lookup record database) will find us, maybe, a name from the database. So what we want to do is take that String (if it found one) and pass it as the argument for makeUsername. How do we do that? We bind them.

greetUser :: Integer -> Maybe String
greetUser record =
    fmap ("Hello, " ++) (lookup record database >>= makeUsername)

If you have a series of computations that should be performed sequentially, such that each new one depends on the successful result of the one before it, then you may want to use >>= to chain them together. You can do this if they are wrapped up in a Monad, like Maybe.

Let’s modify the database so that it includes an invalid name.

database :: [(Integer, String)]
database = [(1, "J00li3"),
            (2, "Chris"),
            (3, "Alonzo"),
            (4, "Melman")]

Now we can :reload and try it out.

λ> :reload

λ> greetUser 1
Nothing

User number 1 exists, but her name does not pass the rejectNonalphabetic rule. But user number 3 is a very good dog:

λ> greetUser 3
Just "Hello, Alonzo"

If there is no record, what will happen?

λ> greetUser 5
Nothing

Nothing! Nothing will happen.

If we care to distinguish between the multiple causes of failureTo explore this path, continue on to The Validation Course, which addresses the Either and Validation types. – where the record does not exist, versus when it exists but is invalid – then we would want to use a more exciting type than Maybe.

How do you do

Since you generally want side effects to be sequenced, and we use the IO type constructor for side-effecting code, IO – the obligatory type constructor for the main action – is a canonical Monad. When we first wrote out the main for our interactive palindrome code some time ago, it looked like this:

main :: IO ()
main =
  do
    word <- getLine
    print (isPalindrome word)

isPalindrome :: String -> Bool
isPalindrome string = string == reverse string

Just about any time you see do,(Sometimes there’s an Applicative context, using the ApplicativeDo language extension.) what’s going on there is that there’s a monadic context. In the case of main, that context is IO. What’s really going on here just under the surface is that the compiler is inserting a bind between each line within the do block.

If we prefer to write main using >>= instead of in the do form, it could look like this:

main :: IO ()
main =
    getLine >>= (print . isPalindrome)

One reason we often hesitate to include Monad in beginner content is that there are a lot of people on the internet who do not really appear to be altogether interested in learning Haskell but really want to know what a monad is, and it feels like monads have taken on a sort of outsized mythical status. But the Monad class is a fairly small thing, once you understand Haskell’s larger system of types, classes, and functions that it fits into.

This is a very common programming task, chaining together a series of functions in some particular way; Monad and the >>= operator is a way of simplifying that. In some ways it may not seem like a simplification when it is new to you, but by giving us certain intuitions about how the pattern should behave, and by being highly composable, the monad idea does remove some complexity – as a good abstraction should.

Everything we do in Haskell, even I/O, can be done without the Monad generalization, but not quite as easily or consistently. Many Haskell learners, ourselves included, have built this concept up in our minds as a huge thing, and it felt a bit anticlimactic to find out that a shared pattern of this (>>=) function is all it is. Instead of nesting case expressions or something like that, we chain stuff together with an infix operator.

Where to go from here

That concludes this beginner course. There is, of course, much more to be said about Haskell! Our goal here has been to stick with a few key ideas and repeat some important idiomatic syntax, to give you a stepping stone to go on to learn however much Haskell in the future you would like to learn.

If you’ve enjoyed this introduction, please consider looking at our other courses. Here are a few recommended followups to The Beginner Crash Course:

  • The Validation Course works through a single example in depth. It contains a little review of what you have learned in this course and covers Either, Validation, and Applicative (and more!).
  • The Haskell Phrasebook is a collection of short annotated Haskell programs. This is a good place to look if you are eager to get some quick exposure to what software written in Haskell can look like.
  • Intro to extensions will set you down the path of learning everything there is to know about all the features of the Haskell language.

Join Type Classes for courses and projects to get you started and make you an expert in FP with Haskell.