This introduction is intended for experienced programmers
beginning to learn functional programming.
All the code samples are in Haskell.
Please note that this is not a full tutorial
1.
It's only an introduction to get you started.
Monads are a class of data types used in pure functional programming.
Monads are
abstract types, which means that each monad
represents a whole class of types.
To make a specific type, you combine the monad with another type
that describes the data inside the monad, which acts as a sort of container.
For example, IO is the monad associated with input/output,
so IO String is the type of value returned by
user input functions, and IO Int
is the type returned by random-number generators.
Once you've defined a monad, you can use it for data of any applicable type.
The data types that can work with a monad are determined
by an operator that's defined for all monads.
The operator is called bind, and is written as >>=.
bind is the most important feature of monads.
It's overloaded with a unique definition for each monad
that gives the monad its own characteristic behavior.
Monads serve two purposes:
- encapsulation of common computational patterns
- isolation of code that interacts with the outside world
Most monads are used to express frequently used computational patterns.
This eliminates repetition in a program's source code
by allowing computationally similar operations to be consolidated
in the definition of bind for the appropriate monad.
The IO monad is used to handle all interactions with
the program's external environment (I/O).
This allows other functions to be combined with each other more easily.
Monads reduce the amount of source code in a program by wrapping
computational patterns in
bind.
There are several standard monads,
each with its own particular behavior pattern:
| Monad | Characteristic behavior
|
|---|
|
| IO | sequential execution of operations in time
|
|---|
| Maybe | operations that may fail to return a value
|
|---|
| List | operations that can return any number of values
|
|---|
| State | operations that need to share data
|
|---|
There are more standard monads, and you can create your own.
Note that List is the type of plain old lists.
Computationally similar operations that act on different data types
can then be consolidated by putting the data in an appropriate monad
and using bind to combine the monad value with
a function that operates on the data inside it.
Expressions using
bind are written as
x >>= f.
x is an expression that returns a monad value of type
m a,
where the variable
m represents a monad,
and
a represents the type of value the monad contains.
f is a function
that takes a single argument
2 of type
a
and returns a monad value of type
m b for some type
b.
bind also returns a value of type
m b.
Maybe is a good example of consolidating source code.
A type
Maybe a has two kinds of value:
A single value called
Nothing to represent a failed operation,
and a class of values written as
Just x
that indicate successful computation of a value
x of type
a.
If you need to pass a value through a chain of operations that don't always succeed,
nonmonadic code to call them might look like something this:
case test1 x in
Nothing -> Nothing
Just y -> case test2 y in
Nothing -> Nothing
Just z -> case test3 z in
...
Obviously there's a pattern here:
The chain terminates and returns Nothing
whenever Nothing is encountered,
i.e. when one of the tests fails.
It continues on success, passing the result of the last operation to the next one.
The definition of bind in Maybe encapsulates this pattern:
x >>= f = case x in Nothing -> Nothing
Just y -> f y
This is the generic computational pattern of returning on failure.
The monadic version of the code snippet above is then much more compact:
test1 x >>= test2 >>= test3 >>= ...
This is the main purpose of most monads.
The source code has been shrunk down to a quarter of its original size.
In large programs with multiple levels of monadic operations,
you can consolidate the code even more.
The idea behind pure functional programming is to make functions in a computer program
act just like mathematical functions:
Given any specific set of arguments,
a function should always return the same value,
and it should never do anything else.
It shouldn't read from or print to the user interface,
and it shouldn't use or change any global variables.
This property is a big help:
- Unit testing is much easier, because the behavior of each function
is independent of the rest of the program.
- The programmer doesn't have to worry about what order functions are
called in, only what arguments they recieve.
This applies even with multiple calls to the same function.
- Since it doesn't matter what order functions are called in,
as long as they have the right arguments,
the compiler can produce better optimized code3,
especially if multiple processors are available.
The only problem with pure functions is that they can't express real-world actions:
- Input functions and random-number generators don't always return the same value
for a given set of arguments.
- Output functions have an effect on the outside world.
This is independent of the function's return value, but dependent on the order
in which function calls are made.
- Functions that assign new values to global variables can affect the behavior
of functions that use those variables.
The
IO monad is used to alleviate this problem.
Placing a value in
IO labels the value as "impure".
4
The compiler keeps the processing of all impure operations separate from the rest of the program
by requiring the programmer to handle them in functions that operate
on data in the
IO monad.
This preserves the mathematical purity of other functions,
allowing them to be optimized better and combined more freely.
Since IO functions interact with the outside world,
which can't be passed to a function as an argument,
pure functions aren't allowed to call them.
So IO functions can only be called by other IO functions.
This means the entry point of every pure functional program has to be an IO function.
In Haskell, this function is main, just like in C.
main initiates the chain of IO function calls,
and pure functions can be called from anywhere in this chain.
A monad function is simply a function that returns a monad value.
It can also take monad arguments.
Inside the monad function,
bind allows you to pull values out of their monads
and safely pass the values to pure functions.
You can think of the monad function as removing the contents
of one or more monads, operating on the contents functionally,
and putting the results back into a monad to be returned as the value of the function.
There's another monad operator, sometimes called
then,
which is written
>>.
then is defined in terms of
bind:
x >> f = x >>= (\_ -> f)
The stuff in parentheses is a nameless function
5 (indicated by the backslash)
that takes an unnamed argument (the underline),
as required by
bind.
But
then ignores
x and just calls
f.
then evaluates
x only for its side effects.
In this case,
f should not take an argument
2.
Here's a simple "Hello User" program:
Module Main where
getName = putStr "Please enter your name: " >> getLine
sayHello name = putStrLn ("Welcome, " ++ name)
main = getName >>= sayHello
All of these functions are IO functions.
The only exception is ++, which concatenates bare strings.
putStr, putStrLn, and getLine
are built-in functions.
The following is the sequence of events.
It differs from functional code in that the order of events
is strictly preserved.
-
main calls bind, which calls getName,
which calls then, which calls putStr.
-
putStr prints its text argument to the user's screen
and returns a null value.
-
then ignores the return value of putStr
and calls getLine, which waits for the user to enter a line of text,
wraps it in an IO monad,
and returns the monad value to then,
which returns it to getName,
which returns it to bind.
-
bind pulls the string out of the monad returned by getName
and passes a greeting containing the string to sayHello,
which calls putStrLn,
which prints the greeting and returns a null value.
If you want to wrap the login greeting in a function that returns the user's name,
you need to use a monad function called
return.
return doesn't do very much.
It just puts a value back into a monad:
greetUser = getName >>= (\name -> sayHello name >> return name)
Note that return isn't an operator that returns from the current function,
as it is in most languages.
It's just a function that "returns" its argument to the current monad.
The basic
bind syntax isn't too bad for a small function like greetUser,
but for larger functions, it's not always easy to read.
For real-life code, it's often more convenient to use do notation.
The monad function begins with the keyword do,
and contains actions in one of two formats:
-
separated by semicolons and surrounded by curly braces, as in C or Perl.
-
indented and separated by linebreaks, as in Python.
A left arrow (<-) binds the identifier on the left
to the contents of the monad on the right.
greetUser = do { name <- getName; sayHello name; return name }
or
greetUser = do
name <- getName
sayHello name
return name
The State monad encapsulates shared data
that might otherwise be contained in global variables.
Unlike IO, values in State are passed between
functions as arguments, keeping State within the functional model.
State makes this process easier by putting many values into a single container.
To continue learning about monads in Haskell, see these sources:
Please let me know
if anything in this introduction was confusing or didn't answer a question you have.
Likewise if you're an experienced Haskell hacker and you found any mistakes.
footnotes
1
Because there are plenty enough of those already.
2
Actually, f can take a larger number of arguments, call it n,
but in that case, x >>= f will return a monad function
of n - 1 arguments,
with the contents of x bound to the first argument of f.
x >> f simply returns f.
3
The theory of lambda calculus makes it much easier for the programmers who write compilers
to build in optimization of pure functions. This is a very powerful tool,
but is not possible for functions with side effects (I/O) or mutable data (variables).
4
This is the source of the
space-suit
and
nuclear-waste
metaphors.
5
A nameless function is usually called an "anonymous" function,
a "lambda" function, or simply a "lambda".
It may be too trivial to bother giving it a name,
it may be used only once,
and it may defined in terms of data that's determined at runtime,
in which case it's called a "closure".