Preface
The use of monads can be thought of as a design pattern that allows us to redefine how function composition works. Before we define what a monad is, let’s develop our intuition with some motivational examples.
Example 1: exception handling (a.k.a. “Maybe”)
To get our feet wet, let’s use a purely functional subset of Python to describe a function which computes the quotient of 100 and some divisor. In the special case where the divisor is zero, we’ll return None:
- def divide100(divisor):
- if divisor == 0:
- return None
- else:
- return 100 / divisor
- def sqrt(x):
- if x < 0:
- return None
- else:
- return x ** 0.5
- a = sqrt(x)
- if a is not None:
- b = divide100(a)
- if b is not None:
- c = sqrt(b)
- def composable_divide100(divisor):
- if divisor is None or divisor == 0:
- return None
- else:
- return 100 / divisor
- def composable_sqrt(x):
- if x is None or x < 0:
- return None
- else:
- return x**0.5
- def bind(x, f):
- if x is None:
- return None
- else:
- return f(x)
Example 2: vector operations (a.k.a. “List”)
We know that, mathematically, positive numbers have two square roots. Let’s modify sqrt to return a list of values:
- def sqrt(x):
- if x < 0:
- return []
- elif x == 0:
- return [0]
- else:
- return [x**0.5, -x**0.5]
- def bind(x, f):
- return [j for i in x for j in f(i)]
It works! But our original goal was to find the square roots of one number. We could always write bind([x], sqrt) where x is a number, maybe it would be better to use a function to abstract the representation of our input. Let’s call this function unit:
- def unit(x):
- return [x]
Cool, we can now intelligently compose functions that might return several values! That’s basically Haskell’s List monad.
Example 3: debug output (a.k.a. “Writer”)
Suppose we have some functions u and v, and each function takes a number and returns a number. It doesn’t really matter what these functions are, so let’s define them as follows:
- def u(x):
- return x + 4
- def v(x):
- return x * 2
- def verbose_u(x):
- return (x + 4, '[verbose_u was called on ' + str(x) + ']')
- def verbose_v(x):
- return (x * 2, '[verbose_v was called on ' + str(x) + ']')
- def bind(x, f):
- result, output = f(x[0])
- return (result, x[1] + output)
Awesome! Not only do we get a numerical result, but we also get a complete stack trace. Notice how we passed (4, ''), when all we cared about was the 4. The empty string means that no functions had been called at that point, but that’s an implementation detail. It would be better to write a unit function to construct this tuple for us, so we don’t have to worry about what goes in the string, the ordering of the elements of the tuple, etc.
- def unit(x):
- return (x, '')
Still works! We’ve just implemented Haskell’s Writer monad!
The pattern
In each example, we had some functions which we wanted to compose, but their type signatures were incompatible. Our functions took a value of some type and returned a value in the corresponding monadic type. All we did was write a wrapper function (bind(x, f)), which defines the logic for how such functions can be chained together. We also defined unit(x) (which is often called return because of the way it is used) to convert a value into a monadic type, so it can be used with our bound functions.
A monad is a type constructor, a bind operation, and a unit function. In Example 1, the monadic type was the union of float and NoneType (we didn’t need a unit function because the monadic type was a superset of the input type). In Example 2, the monadic type was list (of floats), and unit simply put values into a list. Finally, the monadic type for Example 3 was the product (i.e., tuple) of int and str, so unit constructed that tuple from a value and the empty string.So the use of monads is basically a design pattern, and it’s been used to implement several concepts that are normally associated with imperative programming, such as exception handling (much like in Example 2), parsers, state, collections, and I/O (as we will see shortly).
Monad axioms
Proper monads must obey three axioms so they behave the way we expect.
(Here, equality should be taken to mean the two expressions represent the same computation.)
The first two axioms mean that unit and bind roughly do inverse things: unit puts a value into a monadic container for binding and bind takes it out to apply a function. The last axiom means we can compose functions together without worrying about how composition precedence works. I’ll leave it as an exercise for the reader to prove that the versions of unit and bind that we wrote obey the three monad laws.
But what about side effects?
It’s probably not obvious how we can use monads to do side effects in a pure way. But by now you understand what a monad is and how to use it to do some useful things, like exception handling, error propagation, and stack traces. We’ll talk about I/O in the next post, but keep in mind that I/O is just one of many uses of monads.
Supplement
* Monads, part 2: impure computations
沒有留言:
張貼留言