Movatterモバイル変換


[0]ホーム

URL:


Jump to content
WikibooksThe Free Textbook Project
Search

Haskell/Understanding monads

From Wikibooks, open books for an open world
<Haskell
Thelatest reviewed version waschecked on5 May 2021. There is1 pending change awaiting review.
Understanding monads
Monads

Prologue: IO, an applicative functor
Understanding monads
Maybe
List
do notation
IO
State
Alternative and MonadPlus
Monad transformers

There is a certain mystique about monads, and even about the word "monad" itself. While one of our goals of this set of chapters is removing the shroud of mystery that is often wrapped around them, it is not difficult to understand how it comes about. Monads are very useful in Haskell, but the concept is often difficult to grasp at first. Since monads have so many applications, people often explain them from a particular point of view, which can derail your efforts towards understanding them in their full glory.

Historically, monads were introduced into Haskell to perform input and output – that is, I/O operations of the sort we dealt with in theSimple input and output chapter and theprologue to this unit. A predetermined execution order is crucial for things like reading and writing files, and monadic operations lend themselves naturally to sequencing. However, monads are by no means limited to input and output. They can be used to provide a whole range of features, such as exceptions, state, non-determinism, continuations, coroutines, and more. In fact, thanks to the versatility of monads, none of these constructs needed to be built into Haskell as a language; rather, they are defined by the standard libraries.

In thePrologue chapter, we began with an example and used it to steadily introduce several new ideas. Here, we will do it the other way around, starting with a definition of monad and, from that, building connections with what we already know.

Definition

[edit |edit source]

Amonad is defined by three things:

The function and operator are methods of theMonad type class and have types

return::a->ma(>>=)::ma->(a->mb)->mb

and are required to obeythree laws that will be explained later on.

For a concrete example, take theMaybe monad. The type constructor ism = Maybe, whilereturn and(>>=) are defined like this:

return::a->Maybeareturnx=Justx(>>=)::Maybea->(a->Maybeb)->Maybebm>>=g=casemofNothing->NothingJustx->gx

Maybe is the monad, andreturn brings a value into it by wrapping it withJust. As for(>>=), it takes am :: Maybe a value and ag :: a -> Maybe b function. Ifm isNothing, there is nothing to do and the result isNothing. Otherwise, in theJust x case,g is applied tox, the underlying value wrapped inJust, to give aMaybe b result. Note that this result may or may not beNothing, depending on whatg does tox. To sum it all up, if there is anunderlying value of typea inm, we applyg to it, which brings the underlying value back into theMaybe monad.

The key first step to understand howreturn and(>>=) work is tracking which values and arguments are monadic and which ones aren't. As in so many other cases, type signatures are our guide to the process.

Motivation: Maybe

[edit |edit source]

To see the usefulness of(>>=) and theMaybe monad, consider the following example: Imagine a family database that provides two functions:

father::Person->MaybePersonmother::Person->MaybePerson

These look up the name of someone's father or mother. In case our database is missing some relevant information,Maybe allows us to return aNothing value to indicate that the lookup failed, rather than crashing the program.

Let's combine our functions to query various grandparents. For instance, the following function looks up the maternal grandfather (the father of one's mother):

maternalGrandfather::Person->MaybePersonmaternalGrandfatherp=casemotherpofNothing->NothingJustmom->fathermom

Or consider a function that checks whether both grandfathers are in the database:

bothGrandfathers::Person->Maybe(Person,Person)bothGrandfathersp=casefatherpofNothing->NothingJustdad->casefatherdadofNothing->NothingJustgf1->-- found first grandfathercasemotherpofNothing->NothingJustmom->casefathermomofNothing->NothingJustgf2->-- found second grandfatherJust(gf1,gf2)

What a mouthful! Every single query might fail by returningNothing and the whole function must fail withNothing if that happens.

Clearly there has to be a better way to write that instead of repeating the case ofNothing again and again! Indeed, that's what theMaybe monad is set out to do. For instance, the function retrieving the maternal grandfather has exactly the same structure as the(>>=) operator, so we can rewrite it as:

maternalGrandfatherp=motherp>>=father

With the help of lambda expressions andreturn, we can rewrite the two grandfathers function as well:

bothGrandfathersp=fatherp>>=(\dad->fatherdad>>=(\gf1->motherp>>=-- gf1 is only used in the final return(\mom->fathermom>>=(\gf2->return(gf1,gf2)))))

While these nested lambda expressions may look confusing to you, the thing to take away here is that(>>=) releases us from listing all theNothings, shifting the focus back to the interesting part of the code.

To be a little more precise: The result offather p is a monadic value (in this case, eitherJust dad orNothing, depending on whetherp's father is in the database). As thefather function takes a regular (non-monadic) value, the(>>=) feedsp'sdad to itas a non-monadic value. The result offather dad is then monadic again, and the process continues.

So,(>>=) helps us pass non-monadic values to functions without actually leaving a monad. In the case of theMaybe monad, the monadic aspect is the uncertainty about whether a value will be found.

Type class

[edit |edit source]

In Haskell, theMonad type class is used to implement monads. It is provided by theControl.Monad module and included in the Prelude. The class has the following methods:

classApplicativem=>Monadmwherereturn::a->ma(>>=)::ma->(a->mb)->mb(>>)::ma->mb->mbfail::String->ma

Aside from return and bind, there are two additional methods,(>>) andfail. Both of them have default implementations, and so you don't need to provide them when writing an instance.

The operator(>>), spelled "then", is a mere convenience and has the default implementation

m>>n=m>>=\_->n

(>>) sequences two monadic actions when the second action does not involve the result of the first, which is a common scenario for monads such asIO.

printSomethingTwice::String->IO()printSomethingTwicestr=putStrLnstr>>putStrLnstr

The functionfail handles pattern match failures indo notation. It's an unfortunate technical necessity and doesn't really have anything to do with monads. You are advised not to callfail directly in your code.

Monad andApplicative

[edit |edit source]

An important thing to note is thatApplicative is a superclass ofMonad.[2] That has a few consequences worth highlighting. First of all, everyMonad is also aFunctor and anApplicative, and sofmap,pure,(<*>) can all be used with monads. Secondly, actually writing aMonad instance also requires providingFunctor andApplicative instances. We will discuss ways of doing that later in this chapter. Thirdly, if you have worked through thePrologue, the types and roles ofreturn and(>>) should look familiar...

(*>)::Applicativef=>fa->fb->fb(>>)::Monadm=>ma->mb->mbpure::Applicativef=>a->fareturn::Monadm=>a->ma

The only difference between the types of(*>) and(>>) is that the constraint changes fromApplicative toMonad. In fact, that is the only difference between the methods: if you are dealing with aMonad you can always replace(*>) and(>>), and vice-versa. The same goes forpure/return – in fact, it is not even necessary to implementreturn if there is an independent definition ofpure in theApplicative instance, asreturn = pure is provided as a default definition ofreturn.

Notions of Computation

[edit |edit source]

We have seen how(>>=) andreturn are very handy for removing boilerplate code that crops up when usingMaybe. That, however, is not enough to justify why monads matter so much. Our next step towards that will be rewriting the two-grandfathers function in a quite different-looking style: usingdo notation withexplicit braces and semicolons. Depending on your experience with other programming languages, you may find this very suggestive:

bothGrandfathersp=do{dad<-fatherp;gf1<-fatherdad;mom<-motherp;gf2<-fathermom;return(gf1,gf2);}

If this looks like a code snippet in an imperative programming language to you, that's because itis. In particular, this imperative language supportsexceptions :father andmother are functions that might fail to produce results, raising an exception instead; when that happens, the wholedo-block willfail, i.e. terminate with an exception (meaning, evaluate toNothing, here).

In other words, the expressionfather p, which has typeMaybe Person, is interpreted as a statement in an imperativelanguage that returns aPerson as the result, or fails.

This is true for all monads: a value of typeM a is interpreted as astatement in an imperative languageM that returns a value of typea as its result; the semantics of this language are determined by the monadM.[3]

Under this interpretation, thethen operator(>>) is simply an implementation of the semicolon, and(>>=) of the semicolon and assignment (binding) of the result of a previous computational step. Just like alet expression can be written as a function application,

   let x = foo in (x + 3)        corresponds to      foo  &  (\x -> id (x + 3))      -- v & f = f v

an assignment and semicolon can be written with the bind operator:

   x <- foo; return (x + 3)      corresponds to      foo >>= (\x -> return (x + 3))

In case of functions,& andid are trivial; in case of a monad,>>= andreturn are substantial.

The& operator combines two purecalculations,foo andid (x + 3), while creating a new binding for the variablex to holdfoo'svalue, makingx available to the second calculational step,id (x + 3).

The bind operator>>= combines twocomputational steps,foo andreturn (x + 3), in a manner particular to the monadM, while creating a new binding for the variablex to holdfoo'sresult, makingx available to the next computational step,return (x + 3). In the particular case ofMaybe, iffoo will fail to produce a result, the second step is skipped and the whole combined computation will fail right away as well.

The functionreturn lifts a plain valuea toM a, a statement in the imperative languageM, which statement, when executed / run, will result in the valuea without any additional effects particular toM. This is ensured by Monad Laws,foo >>= return === foo andreturn x >>= k === k x; see below.

Note

The fact that(>>=), and thereforeMonad, lies behind the left arrows indo-blocks explains why we were not able to explain them in thePrologue, when we only knew aboutFunctor andApplicative.Applicative would be enough to provide some, but not all, of the functionality of ado-block.


Different semantics of the imperative language correspond to different monads. The following table shows the classic selection that every Haskell programmer should know. If the idea behind monads is still unclear to you, studying each of the examples in the following chapters will not only give you a well-rounded toolbox but also help you understand the common abstraction behind them.

MonadImperative SemanticsWikibook chapter
MaybeException (anonymous)Haskell/Understanding monads/Maybe
ErrorException (with error description)Haskell/Understanding monads/Error
IOInput/OutputHaskell/Understanding monads/IO
[] (lists)NondeterminismHaskell/Understanding monads/List
ReaderEnvironmentHaskell/Understanding monads/Reader
WriterLoggerHaskell/Understanding monads/Writer
StateGlobal stateHaskell/Understanding monads/State

Furthermore, these different semantics need not occur in isolation. As we will see in a few chapters, it is possible to mix and match them by usingmonad transformers to combine the semantics of multiple monads in a single monad.

Monad Laws

[edit |edit source]

In Haskell, every instance of theMonad type class (and thus all implementations of bind(>>=) andreturn) must obey the following three laws:

m>>=return=m-- right unitreturnx>>=f=fx-- left unit(m>>=f)>>=g=m>>=(\x->fx>>=g)-- associativity


Return as neutral element

[edit |edit source]

The behavior ofreturn is specified by the left and right unit laws. They state thatreturn doesn't perform any computation, it just collects values. For instance,

maternalGrandfatherp=domom<-motherpgf<-fathermomreturngf

is exactly the same as

maternalGrandfatherp=domom<-motherpfathermom

by virtue of the right unit law.

Associativity of bind

[edit |edit source]

The law of associativity makes sure that (like the semicolon) the bind operator(>>=) only cares about the order of computations, not about their nesting; e.g. we could have writtenbothGrandfathers like this (compare with our earliest version withoutdo):

bothGrandfathersp=(fatherp>>=father)>>=(\gf1->(motherp>>=father)>>=(\gf2->return(gf1,gf2)))

The associativity of thethen operator(>>) is a special case:

(m>>n)>>o=m>>(n>>o)

Monadic composition

[edit |edit source]

It is easier to picture the associativity of bind by recasting the law as

(f>=>g)>=>h=f>=>(g>=>h)

where(>=>) is themonad composition operator, a close analogue of the function composition operator(.), only with flipped arguments. It is defined as:

(>=>)::Monadm=>(a->mb)->(b->mc)->a->mcf>=>g=\x->fx>>=g

There is also(<=<), which is flipped version of(>=>). When using it, the order of composition matches that of(.), so that in(f <=< g)g comes first.[4]

Monads and Category Theory

[edit |edit source]

Monads originally come from a branch of mathematics called Category Theory. Fortunately, it is entirely unnecessary to understand category theory in order to understand and use monads in Haskell. The definition of monads in Category Theory actually uses a slightly different presentation. Translated into Haskell, this presentation gives an alternative yet equivalent definition of a monad, which can give us some additional insight on theMonad class.[5]

So far, we have defined monads in terms of(>>=) andreturn. The alternative definition, instead, treats monads as functors with two additional combinators:

fmap::(a->b)->Ma->Mb-- functorreturn::a->Majoin::M(Ma)->Ma

For the purposes of this discussion, we will use the functors-as-containers metaphor discussed in thechapter on the functor class. According to it, a functorM can be thought of as container, so thatM a "contains" values of typea, with a corresponding mapping function, i.e.fmap, that allows functions to be applied to values inside it.

Under this interpretation, the functions behave as follows:

  • fmap applies a given function to every element in a container
  • return packages an element into a container,
  • join takes a container of containers and flattens it into a single container.

With these functions, the bind combinator can be defined as follows:

m>>=g=join(fmapgm)

Likewise, we could give a definition offmap andjoin in terms of(>>=) andreturn:

fmapfx=x>>=(return.f)joinx=x>>=id

liftM and Friends

[edit |edit source]

Earlier, we pointed out that everyMonad is anApplicative, and therefore also aFunctor. One of the consequences of that wasreturn and(>>) being monad-only versions ofpure and(*>) respectively. It doesn't stop there, though. For one,Control.Monad definesliftM, a function with a strangely familiar type signature...

liftM::(Monadm)=>(a->b)->ma->mb

As you might suspect,liftM is merelyfmap implemented with(>>=) andreturn, just as we have done in the previous section.liftM andfmap are therefore interchangeable.

AnotherControl.Monad function with an uncanny type isap:

ap::Monadm=>m(a->b)->ma->mb

Analogously to the other cases,ap is a monad-only version of(<*>).

There are quite a few more examples ofApplicative functions that have versionsspecialised toMonad inControl.Monad and other base library modules. Their existence is primarily due to historical reasons: several years went by between the introductions ofMonad andApplicative in Haskell, and it took an even longer time forApplicative to become a superclass ofMonad, thus making usage of the specialised variants optional. While in principle there is little need for using the monad-only versions nowadays, in practice you will seereturn and(>>) all the time in other people's code – at this point, their usage is well established thanks to more than two decades of Haskell praxis withoutApplicative being a superclass ofMonad.

Note

Given thatApplicative is a superclass ofMonad, the most obvious way of implementingMonad begins by writing theFunctor instance and then moving down the class hierarchy:

instanceFunctorFoowherefmap=-- etc.instanceApplicativeFoowherepure=-- etc.(<*>)=-- etc.instanceMonadFoowhere(>>=)=-- etc.

While following the next few chapters, you will likely want to write instances ofMonad and try them out, be it to run the examples in the book or to do other experiments you might think of. However, writing the instances in the manner shown above requires implementingpure and(<*>), which is not a comfortable task at this point of the book as we haven't covered theApplicative laws yet (we will only do so at theapplicative functors chapter). Fortunately, there is a workaround: implementing just(>>=) andreturn, thus providing a self-sufficientMonad instance, and then usingliftM,ap andreturn to fill in the other instances:

instanceMonadFoowherereturn=-- etc.(>>=)=-- etc.instanceApplicativeFoowherepure=return(<*>)=apinstanceFunctorFoowherefmap=liftM

The examples and exercises in this initial series of chapters about monads will not demand writingApplicative instances, and so you can use this workaround until we discussApplicative in detail.


Notes

  1. Thisreturn function has nothing to do with thereturn keyword found in imperative languages like C or Java; don't conflate these two.
  2. This important superclass relationship was, thanks to historic accidents, only implemented quite recently (early 2015) in GHC (version 7.10). If you are using a GHC version older than that, this class constraint will not exist, and so some of the practical considerations we will make next will not apply.
  3. By "semantics" we meanwhat the language allows you to say. In the case ofMaybe, the semantics allow us to express failure, as statements may fail to produce a result, leading to the statements that follow it being skipped.
  4. Of course, the functions in regular function composition are non-monadic functions whereas monadic composition takes only monadic functions.
  5. Deep into the Advanced Track, we will cover the theoretical side of the story in thechapter on Category Theory.
Retrieved from "https://en.wikibooks.org/w/index.php?title=Haskell/Understanding_monads&oldid=4446473"
Category:

[8]ページ先頭

©2009-2026 Movatter.jp