In my previous blog post about categories and functors I already threatened you with the possibility of a follow-up. Well, here we go. Actually I won’t talk about category theory this time, but about an important abstraction from the world of advanced (at least in my understanding) functional programming which turns out to be a close relative to the now well understood functor.
Just in case you don’t remember exactly what a functor looks like, here comes the definition:
1 2 3 4 5 6 7 8 9 10
For the sake of simplicity we will stick to the more specific
Functor definition throughout the rest of this post. Such a functor is an endofunctor, because its source and target are the same (the category of Scala types and Scala functions). Maybe you remember that such a functor can be regarded as a provider of a computational context: The function
f: A => B you give to
fmap is lifted into the functor’s context which means that it is executed (maybe once, maybe several times or maybe even not at all) under the control of the functor.
As an example let’s look at how the
OptionFunctor defined in the previous blog post is working: If we give
Some the given function will be invoked, if we give it a
None it won’t be invoked.
1 2 3 4 5 6
So far, so good. Using functors we can lift functions of arity-1 into a computational context.
But what if we have a function of higher arity? Can we still use a functor to lift a function of, let’s say, arity-2?
Let’s look at what happens if we call
fmap to partially apply an arity-2 function to its first argument within the computational context of an
1 2 3 4 5
What we get back is an
Option[Int => Int], i.e. the “rest” of the partially applied function wrapped in an
Option. Now we have a problem, because we cannot give this lifted function to another call of
1 2 3 4 5
Of course we cannot, because
fmap expects a pure function, not a lifted one. And that’s the moment when applicatives enter the stage. The idea is simple and follows intutively from what we have just seen: Instead of
fmap taking a pure function, an
Applicative defines the method
apply taking a lifted function. And it defines the method
pure to lift pure functions. Using these it is perfectly possible to partially apply an arity-n function to all of its arguments within a computational context. Before we look at our example from this new perspective, let’s code up our new abstraction:
1 2 3 4 5 6 7 8 9 10 11 12
As you can see, there is a strong relation between functors and applicatives: Each applicative is a functor and by one of the laws for applicatives the following has to hold true:
fmap = apply ο pure. Well, this law is pretty intuitive, because it makes sure we can use an applicative as a functor, i.e. for a pure arity-1 function, and it will behave as expected.
In order to be able to work with applicatives we need some scaffolding and for our
Option example we need an implementation of
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Now let’s look at our example from above with an arity-2 function. Using a functor we got stuck, but using an applicative we can make it all the way through the two arguments:
Ain’t that nice? You might answer, that this code looks cumbersome and is not necessary at all. Regarding the first argument please read on. Regarding the second: Yes, of course we don’t need an applicative for
Option, because it already offers
flatMap which does the job. But with this type class approach we can deal with any class, e.g. with
Either from the Scala standard library or with classes from our own projects.
You have seen the basic principle of applicatives: We can apply functions of arbitrary arity (well, greater or even one) to its arguments within a computational context. As functors provide exactly this for arity-1, applicatives are generalized functors.
Thanks to Scala’s flexibility we can of course do much better than above. Using a little pimp my library and some operators we can get something that’s elegant and useful. Luckily the scalaz folks have already done this, so I will just show two ways of expressing the above example using this awesome library:
1 2 3 4 5
Now this is really nice, isn’t it? Especially the second one looks very concise and intuitive. And as I stated above we can use it for types that don’t bring helpful methods like
flatMap. Let’s conclude with another example using
Either which is a perfect candidate to be used for results that might fail with well defined errors:
1 2 3 4 5