Consider the Identity
monad:
newtype Identity a = { runIdentity :: a }
Identity
is a Monad
:
instance Functor Identity where
fmap f = Identity . f . runIdentity
instance Applicative Identity where
pure = Identity
(<*>) f x = Identity (runIdentity f (runIdentity x))
instance Monad Identity where
return = Identity
(>>=) f = f . runIdentity
This means that we can use do
notation:
addTwo :: (Num a) => Identity a -> Identity a -> Identity a
addTwo x y = do
a <- x
b <- y
return (a + b)
Programming in this "extended" language is equivalent to programming in regular Haskell:
addTwo :: (Num a) => a -> a -> a
addTwo x y =
let a = x in
let b = y in
a + b
There is nothing new that we can do in the new language.
However, consider the State
monad. We now have two new primitive operations, get
and put
, in the larger language which were not previously available:
add :: (Num s) => s -> State s s
add s = do
total <- get
put (total + s)
return total
We can think of this function as working in a larger language in which there are two new expressions get
and put x
:
add :: (Num s) => s -> s
add s = do
let total = get in
put (total + s)
total
A category is a collection of objects ("types") and morphisms between objects ("functions"), supporting identities and composition:
class Category k where
id :: k a a
(.) :: k b c -> k a b -> k a c
Think of categories as programming languages. Different refinements of the category concept exist, and give us different ways of talking about programming languages in the abstract:
- Categories with (co)products are programming languages with product types (sum types)
- Categories with (co)limits are programming languages with universal (existential) quantification
Every monad determines a category, the Kleisli category for that monad:
newtype Kleisli m a b = Kleisli { runKleisli :: a -> m b }
instance Category (Klesili m) where
id = Kleisli return
(.) f g = Kleisli $ runKleisli f <=< runKleisli g
Put another way, every monad encodes the relationship between a programming language (Hask
) and a larger programming language.
The additional operations of this larger language are the morphisms Kleisli m a b
which are not equal to return a
for any a
. In other words, return
embeds Hask
into the larger language, but there is additional interesting stuff in the new language.
These are pure models of effects. We work in the larger programming language, but we need to project our functions down to the category Hask
in order to see their effects. The way this is done depends on the monad in question.
- Categories are programming languages
- Monads encode embeddings of languages
- The Kleisli category extends the base category with new operations encoded by the monad
The monad laws capture our expections of working in "larger languages".
- Morphisms are the same
- No new operations
Identity
encodes the trivial embedding of a language into itself.
- Morphisms are partial functions
- Only new operation is
Nothing
which represents a missing value
Maybe
encodes the process of replacing functions with partial functions. Alternatively, we extend our language with a new primitive called Nothing
.
- Morphisms are functions which can throw exceptions
- New operations are
Left x
for anyx
Either e
represents enlarging our language with exceptions of type e
.
- Morphisms are multifunctions
- New operations are
[]
(failure) and any list with length>= 2
(multiple successes).
[]
encodes the process of replacing functions with multifunctions.
- Morphisms are the old morphisms, with the domain extended to include some type
r
- New operation is
id
, which reads the configuration (a.k.a.ask
inControl.Monad.Reader
)
The "reader" monad endows our functions with read-access to some global configuration.
- Morphisms are the old morphisms, with the codomain extended to include some type
w
- New operation is
tell
, which writes a value to the log
The writer monad endows our functions with write-access to a global log.
- Morphisms are the old morphisms, with the domain and codomain extended to include some type
s
- New operations are
get
andput
, which read and write the state respectively
The state monad endows our functions with read and write access to a global state.
-
Morphisms are morphisms in the old language, with the domain extended to include a callback:
type Cont r a b = a -> (b -> r) -> r = (a, b -> r) -> r
The callback can be invoked zero or more times.
-
The new operation is
callCC
, or "call with current continuation", which captures the callback as a function argument.
The continuation monad replaces the usual call stack with a tower of callbacks.
We want to be able to compose these programming language extensions. Monads do not compose in general in Hask
, but many of the standard monads have corresponding monad transformers which can be used to turn one monad into another. We are essentially stacking up new features to add to our base language.