Comment on page

# Maybe Monad

For our next example, let's imagine that we have a database that holds information about accounts and transactions - we don't have to worry about accessing the database right now, we just assume that a given account or transaction either exists in our database or doesn't. We could have the following type defined (using type synonym declarations):

type Transaction = [Char] -- or String

type Account = [Char] -- or String

where both the

`Transaction`

and the `Account`

instances are represented by strings (e.g. through a hashing function). Imagine that we now want to create a function that, given a `Transaction`

, looks up both the `Account`

from which the transaction originated and the destination `Account`

in our database. There are three ways in which this function could fail:- 1.The transaction is not found in our database
- 2.The origin account is not found in our database
- 3.The destination account is not found in our database

From what we already learned, we would write this function as something like this:

-- helper functions for finding transactions/accounts that may fail

findTransaction :: Transaction -> Maybe Transaction

findOriginAccount :: Transaction -> Maybe Account

findDestinationAccount :: Transaction -> Maybe Account

findAccounts :: Transaction -> Maybe (Account, Account)

findAccounts t =

case findTransaction t of

Nothing -> Nothing

Just t ->

case findOriginAccount t of

Nothing -> Nothing

Just originAcc ->

case findDestinationAccount t of

Nothing -> Nothing

Just destinationAcc ->

Just (originAcc, destinationAcc)

The important thing to notice here is the common pattern of exception checking at every step. At any point that we fail to find an element we are looking for, we want the function to handle this exception by returning

`Nothing`

, and we implement this very explicitly resulting in long and hard-to-read function code. In other words, every step of the function depends on the result of the previous one (or *"binds"*to the result of the previous one). This is where we can take advantage of the fact that`Maybe`

is a monad to simplify this by abstracting the error handling away.First, let's look at the declaration of the

`Monad`

class in Haskell:class Applicative m => Monad m where

return :: a -> m a

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

return = pure

-- Minimal complete definition:

-- (>>=), return

A

`Monad`

is therefore an `Applicative`

that also supports the two methods, `return`

and `(>>=)`

(referred to as **). The***bind*`return`

method wraps a value of any basic type `a`

into the monad, returning a **monadic value**. The built-in declaration also has a default declaration for the`return`

function, and it is simply the `pure`

method of the underlying `Applicative`

class that is used to wrap a value in the applicative constructor. The `(>>=)`

method takes a monadic value and a function that returns a monadic value and generally applies that function to the first argument. Let's explore this in more detail with the implementation of the monad class for the `Maybe`

type as an example:instance Monad Maybe where

-- (>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b

Nothing >>= _ = Nothing

(Just x) >>= f = f x

Why don't we wrap the result of the function application

`f x`

into the `Maybe`

type constructor `Just`

?For the

`return`

method, we keep the default declaration of `pure`

which simply resolves to the `Just`

constructor for the `Maybe`

type. The `(>>=)`

method exemplifies the main idea of monads, which is that we first look at the impure argument we receive (in this case `Maybe a`

) and decide what to do from there. That way, function application depends on the result of the first argument (or the underlying value of the first argument). If `Maybe a`

is a `Nothing`

then we can completely ignore the function and simply propagate the exception with `Nothing`

. Only if we know that the first argument is really a `Just a`

do we unwrap the `a`

value and apply the function `f`

to it.We can now re-write our

`findAccounts`

function in a simpler way using the fact that `Maybe`

is a monad:findAccounts :: Transaction -> Maybe (Account, Account)

findAccounts t =

findTransaction t >>=

(\tx -> findOriginAccount tx >>=

(\origin -> findDestinationAccount tx >>=

(\destination -> return (origin, destination) )))

That reads much better - we don't have to take care of every

`Nothing`

possibility as the monadic aspect of `Maybe`

takes care of this for us. One thing to note is that the `tx`

argument is available at any step below the first "bind" as all the lambda functions bind to one another in a sequence. The same applies to the `origin`

argument which we only use in the last step in order to `return`

our result.Does this kind of sequencing remind you of something we saw before? We used the

`do`

notation with I/O actions to perform multiple actions in a sequence. As it turns out, the `do`

notation is not specific to I/O actions but is actually just an alternative monad syntax and can be used with any monad. We can therefore re-write `findAccounts`

using `do`

notation:findAccounts :: Transaction -> Maybe (Account, Account)

findAccounts t =

do

tx <- findTransaction t

origin <- findOriginAccount tx

destination <- findDestinationAccount tx

return (origin, destination)

This now looks much more like an imperative language and we could say it actually is, but with the imperative language being written is the

`Maybe`

language, which supports exceptions. `findTransaction`

, `findOriginAccount`

and `findDestinationAccount`

are all statements in the imperative language of the `Maybe`

monad. And this extends to all monads so that a value of type `M a`

is interpreted as a statement in an imperative language of `M`

, with the semantics of that language being determined by the underlying monad `M`

.Last modified 8mo ago