In the last installation of this series, we covered the Functor and now we take a turn to discuss the Applicative
type class.
It’s all about functions
Being functional programmers, the thing we embrace most are functions. Despite scala being an object-functional
language, I fell in love with it, due to it’s possibilities to write code using the functional paradigm and not state mutating object nonsense. And I believe it is a good exercise, if you (like me) come from a java background to emphasize functions over objects first. It is a different way of approaching problems, and it needs getting used to, but it pays of in the end.
Having said all that, let us remember that functions come in different flavours, they can have one parameter or multiple parameters and return a value and if they don’t return a value, but Unit
we basically know that this function stinks. It is no wonder then that functions can be transformed to take on a new Gestalt
.
What this tells us, that we can turn any function expecting multiple parameters in a function with one parameter (tupled), or in function were we can apply the parameters “lazily” in the curried version. We can also revert these forms into the original “multiple parameter” style. And don’t worry about the different use of “X” and “-“ in the table, I just got carried away.
The problem with our Functor typeclass and map
map
takes a function f: A => B
and applies this function in a context F
. What about functions taking more than one parameter? For example if we have a function f: A => B => C
and do Functor[Option].map(Some(3))(f)
we would end up with a value of Some(B => C)
:
val add : Int => Int => Int = x => y => x + y
Functor[Option].map(Some(3))(add)
add: Int => (Int => Int) = <function1>
//>>> res0: Option[Int => Int] = Some(<function1>)
- Problem 1: we cannot continue to map over the resulting value, which is a function in the context of Option, whereas
map
does not provide that type signature. Try on your own, to see what happens, if you try to continue to map over the resulting value (spoiler: compiler errors happen, but which ones?) - Problem 2: we cannot supply 2 parameters to map as in the fictional
Functor[Option].map(Some(3), Some(4)).map(add)
Workaround
One workaround to our problem with map, could be to transform any function to their tupled representation, and map over a tuple, as in:
def add(x: Int, y: Int) = x + y
Functor[Option].map(Some((3,4)))((add _).tupled)
//>>> res0: Option[Int] = Some(7)
- Problem 3: Though this seems to work, it is clumsy, and what happens, if we do not have all values at hand, e.g. if we want to add
Some(3)
andNone
. How would we build a tuple from that?
Applicative
Having understood these problems, we can introduce the Applicative
type class. It solves our problems, and as an aside has some very funny properties, which make it super powerful. Boiled down to its core interface the Applicative
looks like this:
trait Applicative[F[_]] {
// other functions ommitted for now
def ap[A, B](fa: => F[A])(f: => F[A => B]): F[B]
}
Let’s have a look at the problems which we have described above and see how an Applicative
might help. The signature of ap
seems to suggest that we can apply a value A in context F
to a function A=>B which resides in a context F
and get a result B
in the context of F
. WTF?
Or in plain english: If we have an optional parameter and apply it to an optional function we get an optional result. The same for the context List
: if we have a list of parameter values and a apply those to a list of functions we get a list of results. If the list of functions or parameters would be empty, we would get an empty list of results. Cool, eh? (we observe that an Applicative executes functions in a context, and preserves that context.)
Interlude
Before we can consider the other problems, we introduce two more functions to the Applicative trait:
trait Applicative[F[_]] extends Functor[F] {
def pure[A](a: A): F[A]
def ap[A, B](fa: => F[A])(f: => F[A => B]): F[B]
override def map[A, B](fa: F[A])(f: A => B): F[B] =
ap(fa)(pure(f))
}
One is pure
(often aliased as point
) and one is the fact that every Applicative is also a Functor, and we can implement map
in terms of the functions of ap
and pure
. pure
is the function which allows us to put a single value A
into some context F
. In plain scala this would often be the apply
method of the companion object, and with pure
we have the means to abstract over the construction of values in a context. Every Applicative
is also a Functor
as we can simply lift the function f into the context F
using pure
and then use ap
to apply the value to the function inside the context.
Here are some sample instances:
val optionApplicativeInstance : Applicative[Option] = new Applicative[Option] {
override def pure[A](a: A): Option[A] = Some(a)
override def ap[A, B](fa: => Option[A])(f: => Option[(A) => B]): Option[B] = {
(fa, f) match {
case (Some(fa1), Some(f1)) => Some(f1(fa1))
case _ => None
}
}
}
val listApplicativeInstance : Applicative[List] = new Applicative[List] {
override def pure[A](a: A): List[A] = List(a)
override def ap[A, B](fa: => List[A])(f: => List[(A) => B]): List[B] = {
for { // cheating as I implement this using flatMap which is a function of monads.
fa1 <- fa
f1 <- f
} yield (f1(fa1))
}
}
Problem solving continued
Let’s try to tackle problem 2 and find a way to supply 2 arguments to a function f: (A,B) => C
. In fact Applicative
contains lots of derived functions, and some of them are ap2
… ap22
, where the number denotes the number of arguments of our function.
def ap2[A,B,C](fa: => F[A],fb: => F[B])(f: F[(A,B) => C]) : F[C] =
(ap(fb)(ap(fa)(map(f)(_.curried))))
Step by Step:
- we
map
over the functionf
, thus looking into the context, and create the curried version of our functionA => B => C
- after leaving
map
, we haveF[A => B => C]
- we
ap
plyfa
, resulting inF[B => C]
and ap
plyfb
which results inF[C]
.
q.e.d. Yikes!
There is another group of functions associated with Applicative
which are the applyN
functions. The difference to apN
, is that they do not take function(s) in a context, but just a plain function and put it in the context via pure
for us.
def apply2[A,B,C](fa: => F[A],fb: => F[B])(f: (A,B) => C) : F[C] =
ap2(fa,fb)(pure(f))
Notes
The naming conventions and precise structure within Applicative
can vary between differnt type class libraries. For example most of the functions in scalaz are implemented in Apply
with Applicative
adding the point/pure
function. In my own testbed [simplez][http://github.com/inoio/simplez] I unified this classes and the type classes in the play framework, again use pure
instead of point
and does not inherit directly from Functor
, but implements map
directly. But in the end, it all boils down to the same thing.
Further learning
The books Functional programming in Scala
and Scala in Depth
both cover the essentials of Applicative
s very well.
That’s it?
No, there will be a part 2 for Applicatives in this series covering the ApplicativeBuilder
and a (gosh!) practical example. Slowly but steady we continue to build our type class toolbox. As we will see, the previous classes will pop up here and there again, especially when we want to use Applicatives
to “collect information” while still keeping the control flow intact.
For further articles in this series: TypeClass101
Kommentare