To write a trait definition for UptimeClient that abstracts over the return
types, we can add a type constructor F[_] as a type parameter:
We can then extend it with two traits that bind F to Future and Id
respectively:
To make sure that the code compiles, we write out the method signatures for
getUptime in each case:
We can now have a TestUptimeClient as a full class based on Map[String, Int]
with no need for relying on Future:
Exercise 8.2: Abstracting over Monads
We can rewrite the method signatures of UptimeService so that it abstracts
over the context as follows:
To get the previous implementation working, we need to not only prove the
compiler that F has an Applicative, but also add a few syntax imports so
that we can call traverse and map:
If we use foldLeft with an empty list as the accumulator and :: as the
binary operator we get back the reversed list:
On the other hand, if we use foldRight with an empty list as the accumulator
and :: as the binary operator we get back the same list:
Exercise 7.1.3: Scaf-fold-ing Other Methods
The following are implementations of the map, flatMap, filter and sum
methods for Lists in terms of foldRight:
The sum method makes use of the Numeric type class from the Scala standard
library. In the spirit of this book, we could also have created an
implementation that uses the Monoid type class instead.
Exercise 7.2.2.1: Traversing with Vectors
The result of the provided expression is going to be a Vector of Lists, with
each being the pairwise combination of the elements from both Vectors:
If we use a list of three parameters, we will get back a Vector of Lists
again, but this time each list is going to be of three elements and we will have
one list per each possible triple combination of elements from each of the
Vectors:
Exercise 7.2.2.2: Traversing with Options
The return type of the process method is Option[List[Int]] and it will
return a Some of the provided input if all integers in the list argument are
even and None otherwise. Therefore, it will produce the following for the
first call:
And the following for the second call:
Exercise 7.2.2.3: Traversing with Validated
The provided method will return a Valid with the list argument when all
integers of it are even or an Invalid with a List of String for each
element that is not even otherwise. Therefore, we get the following for the
first call:
The reason product for List produces the Cartesian product is because List
forms a Monad, and product is implemented in terms of flatMap. So
Semigroupal[List].product(List(1, 2), List(3, 4)) is the same as:
Which results in the Cartesian product.
Exercise 6.4.0.1: Parallel List
List does have a Parallel instance. It zips the lists instead of doing the
Cartesian product. This can be exhibited by the following snippet:
We can rewrite Response using a monad transformer as follows:
We can implement getPowerLevel as follows. Note that we need an implicit
ExecutionContext in scope so that we can have an instance of Functor for
Future, even if we just create our Futures with Future.successful (which
doesn’t need one). We are using the global ExecutionContext for convenience.
To implement canSpecialMove we can request the power levels of each autobot
and check if their sum is greater than 15. We can use flatMap on EitherT
which makes sure that errors being raised on calls to getPowerLevel stop the
sequencing and have canSpecialMove return a Response with the appropriate
error message.
To implement tacticalReport, we need to produce a String from a Future, so
we must use Await.
We have pure and flatMap to define map. We want to start from an F[A]
and get to an F[B] from a function A => B. As such, we want to call
flatMap over the value. We can’t use func directly, though. However, we can
produce a function that would lift our value to an F using pure (a =>
pure(func(a))):
Exercise 4.3.1: Monadic Secret Identities
pure, map and flatMap for Id can be implemented as follows:
Since Id[A] is just a type alias for A, we can notice that we avoid all
boxing in the implementations and, due to that fact, flatMap and map are
identical.
Exercise 4.4.5: What is Best?
The answer depends on what we are looking for in specific instances, but some
things that the previous examples for error handling don’t cover are:
We can’t accumulate errors. The proposed examples all fail fast.
We can’t tell exactly where the error was raised.
It’s not easy to do error recovery.
Exercise 4.5.4: Abstracting
A possible implementation for validateAdult is the following:
If age is greater than or equal to 18, we summon an Applicative for F
(which we must have in scope due to MonadError) and lift the age to F. If
age is less than 18, we use the MonadError instance we have in scope to lift
an IllegalArgumentException to F.
Exercise 4.6.5: Safer Folding using Eval
One way to make the naive implementation of foldRight stack safe using Eval
is the following:
We defer the call to the recursive step and then map over it to apply fn, all
within the context of Eval.
Exercise 4.7.3: Show Your Working
A possible rewrite of factorial so that it captures the log messages in a
Writer is the following:
We can show that this allows us to reliably separate the logs for concurrent
computations because we have the logs for each instance captured in each
Writer instance:
Exercise 4.8.3: Hacking on Readers
To create a type alias for a Reader that consumes a Db we want to fix the
first type parameter of Reader to Db, while still leaving the result type as
a type parameter:
The findUsername and checkPassword functions can be implemented as follows:
The checkLogin method can be implemented as follows:
We are making use of the findUsername and checkPassword methods. There are
two scenarios in which checkLogin can return a false for a given Db: when
the username doesn’t exist and when the password doesn’t match.
Exercise 4.9.3: Post-Order Calculator
A possible implementation of evalOne with no proper error handling is the
following:
We’re not told which operands to support, so I assumed at least +, *, -
and /.
For the evalAll implementation, we’re not told what to do in case the input is
empty. I assumed it would be OK to just have an exception thrown (since that was
the case before), and relied on reduce over the evalOne calls:
The evalInput method can rely on a call to evalAll after splitting the
input by whitespaces:
Exercise 4.10.1: Branching out Further with Monads
One implementation of Monad for Tree is the following:
However, tailRecM isn’t tail-recursive. We can make it tail-recursive by
making the recursion explicit in the heap. In this case, we’re using two mutable
stacks: one of open nodes to visit and one of already visited nodes. On that
stack, we use None to signal a non-leaf node and a Some to signal a leaf
node.