Applicatives are useful when dealing with multiple independent effectful values
We have just seen that Functor
allows us to abstract the composition pattern for one effectful value.
That pattern being the ability to transform the "wrapped" value, by applying a function. However, often we will have multiple
effectful values in our programs, and want to combine them. Applicative
provides us with a way to combine two effectful values
that are independent.
The core operation is called zip
in our implementation, and takes an F[A]
and an F[B]
and tuples the inner values
to create F[(A, B)]
. Essentially it "runs" both the F[A]
computation and the F[B]
computation and tuples the results.
For example
Defined("abc").zip(Defined("def")) // Defined(("abc", "def"))
Defined("abc").zip(Undefined) // Undefined
Notice that in the above example we are composing two effectful Maybe
values, additionally we are retaining the
additional semantics added to the composition by the Maybe
effect. We are using the "definedness" semantics encoded
by the Maybe
data type, so if either argument to zip
is Undefined
then the result is also Undefined
The definition of Applicative
looks like this. Note that this is a typeclass that acts on a type constructor F[_]
trait Applicative[F[_]] {
def pure[A](a: A): F[A]
def zip[A, B](fa: F[A], fb: F[B]): F[(A, B)]
}
Notice there is also a function called pure
defined. It wraps a a concrete value into the F[_]
effect
in a trivial way. For example (a: A) => Defined(a)
for our Maybe
data type.
-
In
applicatives.scala
theApplicative
trait has been defined. -
Similar to the previous section, an
implicit class
has been defined to provide dot syntax. -
Notice that
Applicative
extendsFunctor
. This is because in the traditional encoding ofApplicative
,Functor.map
can be implemented in terms of the primitive operationap
. In our encoding we have usedzip
as the primitive operation instead, since it is easier to understand, andzip
+map
are equally as powerful asap
. In fact, theApplicative
type class we are using implements the traditionalap
combinator usingzip
andmap
Exercise 1 – Maybe applicative
- Implement an
Applicative
instance forMaybe
- Run
ApplicativeLaws
usingsbt 'testOnly *ApplicativeLaws'
to check your implementation. Note, there will still be some failing tests forCanFail
applicative at this stage.
Exercise 2 – CanFail applicative
- Implement an
Applicative
instance forCanFail
- Run
AppicativeLaws
usingsbt 'testOnly *ApplicativeLaws'
to check your implementation. All the tests should pass now.
Exercise 3 – Implement sequence
-
Implement the
sequence
function. If you are familiar with theFuture.sequence
method, this method should look familiar. It does essentially the same thing, but in a much more generic way. Since it is more generic, it means we can use it forList[CanFail]
andList[Maybe]
or a list of anything that has an instance of Applicative.If you are not familiar with
Future.sequence
it takes aSeq[Future[A]]
and returns aFuture[Seq[A]]
where the innerSeq
of the resultingFuture
contains that result of each of the futures in the originalSeq
.Tip: Start with an
F
of empty list, fold overfas
adding the innerA
to the list at each step. In your fold you should have anF[List[A]]
as your accumulator and combine it withF[A]
at each step.
Exercise 4 – Use sequence
- Implement the
asInts
function which converts aList[String]
into aList[Int]
in theCanFail
effect, since converting aString
to anInt
can fail. - Run
ApplicativeSpec
usingsbt 'testOnly *ApplicativeSpec'
to check your implementation.