On monads
(This post is a Literate Haskell file that was run through pandoc to produce markdown).
It seems customary in the Haskell space to write an introduction to monads; my attempt in this exposition is not to outdo anyone else, but to make the topic clear enough that I can write about it. My hope is that by doing so, it becomes more clear to me.
module Perhaps where
At this point in my Haskell education, I am halfway through Learn You A Haskell (to the point of having covered functors, but not yet applicative functors or monads), and a quarter of the way through Real World Haskell. For me to learn a language requires solving problems, and along the way I've encountered monads.
Yesterday, I watched Brian Beckman's excellent "Don't Fear the
Monad" talk, and realised
I've been using a monad quite regularly in my experiments thus far: the
Maybe type is a monad. This led me to explore what makes Maybe
a monad and write some simple code to make it more apparent to me.
Let's look at Maybe:
ghci> :info Maybe
data Maybe a = Nothing | Just a -- Defined in `Data.Maybe'
instance Monad Maybe -- Defined in `Data.Maybe'
-- ... other instances follow
For the sake of exposition, I'll define my own Maybe called
Perhaps. Nought is the equivalent of Nothing, and Only is the
equivalent of Just.
data Perhaps a = Nought | Only a deriving (Show)
A Monad is a type that implements two functions (actually, in Haskell,
it's four): bind and return.
Looking at the definition of Monad:
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
fail :: String -> m a
-- Defined in `GHC.Base'
The two easiest functions to implement are fail and return.
fail takes an error string and returns the equivalent of Nothing.
perhapsFail :: String -> Perhaps a
perhapsFail s = Nought
return is one of the important Monad functions. It takes a value of
type a and wraps it in the monad. Given the fact that we always have
a value for return, it never returns Nought.
perhapsReturn :: a -> Perhaps a
perhapsReturn x = Only x
The (>>=) operator is the Haskell bind operator. This is a bit more
tricky. It's type is Monad m => m a -> (a -> m b) -> m b. It's used
to compose monadic functions. Here is its definition:
perhapsBind :: Perhaps a -> (a -> Perhaps b) -> Perhaps b
perhapsBind Nought _ = Nought
perhapsBind (Only x) f = f x
It takes a Perhaps value, and a function for transforming a value of
type a into a monad with type b.
The (>>) operator sequentially composes two monadic functions, ignoring
the result of the first action.
perhapsShift :: Perhaps a -> Perhaps b -> Perhaps b
perhapsShift _ x = x
Now, we can make Perhaps an instance of the Monad type class.
instance Monad Perhaps where
(>>=) = perhapsBind
(>>) = perhapsShift
return = perhapsReturn
fail = perhapsFail
There are two rules for monads: they must be associative and they must have an identity function. Due to Haskell's polymorphism, the identity function is already provided. Associativity means that
f x >>= g >>= h
should be the same as
(f x >>= g) >>= h
should be the same as
f x >>= (g >>= h)
Let's see this in action. The first function we'll send it to is a
function that returns Nought if the number is odd, and Only n if
n is even.
perhapsEven :: Integer -> Perhaps Integer
perhapsEven n
| even n = Only n
| odd n = Nought
Here are some examples of perhapsEven:
ghci> map perhapsEven [0..5]
[Only 0
,Nought
,Only 2
,Nought
,Only 4
,Nought]
The next function returns a string of the number if it's given a number that can be divided by (the number isn't zero):
perhapsDividable :: Integer -> Perhaps String
perhapsDividable 0 = Nought
perhapsDividable n = Only (show n)
Some examples:
ghci> map perhapsDividable [0..5]
[Nought,
,Only "1"
,Only "2"
,Only "3"
,Only "4"
,Only "5"]
We can't just compose perhapsEven and perhapsDividable; their type would be
(Integer -> Perhaps Integer) -> (Integer -> Perhaps String)
The types don't line up. Instead,
perhapsShowEvenDividable :: Integer -> Perhaps String
perhapsShowEvenDividable n = perhapsEven n >>= perhapsDividable
perhapsEven has to be applied first to get to the Perhaps type,
then functions can be applied.
perhapsStringLength :: String -> Perhaps Int
perhapsStringLength s = Only (length s)
After the first application, we can compose an arbitrary number of functions (provided the types match up):
ghci> perhapsEven 3 >>= perhapsDividable >>= perhapsStringLength
Nought
ghci> perhapsEven 4 >>= perhapsDividable >>= perhapsStringLength
Only 1
So how is this useful?
Imagine Perhaps conveys whether a function with side effects
succeeded. Let's consider a pair of functions, perhapsReadFile and
perhapsParseWidget.
perhapsReadFile :: FilePath -> Perhaps String
perhapsParseWidget :: String -> Perhaps Widget
perhapsReadFile either returns Only contents if it could
read the contents of the file, and Nought if nothing could be
read. perhapsParseWidget returns Only w if the string contains a
valid representation of a widget, and Nought otherwise.
In Go, we might write something like:
func readAndUseWidget(fileName string) (w widget.Widget, err error) {
contents, err := ioutil.ReadFile(fileName)
if err != nil {
return nil, err
}
w, err := widget.Unmarshal(contents)
if err != nil {
return nil, err
}
// do some initial actions on the widget
return w, nil
}
In Haskell, it's
readAndUseWidget :: FilePath -> Perhaps Widget
readAndUseWidget path = perhapsReadFile path >>= perhapsParseWidget
>>= perhapsInitWidget
It's much more concise, and the error handling can be abstracted out of view. Monads allow us to safely abstract some of this away.
One useful benefit of this is allowing continued execution in the face of failures.
If our readAndUseWidget wasn't using a monad, and we ran through a
sequence of files to process some widgets, an error would stop us dead
in our tracks. As an example, the following function will die if an
error occurs:
readAndProcessWidgets :: [FilePath] -> [Widget]
readAndProcessWidgets = map readAndUseWidget
Given some paths to widget files, where widget2.w doesn't exist:
ghci> readAndProcessWidgets ["widget1.w", "widget2.w", "widget3.w"]
[Widget Foo,*** Exception: widget2.w: openFile: does not exist (No
such file or directory)
We don't even get a complete list of widgets back. With the monad version, we'd get
ghci> map readAndUseWidget ["widget1.w", "widget2.w", "widget3.w"]
ghci> [Only Widget Foo, Nought, Only Widget Baz]
We get a list back, and can process the widgets that were successfully parsed.
We've uesd the monad here to represent failure, but they can be used to
add any additional information to a result besides the pure result. The
IO monad carries information noting that the world the Haskell
program has been run in is unstable; the State monad carries some
notion of state (state is anathema to functional programming). They carry
information about computational dependencies—knowing that the world
has changed, or the possibility that a component failed—through
a sequence of computations.