50.054 - Applicative and Monad¶
Learning Outcomes¶
- Describe and define derived type class
- Describe and define Applicative Functors
- Describe and define Monads
- Apply Monad to in design and develop highly modular and resusable software.
Derived Type Class¶
Recall that in our previous lesson, we encountered the Eq
and Ord
type classes from the Haskell prelude.
Eq
type class is a super class of the Ord
type class, because any instance of Ord
type class should also be an instance of the Ord
(by some type class instance declaration),
We also say Ord
is a derived type class of Eq
.
In addition, we find some default implementations of the member functions of Ord
in the type class body. Minimally, we only need provide the implementation for either compare
or (<=)
in an instance of the Ord
type class.
Let's consider some instances
Functor (Recap)¶
Recall from the last lesson, we make use of the Functor
type class to define generic programming style of data processing.
Applicative Functor¶
The Applicative
Functor is a derived type class of Functor
, which is defined as follows
We will come to the member functions pure
and (<*>)
shortly. Since Applicative
is a derived type class of
Eq
, type instance of Applicative a
must be also an instance of Ord a
.
For example, we consider the predefined instance of Applicative []
instance from the prelude.
In the pure
function, we take the input argument a
and enclose it in a list.
In the (<*>)
function, (it read as "app"), we encounter a list of functions of type a -> b
and
a list of values of type a
. We apply list comprehension to extract every function elements in fs
and
apply it to every value element in as
.
If we were to consider the alternative implementation of <*>
for list, we could use concatMap
and map
.
Can you try to translate the above list comprehension into an equivalent Haskell expression using
concatMap
andmap
?
Note that since we have defined Functor []
in the earlier section, we don't need to repeat.
Let's consider some example that uses Applicative []
. Imagine we have a set of different operations and a set of data. The operation in the set should operate independently. We want to apply all the operations to all the data. We can use the <*>
operation.
Let's consider another example. Recall that Maybe a
algebraic datatype which captures a value of type a
could be potentially empty.
We find that Functor Maybe
Applicative Maybe
instances are in the prelude as follows
In the above Applicative instance, the <*>
function takes a optional operation and optional value as inputs, tries to apply the operation to the value when both of them are present, otherwise, signal an error by returning Nothing
. This allows us to focus on the high-level function-value-input-output relation and abstract away the details of handling potential absence of function or value.
Applicative Laws¶
Like Functor laws, every Applicative instance must follow the Applicative laws to remain computationally predictable.
- Identity:
(<*>) (pure \x->x)
\(\equiv\)\x->x
- Homomorphism:
(pure f) <*> (pure x)
\(\equiv\)pure (f x)
- Interchange:
u <*> (pure y)
\(\equiv\)(pure (\f->f y)) <*> u
- Composition:
(((pure (.))) <*> u) <*> v) <*> w
\(\equiv\)u <*> (v <*> w)
Note that:
- Identity law states that applying a lifted identity function of type
a->a
is same as an identity function of typet a -> t a
wheret
is an applicative functor. - Homomorphism says that applying a lifted function (which has type
a->a
before being lifted) to a lifted value, is equivalent to applying the unlifted function to the unlifted value directly and then lift the result. -
To understand Interchange law let's consider the following equation $$ u y \equiv (\lambda f.(f y)) u $$
- Interchange law says that the above equation remains valid when \(u\) is already lifted, as long as we also lift \(y\).
-
To understand the Composition law, we consider the following equation in lambda calculus
The Composition Law says that the above equation remains valid when \(u\), \(v\) and \(w\) are lifted, as long as we also lift \(\lambda f.(\lambda g.(f \circ g))\).
Monad¶
Monad is one of the essential coding/design pattern for many functional programming languages. It enables us to develop high-level resusable code and decouple code dependencies and generate codes by (semi-) automatic code-synthesis. FYI, Monad is a derived type class of Applicative thus Functor.
Let's consider a motivating example. Recall that in the earlier lesson, we came across the following example.
In which we use Maybe
to capture the potential div-by-zero error.
One issue with the above is that it is very verbose, we lose some readability of the code thus, it takes us a while to migrate to Either a b
if we want to have better error messages. Monad is a good application here.
Let's consider the type class definition of Monad m
.
Monad
is a derived type class of Applicative
. The minimal requirement of a Monad instance is to implement the (>>=)
(pronounced as "bind") function besides the obligation from Applicative
and Functor
.
In the history of Haskell,
Monad
was not defined not as a derived type class ofApplicative
andFunctor
. It was reported and resolved since GHC version 7.10 onwards. Such approach was adopted by other language and systems.
Let's take a look at the Monad Maybe
instance provided by the Haskell prelude.
The eval
function can be re-expressed using Monad Maybe
.
It certainly reduces the level of verbosity, but the readability is worsened.
Thankfully, we can make use of a do
syntactic sugar provided by Haskell.
In Haskell expression
is automatically desugared intoHence we can rewrite the above eval
function as
Now the readability is restored.
Another advantage of coding with Monad
is that its abstraction allows us to switch underlying data structure without major code change.
For instance, we find the following Functor
, Applicative
and Monad
instances for Either String
Suppose we would like to use Either String a
or some other equivalent as return type of eval
function to support better error message.
But before that, let's consider some subclasses of the Monad
type classes provided in the Haskell standard library mtl
.
In the above, we define a derived type class of Monad
, called MonadError e m
where m
is the Monadic functor and e
is the error type. The additional declaration | m -> e
denotes a functional depenedency between the instances of m
and e
. (You can think of it in terms of database FDs.)
It says that whenever we fix a concrete instance of m
, we can uniquely identify the corresponding instance of e
. The member function throwErrow
takes an error message and injects into the Monad result. Function catchError
runs an monad computation m a
. In case of error, it applies the 2nd argument, a function of type e -> m a
to handle it. You can think of catchError
is the try ... catch
equivalent in MonadError
.
Similarly, we extend Monad
type class with MonadError
type class. Next we examine type class instance MonadError () Maybe
. We use ()
(pronounced as "unit") as the error type as we can't really propogate error message in Maybe
other than Nothing
.
Next, we adjust the eval
function to takes in a MonadError
context instead of a Monad
context. In addition, we make the error signal more explicit by calling the throwError
function from the MonadError
type class.
Now let's try to refactor the code to make use of Either String Int
as the functor instead of Maybe Int
.
MonadError String (Either String)
instance, which satisfies the functional dependency set by the type class as Either String
functionally determines String
.
Note that the concept of currying is applicable to the type constructors.
Either
has kind* -> * -> *
thereforeEither String
has kind* -> *
.
Now we can refactor the eval
function by changing its type signature. And its body remains unchanged (almost).
Commonly used Monads¶
We have seen the option Monad and the either Monad. Let's consider a few commonly used Monads.
List Monad¶
We know that []
is a Functor and an Applicative.
It is not surprising that []
is also a Monad.
As we can observe from above, the bind function for List monad is a variant of concatMap
.
With the above instance, we can write list processing method in for comprehension which is similar to query languages (as an alternative to list comprehension).
We define a scheme of a staff record using the following datatype.
Note that using {}
on the right hand side of a data type definition denotes a record style datatype.
The components of the record data type are associated with field names.
The names of the record fields can be used as getter fnuctions. The above is almost the same as an algebraic data type
One difference is that we can use the record name in the record data type as an pseudo update function.
Now we are ready to write some query in Haskell list monad. Given a table of data,
We can use the follow query to retrieve all the staff ids whose salary is more than 50000.
Reader Monad¶
Next we consider the Reader
Monad. Reader
Monad denotes a shared input environment used by multiple computations. Once shared, this environment stays immutable.
For example, suppose we would like to implement some test with a sequence of API calls. Most of these API calls are having the same host IP. We can set the host IP as part of the reader's environment.
First let's consider the reader data type
It denotes a computation that takes the reference (shared) environment r
and produces some result a
.
Then we provide the necessarily implementation to qualify Reader r
as a Monad
instance.
fmap
function takes a functionf
and a readerra
and returns a reader whose computation function takes an input referenced objectr
and runsra
with r to obtaina
, then appllyf
toa
.pure
function takes a value of typea
and wraps it into a reader object whose computation function is returning the input value ignoring the referenced objectr
.- The app function (
<*>
) takes a readerrf
that produces function(s), and a reader that produces valuea
. It returns a reader whose computation function takes a referenced objectr
and runrf
withra
with it to produce the functionf
and the valuea
. Finally it appliesf
toa
. - The bind function (
>>=
) takes a readerra
and a functiong
, it returns a reader whose computation function takes a referenced objectr
and runra
withr
to obtaineda
, next it appliesg
toa
to generate the readerrb
, finally, it runsrb
withr
.
We consider the MonadReader
type class definition as follows,
We provide the implementation for MonadReader r (Reader r)
.
The following example shows how Reader Monad can be used in making several API calls (computation) to the same API server (shared input
https://127.0.0.1/
).
For authentication we need to call the authentication server https://127.0.0.10/
temporarily.
Calling run test1 apiServer
yields the following debugging messages.
State Monad¶
Next we consider the State
Monad.
A State
Monad allows programmers capture and manipulate stateful computation without using assignment and mutable variable. One advantage of doing so is that program has full control of the state without having direct access to the computer memory. In a typeful language like haskell, the type system segregates the pure computation from the stateful computation. This greatly simplify software verification and debugging.
We consider the following state data type
It denotes a computation that takes the state environment s
and produces some result a
and the updated state envrionment.
Then we provide the necessarily implementation to qualify State s
as a Monad
instance.
- The
fmap
function takes a functionf
and a state objectsa
and returns a state object whose computation takes a states
and runssa
withs
to copmute the result valuea
and the updated states1
. Finally, it returns the result of applyingf
toa
ands1
. - The
pure
function takes a value of typea
and return aState
object by wrapping a lambda which takes a states
and returns back the same states
with the input value. - The app function (
<*>
) takes a state objectsf
and a state objectsa
, its returned value is a state object that expects an input states
and runsf
withs
to extract the underlying functionf
with the updated states1
, then it runssa
withs1
to extract the resulta
and the updated states2
. Finally, it returns the result of applyingf
toa
and the updated states2
. - In the bind function (
>>=
), we take a computationsa
of typeState s a
, i.e. a stateful computation over state types
and return a result of typea
. In addition, we take a function that expects input of typea
and returns a stateful computationState s b
. We return aState
object contains a function that takes the input states
, and running withsa
to retrieve the resulta
and the updated states1
, finally, the function run the state monadf a
withs1
to computeb
and the output state.
We consider the MonadState
type class definition as follows,
get
is the query function that accesses the current state,
put
is the setter function which "updates" the state by returning a (potentially new) state.
We provide the implementation for MonadState s (State s)
.
Let's consider the following example
In the above we define the state environment as an integer counter. Monadic function incr
increase the counter in the state. The deriving
keyword generate the type class instance Show Counter
automatically. Running run app (Counter 0)
yields (2, Counter {c = 2})
.
IO Monad¶
Haskell is a pure functional programming language, i.e. computation should not depend on external states. Determinism in the program semantics is guaranteed.
In many software applications, we need to manage the change of external states. Haskell is a useless programming language without the support of external state management. In Haskell, we manage the external states by abstracting them with the IO
monad.
Unlike all the other monads introduced earlier, the IO
monad is a built-in monad, which has no definition in Prelude or other libraries. The IO
monad and its instances are defined in terms of compiler primitives, e.g. within GHC.
Console input/output actions with IO Monad¶
Printing a message to the console is considered an action that manipulate the external state, i.e. the terminal display. Hence in Haskell, to put a string in the console output, we use the following
where putStrLn
has type String -> IO ()
, which says, putStrLn
takes a string and performs an IO computation and returns ()
(it reads as unit). The reason why ()
is returned is because the computation after the putStrLn
expression should not depend on the result of putStrLn
.
Similar to putStrLn
, we could use print :: Show a => a -> IO ()
.
To read a line from the console, we use getLine :: IO String
.
To read the command line arguments (i.e. arguments we pass to the main function), we use getArgs :: IO [String]
File IO with IO Monad¶
The IO
monad allows us to interact with files using Haskell.
The following program reads the content from the input file, converts it to uppercase then writes the result into the output file.
The readFile
function has type FilePath -> IO String
where FilePath
is a type alias to String
.
Similarly writeFile
function has type FilePath -> String -> IO ()
.
For details of other file IO APIs, you may refer to this document.
https://hackage.haskell.org/package/base-4.20.0.1/docs/Prelude.html#v:readFile
Monad Laws¶
Similar to Functor and Applicative, all instances of Monad must satisfy the following three Monad Laws.
- Left Identity:
(return a) >>= f
\(\equiv\)f a
- Right Identity:
m >>= return
\(\equiv\)m
-
Associativity:
(m >>= f) >>= g
\(\equiv\)m >>= (\x -> ((f x) >>= g))
-
Intutively speaking, a bind operation is to extract results of type
a
from its first argument with typem a
and applyf
to the extracted results. - Left identity law enforces that binding a lifted value to
f
, is the same as applyingf
to the unlifted value directly, because the lifting and the extraction of the bind cancel each other. - Right identity law enforces that binding a lifted value to
return
, is the same as the lifted value, because extracting results fromm
andreturn
cancel each other. - The Associativity law enforces that binding a lifted value
m
tof
then tog
is the same as bindingm
to a monadic bind composition\x -> ((f x) >>= g)
Summary¶
In this lesson we have discussed the following
- A derived type class is a type class that extends from another one.
- An Applicative Functor is a sub-class of Functor, with the methods
pure
and<*>
. - The four laws for Applicative Functor.
- A Monad Functor is a sub-class of Applicative Functor, with the method
>>=
. - The three laws of Monad Functor.
- A few commonly used Monad such as, List Monad, Option Monad, Reader Monad and State Monad.
Extra Materials (You don't need to know these to finish the project nor to score well in the exams)¶
Writer Monad¶
The dual of the Reader
Monad is the Writer
Monad, which has the following definition.
A writer object stores the result and writer output state object which can be empty (via mempty
)
and extended (via <>
). For simplicity, we can think of mempty
is the default empty state,
for example, empty list []
, and <>
is the append operation like ++
. For details about the Monoid
type class refer to
The type class definition of MonadWriter
is given in the mtl
package as follows
With these we are above to define a simple logger app as follows,
Running run app
yields
Monad Transformer¶
In the earlier exection, we encounter our State
datatype to record the computation in a state monad.
What about the following, can it be use as a state datatype for a state monad?
The difference between this class and the State
class we've seen earlier is that the execution method run'
yields result of type Maybe (s,a)
instead of (s,a)
which means that it can potentially fail.
It is ascertained that MyState
is also a Monad, and it is a kind of special State Monad.
Besides "stuffing-in" an Maybe
type, one could use an Either
type and etc. Is there a way to generalize this by parameterizing?
Seeking the answer to this question leads us to Monad Transformer.
We begin by parameterizing the Option
functor in MyState
In the above it is largely similar to MyState
datatype, except that we parameterize Maybe
by a type parameter m
. As we observe from the type class instances that follow, m
must be an instance of Monad
, (which means m
could be Maybe
, Either String
, and etc.)
Let m
be Maybe
, which is a Monad instance, we can replace MonadState s (MyState s)
in terms of MonadState s (StateT s m)
and Monad Maybe
.
If we want to have a version with Either String
, we could define
What about the original vanilla State
Monad? Can we redefine it interms of StateT
?
We could introduce the Identity
Monad.
One advantage of having Monad Transformer is that now we can create new Monad by composition of existing Monad Transformers. We are able to segregate and interweave methods from different Monad serving different purposes.
Similarly we could generalize the Reader
Monad to its transformer variant.
Note that the order of how Monad Transfomers being stacked up makes a difference,
For instance, can you explain what the difference between the following two monad object types is?