How do these functions in the Applicative typeclass work?
(<*>) :: f (a -> b) -> f a -> f b
(*>) :: f a -> f b -> f b
(<*) :: f a -> f b -> f a
(That is, if they weren’t operators, what might they be called?)
As a side note, if you could rename pure to something more friendly to non-mathematicians, what would you call it?
Knowing your math, or not, is largely irrelevant here, I think. As you’re probably aware, Haskell borrows a few bits of terminology from various fields of abstract math, most notably Category Theory, from whence we get functors and monads. The use of these terms in Haskell diverges somewhat from the formal mathematical definitions, but they’re usually close enough to be good descriptive terms anyway.
The
Applicativetype class sits somewhere betweenFunctorandMonad, so one would expect it to have a similar mathematical basis. The documentation for theControl.Applicativemodule begins with:Hmm.
Not quite as catchy as
Monad, I think.What all this basically boils down to is that
Applicativedoesn’t correspond to any concept that’s particularly interesting mathematically, so there’s no ready-made terms lying around that capture the way it’s used in Haskell. So, set the math aside for now.If we want to know what to call
(<*>)it might help to know what it basically means.So what’s up with
Applicative, anyway, and why do we call it that?What
Applicativeamounts to in practice is a way to lift arbitrary functions into aFunctor. Consider the combination ofMaybe(arguably the simplest non-trivialFunctor) andBool(likewise the simplest non-trivial data type).The function
fmaplets us liftnotfrom working onBoolto working onMaybe Bool. But what if we want to lift(&&)?Well, that’s not what we want at all! In fact, it’s pretty much useless. We can try to be clever and sneak another
BoolintoMaybethrough the back……but that’s no good. For one thing, it’s wrong. For another thing, it’s ugly. We could keep trying, but it turns out that there’s no way to lift a function of multiple arguments to work on an arbitrary
Functor. Annoying!On the other hand, we could do it easily if we used
Maybe‘sMonadinstance:Now, that’s a lot of hassle just to translate a simple function–which is why
Control.Monadprovides a function to do it automatically,liftM2. The 2 in its name refers to the fact that it works on functions of exactly two arguments; similar functions exist for 3, 4, and 5 argument functions. These functions are better, but not perfect, and specifying the number of arguments is ugly and clumsy.Which brings us to the paper that introduced the Applicative type class. In it, the authors make essentially two observations:
Functoris a very natural thing to doMonadNormal function application is written by simple juxtaposition of terms, so to make “lifted application” as simple and natural as possible, the paper introduces infix operators to stand in for application, lifted into the
Functor, and a type class to provide what’s needed for that.All of which brings us to the following point:
(<*>)simply represents function application–so why pronounce it any differently than you do the whitespace “juxtaposition operator”?But if that’s not very satisfying, we can observe that the
Control.Monadmodule also provides a function that does the same thing for monads:Where
apis, of course, short for “apply”. Since anyMonadcan beApplicative, andapneeds only the subset of features present in the latter, we can perhaps say that if(<*>)weren’t an operator, it should be calledap.We can also approach things from the other direction. The
Functorlifting operation is calledfmapbecause it’s a generalization of themapoperation on lists. What sort of function on lists would work like(<*>)? There’s whatapdoes on lists, of course, but that’s not particularly useful on its own.In fact, there’s a perhaps more natural interpretation for lists. What comes to mind when you look at the following type signature?
There’s something just so tempting about the idea of lining the lists up in parallel, applying each function in the first to the corresponding element of the second. Unfortunately for our old friend
Monad, this simple operation violates the monad laws if the lists are of different lengths. But it makes a fineApplicative, in which case(<*>)becomes a way of stringing together a generalized version ofzipWith, so perhaps we can imagine calling itfzipWith?This zipping idea actually brings us full circle. Recall that math stuff earlier, about monoidal functors? As the name suggests, these are a way of combining the structure of monoids and functors, both of which are familiar Haskell type classes:
What would these look like if you put them in a box together and shook it up a bit? From
Functorwe’ll keep the idea of a structure independent of its type parameter, and fromMonoidwe’ll keep the overall form of the functions:We don’t want to assume that there’s a way to create an truly “empty”
Functor, and we can’t conjure up a value of an arbitrary type, so we’ll fix the type ofmfEmptyasf ().We also don’t want to force
mfAppendto need a consistent type parameter, so now we have this:What’s the result type for
mfAppend? We have two arbitrary types we know nothing about, so we don’t have many options. The most sensible thing is to just keep both:At which point
mfAppendis now clearly a generalized version ofzipon lists, and we can reconstructApplicativeeasily:This also shows us that
pureis related to the identity element of aMonoid, so other good names for it might be anything suggesting a unit value, a null operation, or such.That was lengthy, so to summarize:
(<*>)is just a modified function application, so you can either read it as “ap” or “apply”, or elide it entirely the way you would normal function application.(<*>)also roughly generalizeszipWithon lists, so you can read it as “zip functors with”, similarly to readingfmapas “map a functor with”.The first is closer to the intent of the
Applicativetype class–as the name suggests–so that’s what I recommend.In fact, I encourage liberal use, and non-pronunciation, of all lifted application operators:
(<$>), which lifts a single-argument function into aFunctor(<*>), which chains a multi-argument function through anApplicative(=<<), which binds a function that enters aMonadonto an existing computationAll three are, at heart, just regular function application, spiced up a little bit.