Scala Functional Programming Guide
Scala Functional Programming Guide
A hands-on approach
Gabriel Volpe
Second Edition
Contents
Preface 1
Acknowledgments 3
People . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Software . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Fonts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Dependency versions 7
Prerequisites 9
ii
Contents
iii
Contents
iv
Contents
v
Contents
vi
Preface
Scala is a hybrid language that mixes both the Object-Oriented Programming (OOP)
and Functional Programming (FP) paradigms. This allows you to get up-and-running
pretty quickly without knowing the language in detail. Over time, as you learn more,
you are hopefully going to appreciate what makes Scala great: its functional building
blocks.
Pattern matching, folds, recursion, higher-order functions, etc. If you decide to continue
down this road, you will discover the functional subset of the community and its great
ecosystem of libraries.
Sooner rather than later, you will come across the Cats1 library and its remarkable
documentation. You might even start using it in your projects! Once you get familiar
with the power of typeclasses such as Functor, Monad, and Traverse, I am sure you will
love it.
As you evolve into a functional programmer, you will learn about functional effects and
referential transparency. You might as well start using the popular IO Monad present in
Cats Effect2 and other similar libraries.
One day you will need to process a lot of data that doesn’t fit into memory; a suitable
solution to this engineering problem is streaming. While searching for a valuable can-
didate, you might stumble upon a purely functional streaming library: Fs23 . You will
quickly learn that it is also a magnificent library for control flow.
A requirement to build a RESTful API4 will more likely come down your way early on
in your career. Http4s5 leverages the power of Cats Effect and Fs2 so you can focus on
shipping features while remaining on functional land.
You might decide to adopt a message broker as a communication protocol between
microservices and to distribute data. You name it: Kafka, Pulsar, RabbitMQ, to mention
a few. Each of these wonderful technologies has a library that can fulfill every client’s
needs.
1
https://typelevel.org/cats
2
https://typelevel.org/cats-effect
3
https://fs2.io
4
https://restfulapi.net/
5
https://http4s.org/
1
Preface
Unless you have taken the stateless train, you will need a database or a cache as well.
Whether it is PostgreSQL, ElasticSearch, or Redis, the Scala FP ecosystem of libraries
has got your back.
So far so good! There seems to be a wide set of tools available to write a complete purely
functional application and finally ditch the enterprise framework.
At this point, you find yourself in a situation where many programmers that are en-
thusiastic about functional programming find themselves: needing to deliver business
value in a time-constrained manner.
Answering this and many other fundamental questions are the aims of this book. Even
if at times it wouldn’t give you a straightforward answer, it will show you the way. It
will give you choices and hopefully enlighten you.
Throughout the following chapters, we will develop a shopping cart application that
tackles system design from different angles. We will architect our system, making both
sound business and technical decisions at every step, using the best possible techniques
I am knowledgeable of at this moment.
2
Acknowledgments
One can only dream of starting writing a book and making it over the finish line. Yet,
I managed to do this twice! Though, this would have been an impossible task without
the help of many people that had supported me over time, as well as many open-source
and free software I consider indispensable.
I am beyond excited and can only be thankful to all of you.
3
Acknowledgments
People
I consider myself incredibly lucky to have had all these great human beings influencing
the content of this book one way or another. This humble piece of work is dedicated:
Last but not least, this edition is dedicated to all the people that make the Typelevel
ecosystem as great as it is nowadays, especially to the maintainers and contributors of
my two favorite Scala libraries: Cats Effect and Fs2. This book wouldn’t exist without
all of your work! #ScalaThankYou
Although the book was thoroughly reviewed, I am the sole responsible for all of the
opinionated sentences, and any remaining mistakes are only mine.
1
https://twitter.com/impurepics
4
Acknowledgments
Software
As a grateful open-source software contributor, this section is dedicated to all the free
tools that have made this book possible.
• NeoVim2 : my all-time favorite text editor, used to write this book as well as to
code the Shopping Cart application.
• Pandoc3 : a universal document converter written in Haskell, used to generate
PDFs and ePub files.
• LaTeX4 : a high-quality typesetting system to produce technical and scientific doc-
umentation, as well as books.
2
https://neovim.io/
3
https://pandoc.org/
4
https://www.latex-project.org/
5
Acknowledgments
Fonts
This book’s main font is Latin Modern Roman5 , distributed under The GUST Font
License (GFL)6 . Other fonts in use are listed below.
• JetBrainsMono7 for code snippets, available under the SIL Open Font License 1.18
• Linux Libertine9 for some Unicode characters, licensed under the GNU General
Public License version 2.0 (GPLv2)10 and the SIL Open Font License11 .
5
https://tug.org/FontCatalogue/latinmodernroman/
6
https://www.ctan.org/license/gfl
7
https://www.jetbrains.com/lp/mono/
8
https://github.com/JetBrains/JetBrainsMono/blob/master/OFL.txt
9
https://sourceforge.net/projects/linuxlibertine/
10
https://opensource.org/licenses/gpl-2.0.php
11
https://scripts.sil.org/cms/scripts/page.php?item_id=OFL
6
Dependency versions
At the moment of writing, all the standalone examples use Scala 2.13.5 and sbt 1.5.3, as
well as the following dependencies defined in this minimal build.sbt1 file.
1
https://gist.github.com/gvolpe/04b31a5caa875f8f16bcd1d12b72face
7
Dependency versions
Please note that Scala Steward2 keeps on updating the project’s dependencies on a daily
basis, which may not reflect the versions described in this book.
2
https://github.com/fthomas/scala-steward
8
Prerequisites
Unfortunately, these topics are quite lengthy to be explained in this book, so readers are
expected to be acquainted with them. However, some examples will be included, and
this might be all you need.
If the requirements feel overwhelming, it is not because the entire book is difficult, but
rather because some specific parts might be. You can try to read it, and if at some point
you get stuck, you can skip that section. You could also make a pause, go to read about
these resources, and then continue where you left off.
Remember that we are going to develop an application together, which will help you
learn a lot, even if you haven’t employed these techniques and libraries before.
1
https://underscore.io/books/scala-with-cats/
2
https://essentialeffects.dev/
3
https://typelevel.org/cats-effect/
4
https://typelevel.org/blog/2016/08/21/hkts-moving-forward.html
5
https://typelevel.org/cats/typeclasses.html
6
https://typelevel.org/blog/2017/05/02/io-monad-for-cats.html
7
https://en.wikipedia.org/wiki/Referential_transparency
9
How to read this book
For conciseness, most of the imports and some datatype definitions are elided from the
book, so it is recommended to read it by following along the two Scala projects that
supplement it.
The first project includes self-contained examples that demonstrate some features or
techniques explained independently.
The latter contains the source code of the full-fledged application that we will develop
in the next ten chapters, including a test suite and deployment instructions.
Bear in mind that the presented Shopping Cart application only acts as a guideline. To
get a better learning experience, readers are encouraged to write their own application
from scratch; getting your hands dirty is the best way to learn.
There is also a Gitter channel3 where you are welcome to ask any kind of questions
related to the book or functional programming in general.
1
https://github.com/gvolpe/pfps-examples
2
https://github.com/gvolpe/pfps-shopping-cart
3
https://gitter.im/pfp-scala/community
10
How to read this book
Notes
A note on what’s being discussed
Tips
A tip about a particular topic
Warning
Claim or decision based on the author’s opinion
If you are reading this on Kindle, you won’t see colors, unfortunately.
11
Chapter 1: Best practices
Before we get to analyzing the business requirements and writing the application, we
are going to explore some design patterns and best practices. A few well-known; others
not so standard and biased towards my preferences.
These will more likely appear at least once in the application we will develop, so you
can think of this chapter as a preparation for what’s to come.
12
Chapter 1: Best practices
Strongly-typed functions
One of the most significant benefits of functional programming is that it lets us rea-
son about functions by looking at their type signature. Yet, the truth is that these
are commonly created by us, imperfect humans, who often end up with weakly-typed
functions.
For instance, let’s look at the following function.
Do you see any problems with it? Let’s see how we can use it.
$ lookup("aeinstein@research.com", "aeinstein")
$ lookup("aeinstein", "123")
$ lookup("", "")
See the issue? It is not only easy to confuse the order of the parameters but it is also
straightforward to feed our function with invalid data! So what can we do about it? We
could make this better by introducing value classes.
Value classes
In vanilla Scala, we can wrap a single field and extend the AnyVal abstract class to avoid
some runtime costs. Here is how we can define value classes for username and email.
$ lookup(Username("aeinstein"), Email("aeinstein@research.com"))
Or can we?
$ lookup(Username("aeinstein@research.com"), Email("aeinstein"))
$ lookup(Username("aeinstein"), Email("123"))
$ lookup(Username(""), Email(""))
13
Chapter 1: Best practices
Smart constructors are functions such as mkUsername and mkEmail, which take a raw value
and return an optional validated one. The optionality can be denoted using types such
as Option, Either, Validated, or any other higher-kinded type.
So let’s pretend that these functions validate the raw values properly and give us back
some valid data. We can now use them in the following way.
(
mkUsername("aeinstein"),
mkEmail("aeinstein@research.com")
).mapN {
case (username, email) => lookup(username, email)
}
(
mkUsername("aeinstein"),
mkEmail("aeinstein@research.com")
).mapN {
case (username, email) =>
lookup(username.copy(value = ""), email)
}
Unfortunately, we are still using case classes, which means the copy method is still there.
A proper way to finally get around this issue is to use sealed abstract case classes.
Or sealed abstract classes, where we need to add the val keyword to make value
accessible from the outside.
Having this encoding in combination with smart constructors will mitigate the issue at
the cost of boilerplate and more memory allocation.
14
Chapter 1: Best practices
Newtypes
Value classes are fine in most cases, but we haven’t talked about their limitations and
performance issues. In many cases, Scala needs to allocate extra memory when using
value classes, as described in the article Value classes and universal traits1 . Quoting the
relevant part:
The language cannot guarantee that these primitive type wrappers won’t actually allo-
cate more memory, in addition to the pitfalls described in the previous section.
Thus, my recommendation is to avoid value classes and sealed abstract classes completely
and instead use the Newtype2 library, which gives us zero-cost wrappers with no runtime
overhead.
This is how we can define our data using newtypes.
import io.estatico.newtype.macros._
It uses macros, for which we need the macro paradise compiler plugin in Scala versions
below 2.13.0, and only an extra compiler flag -Ymacro-annotations in versions 2.13.0 and
above.
Despite eliminating the extra allocation issue and removing the copy method, notice how
we can still trigger the functionality incorrectly.
Email("foo")
This means that smart constructors are still needed to avoid invalid data.
Notes
Newtypes do not solve validation; they are just zero-cost wrappers
Newtypes can also be constructed using the coerce method in the following way.
1
https://docs.scala-lang.org/overviews/core/value-classes.html
2
https://github.com/estatico/scala-newtype
15
Chapter 1: Best practices
import io.estatico.newtype.ops._
"foo".coerce[Email]
Though, this is considered an anti-pattern, and its use is highly discouraged when we
know the concrete type. The only reason for its existence is so we can write polymorphic
code for newtypes, which we will rarely ever need.
Refinement types
We have seen how newtypes help us tremendously in our strongly-typed functions quest.
Nevertheless, it requires smart constructors to validate input data, which adds boilerplate
and leaves us with a bittersweet feeling. Still, do not give up hope as we have one last
card to play: refinement types, provided by the Refined3 library.
Refinement types allow us to validate data at compile time as well as at runtime. Let’s
see an example.
import eu.timepit.refined.types.string.NonEmptyString
We are saying that a valid username is any non-empty string; though, we could also say
that a valid username is any string containing the letter ‘g’, in which case, we would need
to define a custom refinement type instead of using a built-in one like NonEmptyString.
The following example demonstrates how we can do this.
import eu.timepit.refined.api.Refined
import eu.timepit.refined.collection.Contains
By saying that it should contain a letter ‘g’ (using string literals), we are also implying
that it should be non-empty. If we try to pass some invalid arguments, we are going to
get a compiler error.
import eu.timepit.refined.auto._
$ lookup("") // error
$ lookup("aeinstein") // error
$ lookup("csagan") // compiles
3
https://github.com/fthomas/refined
16
Chapter 1: Best practices
Refinement types are great and let us define custom validation rules. Though, in many
cases, a simple rule applies to many possible types. For example, a NonEmptyString
applies to almost all our inputs. In such cases, we can combine forces and use Refined
and Newtype together!
These two types share the same validation rule, so we use refinement types, but since they
represent different concepts, we create a newtype for each of them. This combination is
ever so powerful that I couldn’t recommend it enough.
Another feature that makes the Refined library very appealing is its integration with
multiple libraries such as Circe4 , Doobie5 , and Monocle6 , to name a few. Having support
for these third-party libraries means that we don’t need to write custom refinement types
to integrate with them as the most common ones are provided out of the box.
Runtime validation
Up until now, we have seen how refinement types help us validate data at compile time,
as well as combining them together with newtypes. Yet, we haven’t talked much about
runtime validation, which is something we need in the real world.
Almost every application needs to deal with runtime validation. For example, we can
not possibly know what values we are going to receive from HTTP requests or any other
service, so compile-time validation is not an option here.
Refined gives us a generic function for this purpose, which is roughly defined as follows.
It is not a coincidence that the type parameter is named P, which stands for predicate.
In the following example, pretend str represents an actual runtime value.
4
https://github.com/circe/circe
5
https://github.com/tpolecat/doobie
6
https://github.com/optics-dev/Monocle
17
Chapter 1: Best practices
import eu.timepit.refined._
Most refinement types provide a convenient from method, which take the raw value and
returns a validated one or an error message. For example, the following example is
equivalent to the one above.
It also helps with type inference so it is recommended to use from over the generic refineV.
We can add the same feature to any custom refinement type too.
import eu.timepit.refined.api.RefinedTypeOps
import eu.timepit.refined.numeric.Greater
Summarizing, Refined lets us perform runtime validation via Either, which forms a
Monad. This means validation is done sequentially. It would fail on the first error en-
countered during multiple value validation. In such cases, it is usually a better choice to
go for cats.data.Validated, which is similar to Either, except it only forms an Applica-
tive.
In practical terms, this means it can validate data simultaneously and accumulate errors
instead of validating data sequentially and failing fast on the first encountered error.
A common type for such purpose is ValidatedNel[E, A], which is an alias for Vali-
dated[NonEmptyList[E], A]. We can convert those refinement results to this type via the
toValidatedNel extension method.
18
Chapter 1: Best practices
Invalid(
NonEmptyList(Predicate isEmpty() did not fail.,
Predicate failed: (3 > 5).)
)
Evaluating this function with the previous inputs yields a similar result.
Left(
NonEmptyList(Predicate isEmpty() did not fail.,
Predicate failed: (3 > 5).)
)
Behind the scenes, what makes this work is the cats.Parallel instance for Either and
Validated, which abstracts over monads which support parallel composition via some
related Applicative.
We are able to accumulate errors because of the Semigroup constraint on E. In our exam-
ples, E = String.
Furthermore, since we generally use newtypes together with refinement types, there is
something else to consider. Let’s look at the following Person domain model.
19
Chapter 1: Best practices
To perform validation, we will need an extra map to lift the refinement type into our
newtype, in addition to toEitherNel. E.g.
def mkPerson(
u: String,
n: String,
e: String
): EitherNel[String, Person] =
(
UserNameR.from(u).toEitherNel.map(UserName.apply),
NameR.from(n).toEitherNel.map(Name.apply),
EmailR.from(e).toEitherNel.map(Email.apply)
).parMapN(Person.apply)
It gets the job done at the cost of being repetitive and maybe a bit boilerplatey. Now
what if I told you this pattern can be abstracted away and reduced down to this?
import NewtypeRefinedOps._
def mkPerson(
u: String,
n: String,
e: String
): EitherNel[String, Person] =
(
validate[UserName](u),
validate[Name](n),
validate[Email](e)
).parMapN(Person.apply)
Interesting, isn’t it? This is one of the exceptional cases where I think resorting to the
infamous Coercible7 typeclass, from the Newtype library, is more than acceptable.
object NewtypeRefinedOps {
import io.estatico.newtype.Coercible
7
https://github.com/estatico/scala-newtype#coercible
20
Chapter 1: Best practices
import io.estatico.newtype.ops._
We could also make it work as an extension method of the raw value, though, this
requires two method calls instead.
(
u.as[UserName].validate,
n.as[Name].validate,
e.as[Email].validate
).parMapN(Person.apply)
You can refer to the source code for the implementation. We will skip it because it is
very similar to the validate function we’ve seen above.
I hope it was enough to convince you of the benefits of this ever powerful duo! Through-
out the development of the shopping cart application, we will get acquainted with this
technique, as it will be ubiquitous.
21
Chapter 1: Best practices
Encapsulating state
Mostly every application needs to thread some kind of state, and in functional Scala, we
have great tools to manage it properly. Whether we use MonadState, StateT, MVar, or Ref,
we can write good software by following some design guidelines.
One of the best approaches to managing state is to encapsulate state in the interpreter
and only expose an abstract interface with the functionality the user needs.
Tips
Our interface should know nothing about state
By doing so, we control exactly how the users interact with state. Conversely, if we
use something like MonadState[F, AppState] or Ref[F, AppState] directly, functions can
potentially access and modify the entire state of the application at any given time (unless
used together with classy lenses, which are a bit more advanced and less obvious than
using plain old interfaces).
In-memory counter
Let’s say we need an in-memory counter that needs to be accessed and modified by other
components. Here is what our interface could look like.
trait Counter[F[_]] {
def incr: F[Unit]
def get: F[Int]
}
It has a higher-kinded type F[_], representing an abstract effect, which most of the time
ends up being IO, but it could really be any other concrete type that fits the shape.
Next, we need to define an interpreter in the companion object of our interface, in this
case using a Ref. We will talk more about it in the next section.
import cats.Functor
import cats.effect.kernel.Ref
import cats.syntax.functor._
object Counter {
def make[F[_]: Functor: Ref.Make]: F[Counter[F]] =
Ref.of[F, Int](0).map { ref =>
new Counter[F] {
def incr: F[Unit] = ref.update(_ + 1)
def get: F[Int] = ref.get
22
Chapter 1: Best practices
}
}
}
Tips
Remember that a new Counter will be created on every flatMap call
Moreover, notice the typeclass constraints used in the interpreter: Functor and Ref.Make.
This is all we need, though, both constraints could be subsumed by a single Sync con-
straint, if we wanted that. However, it is preferred to avoid hard constraints that enable
FFI (Foreign Function Interface), i.e. side-effects.
We could also create the interpreter as a class, e.g. LiveCounter, instead of doing it via
an anonymous class in the smart constructor. This is how it was done in the first edition
of this book but my preferences have shifted towards the former over time. See below.
object LiveCounter {
def make[F[_]: Sync]: F[Counter[F]] =
Ref.of[F, Int](0).map(new LiveCounter[F](_))
}
This is up to you; go with the one you favour the most and be consistent about it.
However, be aware that in such cases, we need to make the interpreter’s constructor
private. Otherwise, we would be allowing its construction with arbitrary instances of a
Ref constructed somewhere else.
Moving on, it’s worth highlighting that other programs will interact with this counter
solely via its interface. E.g.
23
Chapter 1: Best practices
In the next chapter, we will discuss whether it is best to pass the dependency implicitly
or explicitly.
24
Chapter 1: Best practices
In the previous section, we have seen how our Counter keeps state using a Ref, but we
haven’t discussed whether that is a good idea or not.
In a few words, it all boils down to whether we need sequential or concurrent state.
State Monad
If we have a program where state could be sequential, it would be safe to use the State
monad whose run function has roughly the following signature.
S => (S, A)
The first S represents an initial state. The tuple in the result contains both the new
state and the result of the state transition. Because of the arrow, the State monad is
inherently sequential (there is no way to run two State actions in parallel and have both
changes applied to the initial state).
The following snippet showcases this monad.
State is threaded sequentially after each flatMap call, which returns the new state that
is used to run the following instruction and so on. Certainly, this makes it impossible to
work in a concurrent setup.
Atomic Ref
In the case of our Counter, we have an interface that might be invoked from many
places at the same time, so it is particularly safe to assume we want a concurrency-safe
implementation. Here is where Ref shines and where the State monad wouldn’t work.
Ref is a purely functional model of a concurrent mutable reference, provided by Cats
Effect. Its atomic update and modify functions allow compositionality and concurrency
safety that would otherwise be hard to get right. Internally, it uses a compare-and-set
loop (or simply CAS loop), but that is something we don’t need to worry about.
25
Chapter 1: Best practices
26
Chapter 1: Best practices
Shared state
To understand shared state, we need to talk about regions of sharing. These regions are
denoted by a simple flatMap call. The example presented next showcases this concept.
Regions of sharing
Say that we need two different programs to concurrently acquire a permit and perform
some expensive task. We will use a Semaphore (another concurrent data structure pro-
vided by Cats Effect) of one permit.
import scala.concurrent.duration._
import cats.effect._
import cats.effect.std.{ Semaphore, Supervisor }
27
Chapter 1: Best practices
Notice how both programs use the same Semaphore to control the execution of the “ex-
pensive tasks”. The Semaphore is created in the run function, by calling its apply function
with an argument 1, indicating the number of permits, and then calling flatMap to share
it with both p1 and p2. The enclosing flatMap block is what denotes our region of sharing.
We are in control of how we share such data structure within this block.
This is one of the main reasons why all the concurrent data structures are wrapped in
F when we create a new one. Ref, Deferred, Semaphore, and similar data structures in
other functional frameworks.
Additionally, we make use of Supervisor, which provides a safe way to execute fire-and-
forget actions. We will learn more about it in Chapter 4.
Leaky state
To illustrate this better, let’s look at what this program would look like if our shared
state, the Semaphore, wasn’t wrapped in IO (or any other abstract effect).
import cats.effect.unsafe.implicits.global
// global access
lazy val sem: Semaphore[IO] =
Semaphore[IO](1).unsafeRunSync()
28
Chapter 1: Best practices
We have now lost our flatMap-denoted region of sharing, and we no longer control where
our data structure is being shared. We don’t know what launchMissiles does internally.
Perhaps, it acquires the single permit and never releases it, which would block our p1
and p2 programs. This is just a tiny example, imagine how difficult it would be to track
similar issues in a large application.
29
Chapter 1: Best practices
Anti-patterns
So far, we have extensively talked about design patterns. However, the other side of the
coin is as important, so we will discuss a bit about it in this section.
Strong claim
Thou shalt not use Seq in your interface
trait Items[F[_]] {
def getAll: F[Seq[Item]]
}
Users of this interface might use it to calculate the total price of all the items.
How do we know it is safe to call toList? What if the Items interpreter uses a Stream
(or LazyList since Scala 2.13.0) representing possibly infinite items? It would still be
compatible with our interface, yet, it will have different semantics.
To be safe, prefer to use a more specific datatype such as List, Vector, Chain, or
fs2.Stream, depending on your specific goals and desired performance characteristics.
30
Chapter 1: Best practices
Strong claim
Thou shalt not use Monad Transformers in your interface
Monad transformers are quite useful (e.g. Http4s makes extensive use of Kleisli) but
we should try to limit their scope to local functions.
Concretely speaking with examples, I believe there is no need for using OptionT in the
following interface.
trait Users[F[_]] {
def findUser(id: UUID): OptionT[F, User]
}
trait Users[F[_]] {
def findUser(id: UUID): F[Option[User]]
}
This is a common API design, sometimes taken for granted. Committing to a specific
Monad Transformer kills compositionality for the API users.
Let’s say we are operating in terms of an abstract F and suddenly we need to use a
function that returns OptionT[F, User] and another that returns EitherT[F, Error,
Customer]. We would need to call value in both cases to get back to our abstract effect
F, an unnecessary wrapping.
trait Users[F[_]] {
def findUser(id: UUID): F[User]
}
object Users {
case object UserNotFound extends NoStackTrace
31
Chapter 1: Best practices
Here we rely on ApplicativeError to deal with the absence of value; another valid design.
We also use NoStackTrace, which is a good alternative to Exception, since stack traces
are heavy-weight on the JVM and provide little benefits.
Tips
Use NoStackTrace instead of Exception for custom error ADTs
Boolean blindness
Filtering collections
class List[A] {
def filter(p: A => Boolean): List[A]
}
What does filter mean exactly? Does it keep the results according to the predicate?
Or does it discard them? We can’t really tell by the type signature.
Scala also defines filterNot, which can be confusing in the same way. Ideally, we should
not deal with ambiguous boolean values at all.
How do we improve this? By eliminating the Boolean from the API, which can usually
be accomplished by introducing an ADT (Algebraic Data Type) with meaningful values.
E.g.
32
Chapter 1: Best practices
List
.range(1, 11)
.filterBy { n =>
if (n > 5) Pred.Keep else Pred.Discard
} // res0: List(6, 7, 8, 9, 10)
It is true that it involves a little bit more of boilerplate than filter(_ > 5) but it is now
clear we intend to keep any number greater than five.
We can expose our custom filterBy as an extension method for any List[A].
Booleans are ubiquitous. You will find them in almost every library out there.
For example, say we have the following interface.
trait BoolApi[F[_]] {
def get: F[Boolean]
}
We could use extension methods such as ifM, which has the following type signature.
def ifM[B](
ifTrue: => F[B],
ifFalse: => F[B]
)(implicit F: FlatMap[F]): F[B] =
F.ifM(fa)(ifTrue, ifFalse)
boolApi.get.ifM(IO.println("YES"), IO.println("NO"))
33
Chapter 1: Best practices
I consider this another case of boolean blindness since both arguments can be inter-
changed while our program would still compile, even if we might be introducing a bug.
Strong claim
Thou shalt not use the ifM extension method
trait Api[F[_]] {
def get: F[Answer]
}
object Api {
sealed trait Answer
object Answer {
case object Yes extends Answer
case object No extends Answer
}
}
In this case, it’s a generic Answer with either Yes or No as possible values. Though, it is
always a better idea to give them meaningful names according to the context.
Now the previous example becomes a flatMap followed by pattern-matching, which can
be exhausted by the compiler.
api.get.flatMap {
case Answer.Yes => IO.println("YES!")
case Answer.No => IO.println("NO!")
}
We successfully eliminated the Boolean from the API at the cost of minimal boilerplate
that makes its intentions crystal-clear.
In most cases, we will have to deal with public APIs that expose functions returning
booleans. This is a very common design from a library perspective, making it very
flexible to adapt and use, so it’s not necessarily a bad thing if you’re a library author.
However, from the user’s perspective, it is always better to avoid boolean blindness.
So coming back to the BoolApi[F] previously defined, if we can’t change it, we can create
our own API on top of it. I call it a proxy.
34
Chapter 1: Best practices
trait Proxy[F[_]] {
def get: F[Result]
}
object Proxy {
sealed trait Result
object Result {
case object Yes extends Result
case object No extends Result
}
The approach is very similar to when we are in control, except in this case, our interpreter
takes a BoolApi[F] as an argument and it builds on top of it.
Boolean isomorphism
All these ADTs we have introduced to avoid dealing with booleans are actually isomor-
phic to the Boolean datatype, which has two possible values: true or false. We can
leverage Monocle’s Iso, a datatype that models the functions A => B and B => A, in
addition to a bunch of laws.
Its definition is more abstract and complex but it basically boils down to this.
The most important guarantee is that a round-trip conversion should leave us exactly
where we started. So we could now define an Iso[Result, Boolean] and place it in the
companion object.
35
Chapter 1: Best practices
Concluding this topic, it begs the question: how far do we push for this approach? It
could be insane to abstract over every Boolean we come across so I would recommend to
stay away from boolean blindness in critical components but to be flexible in the rest of
the application. It is always a matter of agreement within your team.
36
Chapter 1: Best practices
Error handling
There are some known and widely accepted conventions for error handling, yet there is
no standard. So allow me to be biased here, and recommend what has worked well for
me over the years.
We normally work in the context of some parametric effect F[_]. Particularly, when
using Cats Effect, we can rely on MonadError / ApplicativeError and its functions at-
tempt, handleErrorWith, rethrow, among others, to deal with errors, since the IO monad
implements MonadError[F, Throwable], also aliased MonadThrow.
Say we have a Categories algebra that lets us find all the available categories.
trait Categories[F[_]] {
def findAll: F[List[Category]]
}
And the following interpreter (details about Random are not relevant here).
object Categories {
def make[F[_]: MonadThrow: Random]: Categories[F] =
new Categories[F] {
def findAll: F[List[Category]] =
Random[F].nextInt.flatMap {
case n if n > 100 =>
List.empty[Category].pure[F]
case _ =>
RandomError.raiseError[F, List[Category]]
)
}
}
Its interface doesn’t say anything about RandomError so you might wonder whether it
would be better to be specific about the error type and change its signature to something
like this.
37
Chapter 1: Best practices
trait Categories[F[_]] {
def maybeFindAll: F[Either[RandomError, List[Category]]]
}
Tips
Code the happy path and watch the frameworks do the right thing
This means we only need to worry about the successful cases and the business errors.
In other cases, the higher-level frameworks will do the correct thing. In other words,
if you’re using Http4s and forget to handle a RandomError, you will get a 500 Internal
Server Error as a response. Your application will not blow up because of it.
Either Monad
In some other cases, it is perfectly valid to use F[Either[E, A]]. Say we have a Program us-
ing Categories[F], and depending on whether it gets BusinessError or a List[Category],
the business logic changes. This is a fair use case and you can see how we can, at the
same time, eliminate F[Either[E, A]] and go back to F[A] in the example below.
38
Chapter 1: Best practices
Notice that we could have done the same without having the error type in the interface
by using ApplicativeThrow, an alias for ApplicativeError[F, Throwable].
Here we make use of recover, which takes a partial function, but there are other similar
functions.
So what happens if we were to add another error case to the BusinessError ADT? The
compiler will not warn us about it; on the other hand, we would get a compiler error if
our interface had the error type information.
However, since the error type is usually Throwable, we need to have a catch-all clause
which does not really help with our cause.
This is one of the benefits of using F[Either[E, A]], but I would argue the cons outweigh
the benefits. Composing functions of this type signature is cumbersome since we need
to lift nearly every operation into the EitherT monad transformer, and most of the time,
the compiler can not infer the types correctly, so we end up needing to annotate each
part.
You can try this at home by composing at least three different operations returning
F[Either, A], and if you’re up to the challenge, try adding to the mix computations
returning F[Option[A]] and F[A].
39
Chapter 1: Best practices
Furthermore, when E <: Throwable, we are better off using F[A] and relying on MonadError,
which has better ergonomics at the cost of losing the error type.
This is seen as a trade-off. The important take away is to be aware of the different ways
of doing error handling and make a conscious decision.
Classy prisms
In the first edition, I have extensively written about an advanced error handling technique
based on classy prisms. However, instead of venturing into these experimental lands once
again, in this edition I will focus more on other necessary things for the new architecture
of the application (there is even an extra chapter) as well as all the library updates,
including Cats Effect 3.
For those who are still interested in getting this far, I’ll leave links to two blogposts.
• Error handling in Http4s with classy optics - Gabriel Volpe8 : a two-parts blogpost
that explores the classy optics (prisms, being more specific) approach to error
handling using the Meow MTL9 library.
• Functional error handling - Guillaume Bogard10 : it shows how combining
FunctorRaise from Cats MTL11 (now simply called Raise) and MonadError from
Cats can be used together to leverage typed-errors.
8
https://typelevel.org/blog/2018/08/25/http4s-error-handling-mtl.html
9
https://github.com/oleg-py/meow-mtl
10
https://guillaumebogard.dev/posts/functional-error-handling/
11
https://typelevel.org/cats-mtl
40
Chapter 1: Best practices
Summary
This concludes the chapter, where we have learned about design patterns, anti-patterns,
and general (opinionated) advice on functional programming techniques.
In the next nine chapters, we will put this knowledge to work with the shopping cart
application we are going to develop together.
41
Chapter 2: Tagless final encoding
The tagless final encoding (also called finally tagless) is a method of embedding domain-
specific languages (DSLs) in a typed functional host language such as Haskell, OCaml,
Scala, or Coq. It is an alternative to the initial encoding promoted by Free Monads.
This technique is well described in Oleg Kiselyov’s papers1 , and it is also considered one
of the solutions to the expression problem2 . However, in Scala, it has diverged into a
more ergonomic encoding that suits the language’s features better.
In this chapter, we will dive deep into the practical meaning of this technique and also
explore best practices, required for the application we will develop together.
1
http://okmij.org/ftp/tagless-final/index.html
2
https://en.wikipedia.org/wiki/Expression_problem
42
Chapter 2: Tagless final encoding
Algebras
An algebra describes a new language (DSL) within a host language, in this case, Scala.
This may surprise some of you but tagless final encoded algebras are not a new concept
in this book. We have already seen them in Chapter 1 without having to mention their
other name; we called them instead, interfaces with a higher-kinded type.
However, these are not synonyms, as they can also be interfaces with a single-kinded type,
a class with methods, a record of functions (case class), etc. Yet, this can be confusing
given that the original paper uses Haskell typeclasses to describe the technique, but the
truth is tagless final encodings have little to do with typeclasses3 .
Remember our Counter? Let’s recap.
trait Counter[F[_]] {
def incr: F[Unit]
def get: F[Int]
}
This is a tagless final encoded algebra; tagless algebra or algebra for short: a simple
interface that abstracts over the effect type using a type constructor F[_]. Do not
confuse algebras with typeclasses, which in Scala, happen to share the same encoding.
The difference is that typeclasses should have coherent instances, whereas tagless alge-
bras could have many implementations, or more commonly called interpreters.
This is a clear distinction I like to make, even though tagless algebras could also be
implemented as typeclasses by using different newtypes for each interpreter. This is, in
fact, the most popular encoding of tagless final in the Haskell language.
Overall, tagless algebras seem a perfect fit for encoding business concepts. For example,
an algebra responsible for managing items could be encoded as follows.
trait Items[F[_]] {
def getAll: F[List[Item]]
def add(item: Item): F[Unit]
}
Nothing new, right? This tagless final encoded algebra is merely an interface that
abstracts over the effect type. Notice that neither the algebra nor its functions have any
typeclass constraint.
Tips
Tagless algebras should not have typeclass constraints
3
https://www.foxhound.systems/blog/final-tagless/
43
Chapter 2: Tagless final encoding
If you find yourself needing to add a typeclass constraint, such as Monad, to your algebra,
what you probably need is a program.
The reason being that typeclass constraints define capabilities, which belong in programs
and interpreters. Algebras should remain completely abstract.
Naming conventions
Due to my preferences, we named our previous algebra Items, albeit not being a standard.
Out in the wild, you will find people using other names such as ItemService, ItemAlgebra,
or ItemAlg, to name a few. You are free to choose the name you like the most; however,
it is important to be consistent with your choice across your entire application.
For instance, the Scala Steward4 project follows the Alg suffix naming convention. I
recommend looking into its implementation to learn about a different approach to the
tagless final encoding. You’ll find that many things are exactly the opposite of what’s
written in this book but this does not mean one way is more correct than the other, just
different trade-offs.
We may also talk about the file names. In the first edition, all the algebras were named
in lowercase, e.g. cart.scala, brands.scala, etc. In this new edition, these were renamed
to use a more traditional naming scheme, matching the name of the interface. For
example, cart.scala is now ShoppingCart.scala, brands.scala is now Brands.scala, and
so on. Other objects still remain named in lowercase.
These are just naming conventions I opt for at the moment. Readers are encouraged to
pick a preferred naming scheme and stick to it.
4
https://github.com/scala-steward-org/scala-steward
44
Chapter 2: Tagless final encoding
Interpreters
We would normally have two interpreters per algebra: one for testing and one for doing
real things. For instance, we could have two different implementations of our Counter.
A default interpreter using Redis.
object Counter {
@newtype case class RedisKey(value: String)
def testCounter[F[_]](
ref: Ref[F, Int]
): Counter[F] = new Counter[F] {
def incr: F[Unit] = ref.update(_ + 1)
def get: F[Int] = ref.get
}
Interpreters help us encapsulate state and allow separation of concerns: the interface
knows nothing about the implementation details. Moreover, interpreters can be written
either using a concrete datatype such as IO or going polymorphic all the way, as we did
in this case.
Building interpreters
Our default Counter implementation needs a RedisCommands, which lets us operate with a
Redis instance. However, it is important to remark that other programs will only interact
with its algebra, Counter, and will know nothing about what kind of data storage we are
using; the implementation details are hidden from the caller.
45
Chapter 2: Tagless final encoding
If Redis is only used by our Counter interpreter, then no other component in our appli-
cation should know about it. Our Redis connection should be seen as state that must
be encapsulated. Does that sound familiar?
As we have seen in Chapter 1, we can provide a smart constructor that encapsulates the
state. In this case, creating a Redis connection.
object Counter {
def make[F[_]: Sync](
key: RedisKey
): Resource[F, Counter[F]] =
makeRedis[F].map { redis =>
new Counter[F] {
def incr: F[Unit] =
redis.incr(key.value)
Notice how instead of Counter[F], we are returning Resource[F, Counter[F]]. Since our
implementation requires a Redis connection, which is treated as a resource, then we
also need to make our counter’s smart constructor a resource itself; this is a common
practice.
At usage site, this will trivially become something along these lines.
Our Redis connection will only live within the use block. The Resource datatype guar-
antees the clean up of the resource (closing Redis connection) when the program has
terminated, as well as in the presence of failures or interruption.
In this example, we used the Redis4Cats5 library but the same principle applies when
utilizing other libraries.
5
https://redis4cats.profunktor.dev/
46
Chapter 2: Tagless final encoding
Programs
Tagless final is all about algebras and interpreters. Yet, something is missing when it
comes to writing applications: we need to use these algebras to describe business logic,
and this logic belongs in what I like to call programs.
Notes
Programs can make use of algebras and other programs
Although it is not an official name – and it is not mentioned in the original tagless final
paper – it is how we will be referring to such interfaces in this book.
Say we need to increase a counter every time there is a new item added. We could
encode it as follows.
Observe the characteristics of this program. It is pure business logic, and it holds no
state at all, which in any case, must be encapsulated in the interpreters. Notice the
typeclass constraints as well; it is a good practice to have them in programs instead of
tagless algebras.
Here, the program doesn’t need to consider concurrent or parallel effects, but that should
be fine too. Parallelism can be conveyed using the Parallel typeclass and concurrency
using the Concurrent typeclass.
Unfortunately, in Cats Effect 2, the latter implies Async and Sync, which allow encap-
sulating arbitrary side-effects. This has been fixed in Cats Effect 3, where these two
typeclasses have been moved at the bottom of the hierarchy6 , so we can now safely use
Concurrent without worrying about such implications.
Anyway, we should always strive for building applications in terms of tagless algebras.
We could, for example, create our own LimitedConcurrency interface, which only exposes
the functions we are interested in. This way, we can eliminate hard constraints, regardless
of the CE version, as we will see in the next chapter.
6
https://typelevel.org/cats-effect/docs/typeclasses
47
Chapter 2: Tagless final encoding
Moreover, we can discuss typeclass constraints. In this case, we only need Apply to use
*> (alias for productR). However, it would also work with Applicative or Monad. The rule
of thumb is to limit ourselves to adopt the least powerful typeclass that gets the job
done.
It is worth mentioning that Apply itself doesn’t specify the semantics of composition
solely with this constraint, *> might combine its arguments sequentially or parallelly,
depending on the underlying typeclass instance. To ensure our composition is sequential,
we could use FlatMap instead of Apply.
Tips
When adding a typeclass constraint, remember about the principle of
least power
Whether we encode programs in one way or another, they should describe pure business
logic and nothing else.
The question is: what is pure business logic? We could try and define a set of rules to
abide by. It is allowed to:
48
Chapter 2: Tagless final encoding
You can use this as a reference. However, the answer should come up as a collective
agreement within your team.
49
Chapter 2: Tagless final encoding
So far, we have talked about algebras, interpreters, and programs. Yet, little did we talk
about implicit parameters and when we should be using them.
In Scala, tagless final has been misused quite considerably. Have you ever seen anything
like this?
def program[
F[_]: Cache: Console: Users: Monad
: Parallel: Items: EventsManager
: HttpClient: KafkaClient: EventsPublisher
]: F[Unit] = ???
Tips
Business logic algebras should always be passed explicitly
def program[
F[_]: Cache: Console: Monad: Parallel
: EventsManager: EventsPublisher
: KafkaClient: HttpClient
](
users: Users[F],
items: Items[F]
): F[Unit] = ???
We have improved slightly, but it certainly isn’t ideal. Next, we can assume that all the
Events algebras are also business-related and pass them explicitly instead.
def program[
F[_]: Cache: Console: Monad: Parallel
: KafkaClient: HttpClient
](
users: Users[F],
items: Items[F],
50
Chapter 2: Tagless final encoding
eventsManager: EventsManager[F],
eventsPublisher: EventsPublisher[F]
): F[Unit] = ???
We are left with the following two algebras: KafkaClient and HttpClient. Frequently,
such clients have a lifecycle, best managed as resources; hence, they need to be passed
explicitly since creating a resource is an effectful operation. Last but not least, we could
arguably do the same for Cache, which might be backed by Redis, for example.
Much better. But now we seem to face another problem: we have too many dependencies,
which makes dealing with them a cumbersome task.
Though, I would argue that all we need is a better organization. We usually encounter
these kinds of programs at the top level of our application, thus explaining the number
of dependencies.
In the next section, we will learn how modules help us dealing with this issue.
Achieving modularity
package modules
trait Services[F[_]] {
def users: Users[F]
def items: Items[F]
}
51
Chapter 2: Tagless final encoding
trait Events[F[_]] {
def manager: EventsManager[F]
def publisher: EventsPublisher[F]
}
trait Clients[F[_]] {
def kafka: KafkaClient[F]
def http: HttpClient[F]
}
trait Database[F[_]] {
def cache: Cache[F]
}
Does it make sense? Having our dependencies organized in this way makes our codebase
much easier to maintain in the long term.
To build our modules, we can use a smart constructor in the interface’s companion object.
For example, this is what our Clients implementation could look like.
object Clients {
52
Chapter 2: Tagless final encoding
Implicit convenience
There are some examples of implementations that are passed as implicits and that are
not typeclasses. In Cats Effect 2, for example, the types ContextShift, Clock, and Timer
fit this usage pattern.
Why are they used implicitly if they are not typeclasses? It is merely for convenience
since instances for these datatypes are normally given by IOApp as the “environment”.
They are seen as common effects, and this vision allows us to have different instances
for testing purposes, which would not be possible if using typeclasses as we would be
creating orphan instances.
Notes
Common effects do not hold business logic
In such cases, we can say it is fine to thread instances implicitly. It is convenient, and it
doesn’t break anything, so I would personally endorse this usage.
In our example above, we are left with this implicit encoding.
From what we can gather, the only valid and lawful typeclasses are Monad and Parallel.
What about Console? Although it is not a typeclass, it is more convenient to pass it
implicitly since it fits the description of a common effect that would rarely need more
than a single instance. Nevertheless, if we need to, we can create another instance for
testing purposes as well.
Other examples of common effects could be GenUUID, Time, and Random, to deal with
UUIDs, timestamps, and random data generation, respectively.
Capability traits
What has been conveyed in this book as common effects, could also seen as capability
traits, a term that is being adopted in the community. For example, Michael Pilquist7
(Fs2 maintainer) has recently given a talk titled The Future of Typelevel Concurrency8
where he shows the following example (using Cats Effect 3).
7
https://github.com/mpilquist
8
https://speakerdeck.com/mpilquist/the-future-of-typelevel-concurrency?slide=38
53
Chapter 2: Tagless final encoding
trait Files[F[_]] {
def delete(path: Path): F[Unit]
}
object Files {
implicit def forSync[F[_]: Sync] =
new Files[F] {
def delete(path: Path): F[Unit] =
Sync[F].blocking(JFiles.delete(path))
}
}
Thinking about our constraints in terms of capabilities is actually a great idea. It helps
getting rid of hard constraints such as Sync and limits what a function can do (principle
of least power). This is the reason why I think it does not matter whether you use
Cats Effect 2 or 3 in your application (though, it matters to library authors). Stick to
program against the interface and, as an added benefit, this will also ease the migration
over to CE3, if you plan to do so.
In the end, it is all about common sense, consistency, and good practices. The language
is flexible enough to allow typeclasses and convenient interfaces to be encoded in the
same way. Let’s just remember to adhere to our practical rules and use the language in
terms of our own Domain Specific Language (DSL) defined as a set of tagless algebras
and capability traits.
54
Chapter 2: Tagless final encoding
This quote is the title of a transcendental talk9 from 2015 that intends to show that
“Restraint and precision are usually better than power and flexibility. A
constraint on component design leads to freedom and power when putting
those components together into systems.”
Parametricity
In this chapter, we have learned a lot about the tagless final encoding and how to be
successful with it in Scala. Still, some might question the decision to invest in this
technique for a business application, claiming it entails great complexity.
This is a fair concern but let’s ask ourselves, what’s the alternative? Using IO directly
in the entire application? By all means, this could work, but at what cost? At the very
least, we would be giving up on parametricity10 and the principle of least power.
A simple way of explaining parametricity is the following classic trivia: How many
correct implementations can possibly have the following function (leaving aside throwing
exceptions and side-effects, of course)?
The answer is infinite; it could return 0, or maybe a * 5, or even -23. Basically any
arithmetic operation we could think of. How about the following function?
The only correct implementation that terminates is to return a. In this case, emphasising
on termination because returning parametric(a) would also compile, but it would never
terminate upon evaluation.
This is commonly known as the identity function. It quickly shows how being constrained
by types effectively reduces the margin for errors.
9
https://www.youtube.com/watch?v=GqmsQeSzMdw
10
https://en.wikipedia.org/wiki/Parametricity
55
Chapter 2: Tagless final encoding
Comparison
trait Log[F[_]] {
def info(str: String): F[Unit]
}
Whereas the creation of time has been moved over to the Time interface.
56
Chapter 2: Tagless final encoding
trait Time[F[_]] {
def getHour: F[Time.Hour]
}
object Time {
@newtype case class Hour(int: NonNegInt)
}
Teams making use of this technique will immediately understand that all we can do
in the body of the constrained function is to compose Counter, Log, and Time actions
sequentially as well as to use any property made available by the Monad constraint. It is
true, however, that the Scala compiler does not enforce it so this is up to the discipline
of the team.
Since Scala is a hybrid language, the only thing stopping us from running wild side-
effects in this function is self-discipline and peer reviews. However, good practices are
required in any team for multiple purposes, so I would argue it is not necessarily a bad
thing, as we can do the same thing in programs encoded directly in IO.
Training your team is always important, regardless.
Having this function define clear boundaries by restricting its power is liberating. We
know exactly what can be possible done with it by introducing capabilities. Additionally,
we make testing much easier.
for {
c <- Counter.make[IO]
p <- constrained[IO](c)
} yield {
assert(p === 10, "Expected result === 10")
}
}
This test is defined as a simple program, as we haven’t learned about test frameworks
yet. We can do the same to test the scenario when the count should not be incremented
in a similar way.
The Log.noop[IO] constructor gives us an instance that does not print out to standard
out, as this is completely irrelevant in our test.
57
Chapter 2: Tagless final encoding
object Log {
def apply[F[_]: Log]: Log[F] = implicitly
object Time {
@newtype case class Hour(int: NonNegInt)
object Hour {
def from(instant: Instant): Hour =
Hour(
NonNegInt.unsafeFrom(
instant.atZone(ZoneOffset.UTC).getHour()
)
)
}
58
Chapter 2: Tagless final encoding
}
}
In addition to the Hour newtype and its smart constructor, it also defines a default
instance for Sync that we can override with a local implicit in scope.
Granted, we haven’t invented anything new here. Instead, we are leveraging the all-time
recommendation of coding to the interface, which can also be done by sticking to IO
instead of going abstract all the way.
Doesn’t this give us the same benefits as the abstract version? Surely it is better than
having uncontrolled side-effects but we are no longer restricted to Counter, Log, Time and
Monad. Instead, we have all the power of IO available to us, which makes it extremely
easy to commit mistakes.
Warning
IO is quite permissive, making the programmer error-prone
If we let parametricity dictate what is possible, the responsibility of such function would
be much clearer to convey.
In conclusion, advanced developers who know exactly what they do can certainly swap
either techniques at any time but you will have a hard time training Junior members
not to abuse IO in such functions.
Being constrained to specific capabilities also acts as a guide. In my experience, beginners
can easily understand and pick up this technique faster than the unrestricted one.
It’s fair to say all of this was about a single function. Imagine reasoning about every
component of a big application where IO is made available!
59
Chapter 2: Tagless final encoding
Finally, if you invest in your team’s training, reading and writing code in terms of
capabilities (aka constraints) will become a natural task soon enough, leading to clean
and maintainable code, as well as highly motivated individuals.
60
Chapter 2: Tagless final encoding
Summary
11
https://impurepics.com/posts/2018-04-09-final-tagless-path.html
61
Chapter 3: Shopping Cart project
Here is the beginning of our endeavor. We will develop a shopping cart application
utilizing the best libraries, architecture, and design patterns I am aware of. We are going
to start with understanding the business requirements and see how we can materialize
them into our system design.
By the end of this chapter, we should have a clearer view of the business expectations.
62
Chapter 3: Shopping Cart project
Business requirements
A Guitar store located in the US has hired our services to develop the backend system
of their online store. The requirements are clear to the business. However, they don’t
know much about what the necessities of the backend might be. So this is our task. We
are free to architect and design the backend system in the best way possible.
For now, they only need to sell guitars. Though, in the future, they want to add other
products. Here are the requirements we have got from them:
Notes
For now, there will be a single admin user created manually
The external payment system exposes an HTTP API. We are told it is idempotent,
meaning that it is capable of handling duplicate payments. If we happen to make a
63
Chapter 3: Shopping Cart project
request for the same payment twice, we are going to get a specific HTTP response code
containing the Payment Id.
POST Request body
{
"user_id": "hf8hf ...",
"total": 324.35,
"card": {
"name": "Albert Einstein",
"number": "5555222288881111",
"expiration": "0821",
"cvv": 123
}
}
{
"payment_id":"eyJ0eXA ..."
}
• Response codes:
– 200: the payment was successful.
– 400: e.g. invalid request body.
– 409: duplicate payment (returns Payment Id).
– 500: unknown server error.
With this information, we should be able to design the system and get back to the
business with our proposal.
We could try to represent guitars as a generic Item since, in the future, they want to add
other products. A possible sketch of our domain model is presented below.
Tips
Understanding the product is fundamental to design a good system
64
Chapter 3: Shopping Cart project
Item
Brand
Category
Cart
Order
Card
• name:
card holder’s name.
• number:
a 16-digit number.
• expiration: a 4-digit number as a string, to not lose zeros: the first two digits
indicate the month, and the last two, the year, e.g. “0821”.
• cvv: a 3-digit number. CVV stands for Card Verification Value.
65
Chapter 3: Shopping Cart project
Guest User
Since we don’t know anything about such users, we are not going to represent them in
our domain model, but we know they should be able to register and login to the system
given some valid credentials. We are going to see in Chapter 5, that this belongs to the
HTTP request body of our authentication endpoints.
User
It represents a registered user that has been logged into the system.
Admin User
It has special permissions, such as adding items into the system’s catalog.
Our API should be versioned to allow a smooth evolution as the requirements change in
the future. Therefore, all the endpoints will start with the /v1 prefix.
66
Chapter 3: Shopping Cart project
The next few pages contain details about every HTTP endpoint such as request bodies,
response status codes, and response bodies. Feel free to skip ahead to the Technical
stack section or just skim through them since it can get quite dense.
Open Routes
Authentication routes
• POST /users
– 201: the user was successfully created.
– 400: invalid input data, e.g. empty username.
– 409: the username is already taken.
Request body
{
"username": "csagan",
"password": "<c05m05>"
}
{
"access_token":"eyJ0eXA ..."
}
• POST /auth/login
– 200: the user was successfully logged in.
– 403: e.g. invalid username or credentials.
Request body
{
"username": "csagan",
"password": "<c05m05>"
}
67
Chapter 3: Shopping Cart project
{
"access_token":"eyJ0eXA ..."
}
• POST /auth/logout
– 204: the user was successfully logged out.
Brand routes
• GET /brands
– 200: returns a list of brands.
Response body on success
[
{
"uuid": "7a465b27-0db ...",
"name": "Fender"
},
{
"uuid": "f40e8104-9be ...",
"name": "Gibson"
}
]
Category routes
• GET /categories
– 200: returns a list of categories.
Response body on success
68
Chapter 3: Shopping Cart project
[
{
"uuid": "10739c61-c93 ...",
"name": "Guitars"
}
]
Item routes
• GET /items
– 200: returns a list of items.
Response body on success
[
{
"uuid": "509b77fd-3a ...",
"name": "Telecaster",
"description": "Classic guitar",
"price": 578,
"brand": {
"uuid": "7a465b27-0d ...",
"name": "Fender"
},
"category": {
"uuid": "10739c61-c93 ...",
"name": "Guitars"
}
}
]
• GET /items?brand=gibson
– 200: returns a list of items.
69
Chapter 3: Shopping Cart project
Secured Routes
These are the HTTP routes that require registered users to be logged in. All of them
can return the following response statuses in addition to the specific ones.
Cart routes
• GET /cart
– 200: returns the cart for the current user.
Response body on success
{
"items": [
{
"item": {
"uuid": "509b77fd-3a ...",
"name": "Telecaster",
"description": "Classic guitar",
"price": 578,
"brand": {
"uuid": "7a465b27-0d ...",
"name": "Fender"
},
"category": {
"uuid": "10739c61-c93 ...",
"name": "Guitars"
}
},
"quantity": 4
}
],
"total": 2312
}
• POST /cart
70
Chapter 3: Shopping Cart project
Request body
{
"items": {
"509b77fd-3a ...": 4
}
}
No Response body.
• PUT /cart
– 200: the quantity of some items were updated in the cart.
– 400: quantities must be greater than zero.
Request body
{
"items": {
"509b77fd-3a ...": 1
}
}
No Response body.
• DELETE /cart/{itemId}
– 204: the specified item was removed from the cart, if it existed.
71
Chapter 3: Shopping Cart project
Checkout routes
• POST /checkout
– 201: the order was processed successfully.
– 400: e.g. invalid card number.
Request body
{
"name": "Isaac Newton",
"number": 1111444422223333,
"expiration": "0422",
"ccv": 131
}
Response body
{
"order_id": "gf34y54g ..."
}
Order routes
• GET /orders
– 200: returns the list of orders for the current user.
Response body on success
[
{
"uuid": "54312359 ...",
"payment_id": "Ex4dfd4 ...",
"items": [
{
"uuid": "14427832 ...",
"quantity": 1
},
{ ...}
],
"total": 3769.45
}
]
72
Chapter 3: Shopping Cart project
• GET /orders/{orderId}
– 200: returns specific order for the current user.
– 404: order not found.
{
"uuid": "54312359 ...",
"payment_id": "Ex4dfd4 ...",
"items": [
{
"uuid": "14427832 ...",
"quantity": 1
},
{ ...}
],
"total": 3769.45
}
Admin Routes
These are the HTTP routes that can be accessed only by administrators with a specific
API Access Token. All of the following response statuses can be returned in addition to
the specific ones.
Brand routes
• POST /brands
– 201: brand successfully created.
– 409: the brand name is already taken.
Request body
73
Chapter 3: Shopping Cart project
{
"name": "Ibanez"
}
Response body
{
"brand_id": "7a465b27-0d ..."
}
Category routes
• POST /categories
– 201: category successfully created.
– 409: the category name is already taken.
Request body
{
"name": "Guitars"
}
Response body
{
"category_id": "10739c61-c9 ..."
}
Item routes
• POST /items
– 201: items successfully created.
– 409: some of the items already exist.
Request body
74
Chapter 3: Shopping Cart project
{
"name": "Telecaster",
"description": "Classic guitar",
"price": 578,
"brandId": "7a465b27-0d ...",
"categoryId": "10739c61-c9 ..."
}
Response body
{
"item_id": "gf34y54g ..."
}
• PUT /items
– 200: item’s price successfully updated.
– 400: the price must be greater than zero.
Request body
{
"uuid": "509b77fd-3a ...",
"description": "Classic guitar",
"price": 5046.14,
"brandId": "7a465b27-0d ...",
"categoryId": "10739c61-c9 ..."
}
No Response body.
75
Chapter 3: Shopping Cart project
Technical stack
Below is the complete list of all the libraries we will be using in our application:
• cats: basic functional blocks. From typeclasses such as Functor to syntax and
instances for some datatypes and monad transformers.
• cats-effect: concurrency and functional effects. It ships the default IO monad.
• cats-retry: retrying actions that can fail in a purely functional fashion.
• circe: standard JSON library to create encoders and decoders.
• ciris: flexible configuration library with support for different environments.
• derevo: typeclass derivation via macro-annotations.
• fs2: powerful streaming in constant memory and control flow.
• http4s: purely functional HTTP server and client, built on top of fs2.
• http4s-jwt-auth: opinionated JWT authentication built on top of jwt-scala.
• log4cats: standard logging framework for Cats.
• monocle: access and transform immutable data with optics.
• redis4cats: client for Redis compatible with cats-effect.
• refined: refinement types for type-level validation.
• scalacheck: property-based test framework for Scala.
• scala-newtype: zero-cost wrappers for strongly typed functions.
• skunk: purely functional, non-blocking PostgreSQL client.
• squants: strongly-typed units of measure such as “money”.
• weaver: a test framework with native support for effect types.
The first edition was based on Cats Effect 2 (CE2 for short). However, a new major
version1 has recently seen the light with incredible improvements. It would be silly to
waste this chance, so this second edition will be based on Cats Effect 3 (CE3). Here is
a quote from its website2 .
1
https://github.com/typelevel/cats-effect/releases/tag/v3.0.0
2
https://typelevel.org/cats-effect/
76
Chapter 3: Shopping Cart project
That said, whether you are still on CE2 or are lucky enough to have migrated to CE3,
all the design patterns and best practices discussed in this book will remain relevant,
albeit having some differences.
77
Chapter 3: Shopping Cart project
Summary
Although we haven’t seen any application code yet, there wouldn’t be any without
business requirements!
The analysis we performed in this chapter is critical in any case. We should never start
writing code without clearly understanding the problem we need to solve first.
78
Chapter 4: Business logic
In previous chapters, we have distilled the business requirements into technical specifi-
cations and have identified the possible HTTP endpoints our application should expose.
We have also explored some functional design patterns and unleashed the power of ab-
straction with the tagless final encoding.
Now it is time to get to work, as we apply these techniques to our business domain. A
common way to get a feature design started is to decompose business requirements into
small, self-contained algebras.
79
Chapter 4: Business logic
Identifying algebras
Summarizing, this list contains the secured, admin, and open HTTP endpoints.
• GET /brands
• POST /brands
• GET /categories
• POST /categories
• GET /items
• GET /items?brand={name}
• POST /items
• PUT /items
• GET /cart
• POST /cart
• PUT /cart
• DELETE /cart/{itemId}
• GET /orders
• GET /orders/{orderId}
• POST /checkout
• POST /auth/users
• POST /auth/login
• POST /auth/logout
Our mission is to identify common functionality between these endpoints and create a
tagless algebra. Although there are many valid designs in this space, we still stick to
what we have learned in previous chapters.
We will start with the brands group of endpoints. Ready?
Brands
Our Brand domain consists of two endpoints: a GET to retrieve the list of brands and
a POST to create new brands. The POST endpoint should only be used by administra-
tors. However, we don’t consider permission details at this level. So let’s condense this
functionality into a single algebra.
trait Brands[F[_]] {
def findAll: F[List[Brand]]
def create(name: BrandName): F[BrandId]
}
80
Chapter 4: Business logic
That is all we need, a clear tagless algebra that programs can use to implement some
functionality. At this point, we don’t particularly care about implementation details.
The Categories algebra is quite similar to Brands so it will be eluded. Instead, we can
get straight to the Items domain.
Items
It consists of two GET endpoints: one to retrieve a list of all the items, and another to
retrieve items filtering by brand. It also has a POST endpoint to create an item and a PUT
endpoint to update an item. Both are administrative tasks, but as we mentioned before,
it is not a concern at this level.
trait Items[F[_]] {
def findAll: F[List[Item]]
def findBy(brand: BrandName): F[List[Item]]
def findById(itemId: ItemId): F[Option[Item]]
def create(item: CreateItem): F[ItemId]
def update(item: UpdateItem): F[Unit]
}
The Item datatype is a bit more interesting than our previous domain datatypes on closer
inspection.
import squants.market.Money
81
Chapter 4: Business logic
name: ItemName,
description: ItemDescription,
price: Money,
brandId: BrandId,
categoryId: CategoryId
)
Our price field is going to be represented using the Money type provided by Squants,
which supports many different currencies. In the future, we may need to support other
markets; this can be easily achieved by converting between currencies, e.g. using the
exchange rate of the day.
Shopping Cart
Next is our Cart domain. It has a GET endpoint to retrieve the shopping cart of the current
user, a POST endpoint to add items to the cart, a PUT endpoint to edit the quantity of
any item, and a DELETE endpoint to remove an item from the cart. The following algebra
encodes this functionality in the respective order.
trait ShoppingCart[F[_]] {
def add(
userId: UserId,
itemId: ItemId,
quantity: Quantity
): F[Unit]
def get(userId: UserId): F[CartTotal]
def delete(userId: UserId): F[Unit]
def removeItem(userId: UserId, itemId: ItemId): F[Unit]
def update(userId: UserId, cart: Cart): F[Unit]
}
Here we have some new datatypes, including a few we haven’t classified in Chapter 3.
82
Chapter 4: Business logic
Our Cart is a simple key-value store of ItemIds and Quantitys, respectively, so we can
easily avoid duplicates and tell how many specific items there are in the cart. Further-
more, CartItem is a simple wrapper of Item and Quantity, so we can provide more details
about the item.
Orders
Once we process a payment, we need to persist the order, but we also want to be able
to query past orders. Here is our algebra.
trait Orders[F[_]] {
def get(
userId: UserId,
orderId: OrderId
): F[Option[Order]]
def create(
userId: UserId,
paymentId: PaymentId,
items: NonEmptyList[CartItem],
total: Money
): F[OrderId]
}
This is the information we will be persisting in our database. The persisted object
contains the PaymentId returned by the external payment system and the total amount
specified in US Dollars.
83
Chapter 4: Business logic
Users
Our system should be able to store basic information about users, such as usernames
and encrypted passwords.
trait Users[F[_]] {
def find(
username: UserName
): F[Option[UserWithPassword]]
def create(
username: UserName,
password: EncryptedPassword
): F[UserId]
}
Authentication
There are also the authentication endpoints. We are going to use JSON Web Tokens
(JWT) as the authentication method, as we will further expand in Chapter 5. Until we
get there, we can sketch something out with what we currently have and make some
modifications later on, if necessary.
Warning
Interface subject to change in future iterations
trait Auth[F[_]] {
def findUser(token: JwtToken): F[Option[User]]
def newUser(username: UserName, password: Password): F[JwtToken]
84
Chapter 4: Business logic
Auth will also be responsible for validating encrypted passwords against those received
via the login function for returning users but that’s an implementation detail.
Remember that we have guest users, common users, and admin users. The former is
the only one that doesn’t require authentication, so we don’t need to represent it in our
domain model. Next, we have a few common types.
Payments
Finally, let’s not forget about our external payments API. A good practice is to also
define a tagless algebra for remote clients.
trait PaymentClient[F[_]] {
def process(payment: Payment): F[PaymentId]
}
This is all we know about the payment system’s input. In Chapter 3, we have defined
the Card datatype, and in the next chapter, we are going to see its full implementation.
Our work defining the algebras for our application is now complete. We skipped checkout,
as you might have noticed, and you will soon find out why.
85
Chapter 4: Business logic
Before we can create the interpreters for our algebras, we should identify what kind of
state we need in each of them, which takes us to the next question: What kind of storage
are we going to use?
Generally speaking, such applications need some kind of persistent storage where data
can be queried for further analysis over time (e.g. to generate reports). SQL databases
are a great fit for these requirements, and although there are a few options in the market,
we can arguably say PostgreSQL1 is one of the most solid choices, which also happens
to be open source.
The cache layer is another important component in any application. It allows us to
quickly access data stored in memory, usually with an eviction policy set, while avoiding
expensive requests to a database server. Redis2 is undeniably one of the best options in
this field, if not the best.
Having mentioned these two giants, and their use case, we can now discuss their role in
our application. We are going to persist brands, categories, items, orders, and users in
PostgreSQL, which represent data that should remain stored in our system.
For fast access, we are going to store the shopping cart in Redis, which should not survive
a session. Additionally, authentication tokens will also be stored in our cache, as they
should be invalidated after a certain amount of time.
Health check
Our application needs to report its health status, which usually involves database con-
nection checks, among other things. We can model the algebra as follows.
trait HealthCheck[F[_]] {
def status: F[AppStatus]
}
@derive(encoder)
@newtype
case class RedisStatus(value: Status)
@derive(encoder)
@newtype
case class PostgresStatus(value: Status)
1
https://www.postgresql.org/
2
https://redis.io/
86
Chapter 4: Business logic
@derive(encoder)
case class AppStatus(
redis: RedisStatus,
postgres: PostgresStatus
)
The Status datatype is isomorphic to Boolean, for which we define an instance of Iso.
87
Chapter 4: Business logic
Defining programs
So far, we have defined a lot of new functionality in our algebras. Some parts of our
application are going to make direct use of some of them; other parts will require more
than just calling simple functions defined by them. The principal role of our programs is
to describe business logic operations as a kind of a DSL, without needing to know about
implementation details.
This is arguably one of the most exciting challenges in this book. Let’s dive into it!
Checkout
The following process function conveys the idea of the simplest implementation: a se-
quence of actions denoted as a for-comprehension – syntactic sugar for a sequence of
flatMap calls and a final map call. In essence, this function is retrieving the cart for the
current user, calling the remote API that processes the payment, and finally persisting
a new order.
It seems we are done here, but if we think about it, this is the most critical piece of
code in our application! How does this function behave if a failure occurs at any stage?
How should we react to the various failure types? The answer strictly depends on the
88
Chapter 4: Business logic
business requirements. However, we should notify them about the suggested alternatives
so they can make an informed decision.
As good Software Engineers, let’s dissect the former application and explore how we
could mitigate some of the potential issues.
The following functions are executed sequentially. If one function fails, the one below
won’t be executed, unless there’s some explicit error handling. With this in mind, let’s
look at the first line.
c <- cart.get(userId)
What happens if we cannot find the shopping cart for the current user? In this case, the
user’s cart is either empty, or there is an issue communicating with our database. In
any case, there is not much we can do; without the cart we cannot continue so the best
thing we can do is to let it fail, returning some kind of error message to the client.
In between, we try to obtain a NonEmptyList[CartItem] from a simple list. If it’s empty,
we raise an EmptyCartError, which short-circuits the process.
Payment failure #1 Either there is an error response code, distinct from 409 (Conflict),
from the external HTTP API; or something went wrong and our HTTP request didn’t
complete as we expected (e.g. network issues, request timeouts, etc).
This is the simplest error, in which case we can retry. The most common procedure is
to log the error and make the request once again, using a specific retry policy, as we will
see soon.
89
Chapter 4: Business logic
Payment failure #2 The next case scenario involves a duplicate request. Let’s say we
make an HTTP request and the payment is processed successfully on their end but we
fail to get a response (again, due to some network issues). In such a case, we are going
to retry a few moments later as well.
When we retry, we get a specific response code (409) from the remote API, indicating
the payment has already been processed. Additionally, we get the Payment ID as the
body of the response. This is easy. We are only interested in the Payment Id, so all we
need to do is to handle this specific error, extract the Payment Id, and continue with
the checkout process.
Next, we have the creation of the order.
Order failure #1 We didn’t get to see the implementation yet, but we know that the
orders are to be persisted in PostgreSQL. Thus, we need to handle possible database or
connection failures.
If our database call fails to be processed (e.g. network failure), we can again retry a
limited amount of times.
Order failure #2 Let’s say that our retry mechanism has completed, and we finally
give the user a response. This has taken some time, but let’s be honest, nobody likes
to wait more than a couple of milliseconds when purchasing goods online; it’s not the
1990s anymore.
Since the payment has been processed and the customer has been charged, we can try to
revert the payment and return an error. Unfortunately, we are told the remote payment
system doesn’t support this feature yet, so we would need to solve this issue differently.
The payment is immutable and cannot be reverted from our end. All we can do is deal
with this error later. One approach would be to reschedule the order creation to run in
the background at some point in the future and, in the meantime, get back to the user
saying the payment was successful and that the order should be available soon.
One arbitrary decision would be to run this background action forever until it succeeds.
The order needs to be created, no matter what. Yet, we need to be realistic and
contemplate the possible drawbacks of such a drastic decision, even if they are minimal.
The issue that has been affecting our order creation might be unrecoverable, let’s say,
the database server went on fire.
We can either live with this decision, knowing that our application restarts regularly; or
give this background task a limited amount of retries as well, possibly never persisting
such order in our local database. In this case, we are going to go with the first option,
informing the business of the choices made.
90
Chapter 4: Business logic
Last but not least, we delete the shopping cart for the current user.
_ <- cart.delete(userId)
There is nothing critical in this part, but just in case we should .attempt the action
(which means changing our Monad constraint to MonadError[F, Throwable]), to make our
program resilient to possible failures (e.g. Redis connection issues). If it fails for some
reason, it is not a big deal since the cart should expire shortly (more about this in
Chapter 7). It should result as follows.
_ <- cart.delete(userId).attempt.void
We add an explicit void to discard its result. Although unnecessary, I believe discarding
a result in a for-comprehension should be rejected by the compiler. Unfortunately, the
compiler thinks otherwise.
Retrying effects
Retrying arbitrary effects using Cats Effect is fairly easy. For instance, we could delay
the execution of a specific action, and then do it all over again, recursively.
We can either build more complex retrying functions in this way, or we can choose a
library that does it all for us.
Cats Retry3 is a great choice, offering different retry policies, powerful combinators, and
a friendly DSL. Let’s see how we can exploit its power.
First, we need to define a common function to log errors for both cases: processing the
payment and persisting the order. To make retries easy to test, we will place it behind a
new capability trait that also defines how to retry with a given policy, cleverly avoiding
hard constraints such as Temporal, even though it’s considered pure.
trait Retry[F[_]] {
def retry[A](
policy: RetryPolicy[F], retriable: Retriable
)(fa: F[A]): F[A]
}
object Retry {
def apply[F[_]: Retry]: Retry[F] = implicitly
3
https://github.com/cb372/cats-retry
91
Chapter 4: Business logic
retryingOnAllErrors[A](policy, onError)(fa)
}
}
}
object Retriable {
case object Orders extends Retriable
case object Payments extends Retriable
}
We also need a retry policy. In both cases, we are going to have a maximum of three
retries with an exponential back-off of 10 milliseconds between retries (these values will
be configurable in our final application).
import retry.RetryPolicies._
val retryPolicy =
limitRetries[F](3) |+| exponentialBackoff[F](10.milliseconds)
Easy right? Retry policies have a Semigroup instance that makes combining them
straightforwardly.
We can now create a function that retries payments.
92
Chapter 4: Business logic
The last part of our function is quite interesting. Using adaptError, we transform the
error given by the payment client (re-thrown after our retry function gives up) into our
custom PaymentError. We also need to wrap e.getMessage in an Option because it may
be null; remember that we are dealing with java.lang.Throwable here.
Here is another function that makes creating and persisting orders a retriable action.
def createOrder(
userId: UserId,
paymentId: PaymentId,
items: NonEmptyList[CartItem],
total: Money
): F[OrderId] = {
val action =
Retry[F]
.retry(policy, Retriable.Orders)(
orders.create(userId, paymentId, items, total)
)
.adaptError {
case e => OrderError(e.getMessage)
}
bgAction(action)
}
93
Chapter 4: Business logic
Besides our retry mechanism, we have now introduced a new common effect Background,
which lets us schedule tasks to run in the background sometime in the future. Let’s have
a look at its interface.
trait Background[F[_]] {
def schedule[A](
fa: F[A],
duration: FiniteDuration
): F[Unit]
}
We could have done this directly using Temporal; in fact, this is almost what our default
implementation does, though, there are a few reasons why having a custom interface is
a better approach.
• We gain more control by restricting what the final user can do.
• We avoid having stronger constraints in our program.
• We achieve better testability, as we will see in Chapter 8.
We’ve seen this in Chapter 2 with the principle of least power and capability traits, but
it’s always good to highlight it in the right context.
For completeness, here is our default Background instance.
4
https://typelevel.org/cats-effect/api/3.x/cats/effect/std/Supervisor.html
94
Chapter 4: Business logic
It has never been easier to manage effects in a purely functional way in Scala! Com-
posing retry policies using standard typeclasses and sequencing actions using monadic
combinators led us to our ultimate solution.
Our final class is defined as follows – MonadThrow is defined in Cats and is a type alias
for MonadError[F, Throwable].
In Chapter 8, when we talk about testing, we are going to see how to test this and other
complex functions.
95
Chapter 4: Business logic
Architecture
The HTTP layer is the user-facing API whereas the Core API represents all the algebras
and programs we defined in this chapter. Lastly, at the bottom level we have PostgreSQL
and Redis, the main storage system and cache storage, respectively.
96
Chapter 4: Business logic
Summary
Dissecting business requirements into small and meaningful algebras usually translates
to a clear and concise design.
We have successfully achieved this, in addition to identifying the possible issues we may
need to handle or endure in the near future. We are now on the right path to write an
elegant, strongly-typed, and resilient application.
Next, we are going to get knee-deep into the HTTP layer.
97
Chapter 5: HTTP layer
Our library of choice for serving requests via HTTP is going to be Http4s, a purely
functional HTTP library built on top of Fs2 and Cats Effect. It is an extensive library,
so it is recommended to read its documentation1 if you’re not familiar with it.
It is fundamental to understand functional effects to work with Http4s, specifically Cats
Effect. In some cases, fs2.Stream is used as well, for which acquaintanceship with both
libraries would help.
Notwithstanding, let’s explore its API and unravel its potential.
1
https://http4s.org/
98
Chapter 5: HTTP layer
A server is a function
In order to compose routes, we need to model the possibility that not every single request
will have a matching route, so we can iterate over the list of routes and try to match the
next one. When we reach the end, we give up and return a default response, more likely
a 404 (Not Found). For such cases, we need a type that lets us express this optionality.
This can also be expressed using the OptionT monad transformer, as shown below.
With a bit of modification to our Request and Response types, we get the following.
There are some cases where we need to guarantee that given a request, we can return a
response (even if it is a default one). In such cases, we need to remove the optionality.
99
Chapter 5: HTTP layer
This is a fine detail we don’t really need to know about, though. Just remember the
core types, we are going to be using them a lot.
Ross A. Baker2 (Http4s maintainer) gave a great talk3 about this, walking us through
the history of changes and explaining the motivations behind the actual design.
Lastly, don’t worry if you still don’t understand everything. Let’s try and make some
sense of these definitions with some usage examples.
2
https://github.com/rossabaker/
3
https://www.youtube.com/watch?v=urdtmx4h5LE
100
Chapter 5: HTTP layer
HTTP Routes #1
Now that we have introduced Http4s, let’s see how we can model our HTTP endpoints,
or more commonly called HTTP routes.
We are going to represent routes using final case classes with an abstract effect type
that can at least provide a Monad instance, required by the HttpRoutes.of constructor.
Imports are going to be omitted for conciseness; please refer to the source code of the
project for the complete working version.
Brands
This is one of the easiest routes. It only exposes a GET endpoint to retrieve all the existing
brands. We are going to model it as follows.
101
Chapter 5: HTTP layer
The DSL-style makes the intentions quite clear as it shows both the happy and the
unhappy paths with different responses.
Our Category routes is fairly similar to the Brand routes, so we will skip it.
Items
object BrandQueryParam
102
Chapter 5: HTTP layer
extends OptionalQueryParamDecoderMatcher[BrandParam]("brand")
Since we should be able to filter by brand, we have introduced an optional query param-
eter named “brand”. The great thing about it is that we can perform validation using
Refined as well! See how BrandParam is defined below.
Exactly what we have learned in Chapter 1: combining Newtype and Refined to obtain
strongly-typed functions. We require our BrandParam to be a NonEmptyString.
To get this compiling, we need a QueryParamDecoder instance for refinement types.
We also need a QueryParamDecoder instance for any newtype but we will get to it in the
next chapter.
If we make a GET request to /items?brands=, we will get a response code 400 (Bad
Request) along with a message indicating that our input is empty. Otherwise, we will
retrieve the list of items filtering by the given brand.
If we omit the brand parameter, making a GET request to /items, we will just return
all the items, folding over our optional query parameter as shown in the following code
snippet.
103
Chapter 5: HTTP layer
This is how Http4s lets us indicate that a parameter is optional, using the symbol :?,
provided by its DSL.
Health check
The HealthCheck service reports both the status of Redis and PostgreSQL, which we are
going to expose via HTTP for easy access.
{
"redis": {
"status": "Okay"
},
"postgres": {
"status": "Okay"
}
}
104
Chapter 5: HTTP layer
not healthy. There are many valid designs in this space, but we are going to stick with
this simple one.
105
Chapter 5: HTTP layer
Authentication
In order to get access to the authenticated user, we need to use AuthedUser[F, User],
which is a case class isomorphic to (User, Request[F]), where User is some arbitrary
datatype we declare to represent a user in our system. In reality, though, it is a type
alias for ContextRequest, defined as a polymorphic case class.
The most common methods of authentication are cookies and bearer token. You can find
examples of both in the official docs, though, we are going to specialize on the latter.
106
Chapter 5: HTTP layer
JWT Auth
Our library of choice will be the opinionated Http4s JWT Auth4 , which offers some
functionality on top of Http4s and JWT Scala5 . Disclaimer: I am its maintainer.
Http4s provides an AuthMiddleware, previously mentioned. It is a type alias for a com-
plicated type.
Though, don’t let that scare you away, you will still get it when we see middlewares
shortly, accompanied by some examples. For now, it is fine to think of them as functions
AuthedRoutes[T, F] => HttpRoutes[F].
It requires a JwtAuth and a function JwtToken => JwtClaim => F[Option[A]], as shown
above. The former can be created as follows.
Once we have defined everything we need, we can use our AuthMiddleware as any other
middleware. E.g.
Following the same principle, we could implement authentication for other kinds of users,
such as admin users.
4
https://github.com/profunktor/http4s-jwt-auth
5
https://github.com/pauldijou/jwt-scala
107
Chapter 5: HTTP layer
It is worth mentioning that we can combine the HTTP routes of Users and AdminUsers,
achieving the functionality of having different roles.
108
Chapter 5: HTTP layer
HTTP Routes #2
Now that we have learned about authentication, let’s continue defining the secured and
administrative HTTP routes of our application.
Shopping Cart
So far, we have only dealt with open routes that don’t require authentication. This is
not the case for our shopping cart routes, though, which needs a user to be logged in.
109
Chapter 5: HTTP layer
NoContent()
}
def routes(
authMiddleware: AuthMiddleware[F, CommonUser]
): HttpRoutes[F] = Router(
prefixPath -> authMiddleware(httpRoutes)
)
object ItemIdVar {
def unapply(str: String): Option[ItemId] =
Either.catchNonFatal(ItemId(UUID.fromString(str))).toOption
}
Users are encouraged to define custom objects this way and use the given ones whenever
suitable. Http4s features IntVar, LongVar, and UUIDVar, among others.
Orders
110
Chapter 5: HTTP layer
AuthedRoutes.of {
case GET -> Root as user =>
Ok(orders.findBy(user.value.id))
def routes(
authMiddleware: AuthMiddleware[F, CommonUser]
): HttpRoutes[F] = Router(
prefixPath -> authMiddleware(httpRoutes)
)
We created a custom OrderIdVar as we did with ItemIdVar. Other than that, there’s
nothing we haven’t seen before, just another authenticated endpoint.
Checkout
111
Chapter 5: HTTP layer
def routes(
authMiddleware: AuthMiddleware[F, CommonUser]
): HttpRoutes[F] = Router(
prefixPath -> authMiddleware(httpRoutes)
)
In order to avoid hitting the remote payment system with invalid data, we need to
validate the credit card details entered by the user. In most cases, this will be validated
112
Chapter 5: HTTP layer
in the front-end but we also need to validate it in the back-end, for which we have defined
the Card datatype using refinement types, as shown below.
This is what we have for now, but software evolves quickly and might require further
refinements to avoid invalid data in our application.
Login
113
Chapter 5: HTTP layer
In any other case, we would return NotFound (404) when we get a UserNotFound error, or
a BadRequest (400) with a specific error message when we get an InvalidPassword error.
Here, however, we return Forbidden (403) in both cases to avoid leaking information.
Since this is an open endpoint, anyone could potentially issue a brute-force attack against
it and getting a 404 would reveal that a username exists and we just need to crack the
password.
The other thing to notice is the use of the extension method toDomain, which converts
refined values into common domain values.
Logout
def routes(
authMiddleware: AuthMiddleware[F, CommonUser]
114
Chapter 5: HTTP layer
): HttpRoutes[F] = Router(
prefixPath -> authMiddleware(httpRoutes)
)
We are accessing the headers of the request to find the current access token and invalidate
it, which means removing it from our cache, as we will see in the Auth interpreter.
Users
The following HTTP routes will be responsible for the registration of new users.
115
Chapter 5: HTTP layer
Note that we are only able to register new common users; it is not possible to create
new admin users. Once again, we are using decodeR for validation, toDomain for data
conversion, and recoverWith for business logic error handling.
Brands Admin
Finally, we reached the administrative endpoints. In this case, admin users should be
able to create new brands.
def routes(
authMiddleware: AuthMiddleware[F, AdminUser]
): HttpRoutes[F] = Router(
prefixPath -> authMiddleware(httpRoutes)
)
{
"brand_id": "7a465b27-0d ..."
}
For such purpose, we construct a JsonObject directly instead of creating a newtype with
a different Encoder instance. It comes in handy when the response is this simple.
116
Chapter 5: HTTP layer
Additionally, using decodeR, we validate the brand received in our request body is not
empty.
The AdminCategoryRoutes is quite similar so we will skip it.
Items Admin
Admin users should be able to create items as well as updating their prices.
def routes(
authMiddleware: AuthMiddleware[F, AdminUser]
): HttpRoutes[F] = Router(
prefixPath -> authMiddleware(httpRoutes)
)
At this point, nothing here should come as a surprise. Regardless, let’s have a look at
the domain model for item creation.
117
Chapter 5: HTTP layer
@newtype
case class ItemNameParam(value: NonEmptyString)
@newtype
case class ItemDescriptionParam(value: NonEmptyString)
@newtype
case class PriceParam(value: String Refined ValidBigDecimal)
USD is one of the many concrete implementations of the Money type, defined by the Squants
library.
Below we define the domain model for the item’s price update.
118
Chapter 5: HTTP layer
price: Money
)
Once again, leveraging our new favorite team Newtype-Refined, aiming for a strongly-
typed application.
119
Chapter 5: HTTP layer
Composition of routes
HTTP routes are functions, and functions compose. See the connection?
Say we have the following routes.
SemigroupK comes from Cats Core, so be sure to have import cats.syntax.all._ in scope.
It is very similar to Semigroup; the difference is that SemigroupK operates on type con-
structors of one argument, i.e. F[_].
6
https://typelevel.org/cats/typeclasses/semigroupk.html
120
Chapter 5: HTTP layer
Middlewares
Middlewares allow us to manipulate Requests and Responses, and are also plain functions.
The two most common middlewares have either of the following shapes:
Or:
type Middleware[F[_], A, B, C, D] =
Kleisli[F, A, B] => Kleisli[F, C, D]
There are a few predefined middlewares we can make use of such as the CORS middleware.
If we wanted to support CORS (Cross-Origin Resource Sharing) for all our routes, we
could do the following.
The official documentation is pretty good, you can find all this information right there,
so we are not going to be repeating the same thing in this book. Instead, we are going
to focus on compositionality and best practices.
Compositionality
Given that middlewares are functions, we can define a single function that combines all
the middlewares we want to apply to all our HTTP routes. Here is one simple way to
do it.
121
Chapter 5: HTTP layer
122
Chapter 5: HTTP layer
HTTP server
We are going to quickly see how to build our server. Up to this point, we have only seen
functions, but we need something else to get a running HTTP server. Let me introduce
you to our default server implementation, Ember.
EmberServerBuilder
.default[IO]
.withHttpApp(httpApp)
.build
The build method returns a Resource[F, Server[F]]. Since Resource forms a Monad, we
can sequence multiple resources, such as a remote database or a message broker, and
run them all together.
Be aware that Ember is a relatively new interpreter. If you are looking for a battle-tested
server, I would recommend Blaze. Same goes for the client side.
Notes
Ember is the newest HTTP Server and Client for Http4s
In Chapter 9, when we put all the pieces together, we are going to see how to initialize
our dependencies and start our server up.
123
Chapter 5: HTTP layer
Entity codecs
import org.http4s.circe.CirceEntityEncoder._
Notes
We refer to codecs as having both an encoder and a decoder
Decoding, on the other hand, it is already abstracted away by JsonDecoder, which pro-
vides a default instance for any F[_]: Sync that can be summoned at the edge of the
application. If we were working on an API that needed to decode other formats such as
XML, we would either need an EntityDecoder[F, A] or come up with our own capability
trait, e.g. XmlDecoder.
Additionally, we need instances of Circe’s Decoder and Encoder for our datatypes. We
will learn more about it in the next chapter.
124
Chapter 5: HTTP layer
HTTP client
Up until now, we have only talked about the server-side of what Http4s offers. Yet, little
did we talk about the client-side.
Expectedly, Http4s also comes with support for clients, the newest implementation being
Ember as on the server-side.
Payment client
trait PaymentClient[F[_]] {
def process(payment: Payment): F[PaymentId]
}
As usual, we do not have any implementation details in our interface. This is going to
be delegated to our interpreter, where we are going to use a real HTTP client.
object PaymentClient {
def make[F[_]: BracketThrow: JsonDecoder](
client: Client[F]
): PaymentClient[F] =
new PaymentClient[F] with Http4sClientDsl[F] {
val baseUri = "http: //localhost:8080/api/v1"
Our interpreter takes a Client[F] as an argument, which is the abstract interface for all
the different HTTP clients the library supports. It comes from the org.http4s.client
package.
Notice how we also mix-in the Http4sClientDsl interface, which will grant us access to
a friendly DSL to build HTTP requests.
Our process function only makes a call to the remote API, expecting a PaymentId as the
response body. The fetchAs function is defined as follows.
125
Chapter 5: HTTP layer
def fetchAs[A](
req: Request[F]
)(implicit d: EntityDecoder[F, A]): F[A]
This is the most optimistic scenario as we are not handling the possibility of a dupli-
cate payment error. To do so, we need a function different from fetchAs that lets us
manipulate the response we get from the client. What we need is run(req).use(f).
In the first edition, we have used fetch but it has been deprecated.
This is another function given by Client[F] that takes a Request[F] and gives us a
Resource[F, Response[F]]. Once we call use, we get access to a function Response[F] =>
F[A]. This is our opportunity to do things right.
When we get a Response, we check its status. If it is either 200 (Ok) or 409 (Conflict),
we know we can expect a PaymentId as the body of the response. In such a case, we try
to automatically decode it using our JSON decoders. This is what the asJsonDecode[A]
function does, defined as follows.
Though, in our implementation, we are using syntactic sugar instead of calling the
function directly.
That is all we have to do. If other kinds of errors occur, such as a network failure, we
are going to let it fail. Whatever component makes use of it, should handle that.
126
Chapter 5: HTTP layer
Creating a client
EmberClientBuilder
.default[F]
.build
EmberClientBuilder
.default[F]
.build
.map(PaymentClient.make[F])
In Chapter 9, we are going to see how all the resources in our application are composed
together, including our HTTP Client.
127
Chapter 5: HTTP layer
Summary
The HTTP protocol is ubiquitous in this era, for which learning about defining open and
authenticated HTTP routes, handling requests and responses, composing middlewares,
and creating HTTP clients, is generally a great skill to have.
We have learned the most important things about Http4s, and discovered it is a full-
fledged HTTP library where compositionality is a first-class citizen.
It is now time to take a little detour to learn about typeclass derivation.
128
Chapter 6: Typeclass derivation
In Chapter 4, we have defined the most important datatypes of our domain. Yet, we
intentionally elided some irrelevant parts in that context. Same story with Chapter 5,
where some topics were intentionally left unexplained. It is now time to pay the debt
and learn about typeclass derivation, which is essential to our domain model.
Manual typeclass derivation can be complicated and time-consuming, but for most cases
it is unnecessary. In this chapter, we will learn how it can be accomplished automatically
using existing libraries.
We will employ this technique on the relevant components of our shopping cart system,
emphasizing its practical application.
After all, this book ought to pay tribute to its title.
129
Chapter 6: Typeclass derivation
Standard derivations
Most of our datatypes – including newtypes – will need typeclass instances for Eq, Order,
and Show, among others. In this space, we have two options: either we write them
manually, or we derive them.
There are two great libraries capable of such a thing in Scala: Shapeless1 and Magno-
lia2 . However, these libraries are bare metal; they only provide the machinery to derive
typeclasses. To get something fruitful out of it, we need some extra work.
Fortunately, there exists a good selection of libraries that cover this space.
This list is presented to create awareness of some options. Yet, readers are encouraged
to research and pick the most suitable for the use case at hand. Having said that, in our
application we will use Derevo because of its extensive support for many of the libraries
we use, such as Newtypes, Cats, and Circe. Furthermore, I consider it user-friendly.
To get started, nothing better than an example, right? You would probably understand
what it does by just looking at it.
import derevo.cats._
import derevo.derive
object Person {
@derive(eqv, order, show)
@newtype
case class Age(value: Int)
1
https://github.com/milessabin/shapeless/
2
https://github.com/softwaremill/magnolia
3
https://github.com/tofu-tf/derevo
4
https://github.com/spotify/magnolify
5
https://github.com/typelevel/kittens
6
https://github.com/scalalandio/catnip
130
Chapter 6: Typeclass derivation
Got it? The @derive macro-annotation takes a variable number of arguments. In this
case, we use a few coming from derevo.cats._, which – as you might have guessed – is
the built-in Cats support for typeclass derivation. We should now be able to summon
those instances, as well as using extension methods that require them. E.g.
import cats._
import cats.syntax.all._
Without the @derive annotation, this would be the equivalent for newtypes.
object Person {
@newtype case class Age(value: Int)
object Age {
implicit val eq: Eq[Age] = deriving
implicit val order: Order[Age] = deriving
implicit val show: Show[Age] = deriving
}
131
Chapter 6: Typeclass derivation
Warning
The order of the @derive and @newtype annotations matters
If you have read the first edition, you might recall that Coercible was used to derive
instances for newtypes on demand, i.e. via an import. However, this was a somewhat
controversial decision since its use is not recommended by the library author even though
it certainly removes a lot of boilerplate. We couldn’t have used Derevo back then because
newtypes were unsupported. Fortunately, this situation has changed so this time there
is no need to do that again.
132
Chapter 6: Typeclass derivation
JSON codecs
In the previous chapter, it was mentioned that the HTTP routes will be responsible for
serializing and deserializing the data that goes through the wire. For such purpose, we
use the Circe JSON library.
The main typeclasses are Decoder and Encoder. Continuing with our previous example,
let’s see how we can add support for these as well.
object Person {
@derive(decoder, encoder, eqv, order, show)
@newtype
case class Age(value: Int)
import io.circe.parser.decode
import io.circe.syntax._
Map codecs
We sometimes need Circe’s KeyDecoder and KeyEncoder instances when using a Map data
structure. This is the case for ItemId, the key of the inner Map of Cart.
@derive(eqv, show)
@newtype
case class Cart(items: Map[ItemId, Quantity])
133
Chapter 6: Typeclass derivation
import derevo.circe.magnolia._
134
Chapter 6: Typeclass derivation
Orphan instances
Following the typeclass derivation approach, we are forced to mix JSON codecs with our
domain model. I believe it’s a good trade-off. However, in the first edition such codecs
were placed into a single file, unifying all the instances for the entire domain. Wasn’t
that a better way?
Typeclass instances are commonly placed in companion objects, as the Scala compiler
can easily find them there, and we do not need extra imports. This also guarantees
global coherence since orphan instances would be immediately rejected, or even worse,
silently overridden.
Notes
Global coherence allows only one typeclass instance per type
So this common practice is actually recommended and it works well for Eq, Show, Order,
etc. I think the fundamental problem is that JSON codecs should probably not be
typeclasses at all. Sometimes we need to encode or decode the same data in a different
way, e.g. one to be the response body of an HTTP route; other to serialize data to fit in
our database.
The current way around this is to create another newtype with a different codec instances,
even when the datatype represents exactly the same thing.
In the next chapter, we will learn about an alternative approach used by SQL codecs,
which are plain values invoked explicitly instead of typeclass instances.
Occasionally, we may also need instances for datatypes we do not own, e.g. those coming
from a third-party library. The current approach followed in this edition is to expose
them in the domain package object.
135
Chapter 6: Typeclass derivation
The downside of this strategy is that we need to manually ensure we have the right
import in scope: import shop.domain._. However, since these instances are not available
anywhere else, it should not be a problem.
Consistency in handling orphan instances is key to a principled application.
136
Chapter 6: Typeclass derivation
Identifiers
In our domain, we have many unique identifiers, or IDs for short. We represent them
as a newtype over a UUID, which can be randomly created using UUID.randomUUID, but
that’s a side-effect we need to capture in our effect type.
A better way to deal with this problem is, as we have seen in Chapter 2, by introducing
a common effect, also known as capability trait. Following this line of reasoning, here
we have a new effect named GenUUID, which exposes two functions: one to generate a
random UUID; another to parse a String as a possible valid UUID.
trait GenUUID[F[_]] {
def make: F[UUID]
def read(str: String): F[UUID]
}
object GenUUID {
def apply[F[_]: GenUUID]: GenUUID[F] = implicitly
It features a default instance for any F[_]: Sync and a summoner method.
Another option could be fuuid7 , a functional library that also provides integration with
Circe, Doobie, and Http4s. However, we should think twice before adding a dependency
to our classpath, and in this case, it might not be worth the trouble.
Classy Isomorphism
137
Chapter 6: Typeclass derivation
However, it might be hard to justify an extra dependency to only use Iso. In such case,
we can choose to represent it as a simple case class with two type parameters.
If you find yourself in a similar position and decide to go with the custom implementation,
it is recommended to at least test the round-trip conversion. The following code snippet
shows the equivalence that should hold for any isomorphism.
trait IsUUID[A] {
def _UUID: Iso[UUID, A]
}
object IsUUID {
def apply[A: IsUUID]: IsUUID[A] = implicitly
The reason for IsUUID to exist is so we can create our IDs directly instead of creating a
UUID and perform a manual conversion each time. Also, to demonstrate this technique
and show how it can be used in other applications.
With the following ID object, defined under shop.domain.
object ID {
def make[
F[_]: Functor: GenUUID, A: IsUUID
]: F[A] =
GenUUID[F].make.map(IsUUID[A]._UUID.get)
def read[
F[_]: Functor: GenUUID, A: IsUUID
](str: String): F[A] =
138
Chapter 6: Typeclass derivation
GenUUID[F].read(str).map(IsUUID[A]._UUID.get)
}
GenUUID[F].make.map(ItemId.apply) // F[ItemId]
Another valid design could be having a GenId effect instead of a singleton object.
trait GenID[F[_]] {
def make[A: IsUUID]: F[A]
def read[A: IsUUID](str: String): F[A]
}
Although this may seem to contradict what was said in Chapter 2 about not having type-
class constraints in our interface, it is a valid design. As we can observe, the constraint
is on A, not on F, which is the case I usually recommend to avoid.
Custom derivation
We are going to restrict custom derivations to work only for newtypes, which is an
operation that can be generalized.
trait Derive[F[_]]
extends Derivation[F]
with NewTypeDerivation[F] {
The IsUUID typeclass can be automatically derived with the following object.
139
Chapter 6: Typeclass derivation
This allows us to use @derive(uuid) as we do with other derivations like eqv and show,
and it is exactly what we do in our domain with newtypes such as BrandId and CartId.
import shop.optics.uuid
We are now ready to use ID.make[F, BrandId], as shown in the examples above.
140
Chapter 6: Typeclass derivation
Validation
In many cases, we use refinement types that need to be either encoded or decoded as
JSON. For this purpose, we are going to use the circe-refined library, which can derive
a few instances for us.
Our Card domain model is one of the most refined types we have so far. However, if we
tried to derive a Decoder for it, it would fail.
import io.circe.refined._
Well, not that easy. Our derivation still wouldn’t compile. Remember we have the
following refinement types in our Card model.
@derive(encoder, show)
@newtype
case class CardNumber(value: CardNumberPred)
@derive(encoder, show)
@newtype
case class CardExpiration(value: CardExpirationPred)
@derive(encoder, show)
@newtype
case class CardCVV(value: CardCVVPred)
141
Chapter 6: Typeclass derivation
name: CardName,
number: CardNumber,
expiration: CardExpiration,
cvv: CardCVV
)
Unfortunately, the Circe Refined module doesn’t come with instances for Size[N], where
N is an arbitrary literal number. Yet, that’s easy to fix by making the following instance
available.
Refined needs a Validate instance for every possible size. Fortunately, we can abstract
over its arity with a little bit of work and finally get our Card derivation working.
142
Chapter 6: Typeclass derivation
Http4s derivations
For Http4s, we only define a custom derivation for QueryParamDecoder, used by a few
datatypes. One of them is BrandParam, used by both ItemRoutes and AdminBrandRoutes.
@derive(queryParam, show)
@newtype
case class BrandParam(value: NonEmptyString) {
def toDomain: BrandName =
BrandName(value.toLowerCase.capitalize)
}
For this derivation to compile, we need two different things. Firstly, we need a
QueryParamDecoder instance for any Refined type, as written in Chapter 5.
Secondly, we need a queryParam object the can perform such derivation – only for new-
types – by extending Derive[QueryParamDecoder], as we have done with uuid.
import org.http4s.QueryParamDecoder
Note that it is also possible to derive this instance for simple datatypes, though, that
requires resorting to Magnolia, which may seem daunting to the inexperienced.
143
Chapter 6: Typeclass derivation
Higher-kinded derivations
Although we are not going to use this feature in our application, it wouldn’t hurt to
learn a few other interesting things Derevo is capable of.
Higher-kinded types
Higher-kinded types are types that have type arguments. We will focus on the most
common example of such types, which has shape F[A] (and kind * -> *, meaning that
they take a concrete type and return a concrete type). For instance, Option is a higher-
kinded type that takes a concrete type (e.g. Int) and produces another concrete type
(Option[Int]).
Here’s an example using derevo-cats-tagless, which supports Cats Tagless8 .
import derevo.tagless.flatMap
@derive(flatMap)
sealed trait HigherKind[A]
case object KindOne extends HigherKind[Int]
case object KindTwo extends HigherKind[String]
It is worth noticing that by deriving FlatMap, we get access to another set of typeclasses
such as Functor, Apply, and Semigroupal.
Higher-order functors
Derevo also supports derivation for higher-order functors, loosely speaking. Once again,
we will only explore the most common of such types, which have shape X[F[A]] and kind
(* -> *) -> *. Most of our tagless final encoded algebras fit this shape. For example, a
tagless algebra Alg[F[_]] takes a type constructor F[_] as a type parameter, which has
kind * -> *. Examples may include IO, Option, or Either[String, *], among others.
8
https://typelevel.org/cats-tagless/
144
Chapter 6: Typeclass derivation
Let’s see how an instance of ApplyK can be derived for our custom algebra, also via the
Cats Tagless module.
import derevo.tagless.applyK
@derive(applyK)
trait Alg[F[_]] {
def name: F[String]
}
By deriving ApplyK, we get access to a few other typeclasses such as FunctorK and
SemigroupalK. Let’s look at the following example, which shows how these typeclasses’
methods are used.
These typeclasses are quite advanced and won’t be used in our application so don’t lose
your mind on them. This is just an example of how far we can get with Derevo.
145
Chapter 6: Typeclass derivation
Summary
This chapter was brief but crucial to our cause. Deriving typeclass instances is a super-
power we can easily leverage with libraries like Derevo.
We have mainly learned that most of the typeclasses we need have built-in support in
Derevo but whenever the need arises, we know more or less where to look. Who knows?
Perhaps the instances you come up with can be shared with the community.
If you are still eager to learn more about Magnolia and other derivation frameworks, you
should definitely pursue it. It is a very interesting topic but we have reached the limit
of what was planned for the scope of this book.
We now have everything we need in terms of custom derivations for our application and
can continue its steady development with the persistent layer.
146
Chapter 7: Persistent layer
After a quick necessary detour on typeclass derivation, we are now back on business,
and already halfway through the book: Time to talk about interpreters! Some of the
algebras need implementations based on PostgreSQL; others based on Redis.
In this chapter, we are going to learn how to deal with blocking and non-blocking oper-
ations, and how to manage a connection pool, among other things.
147
Chapter 7: Persistent layer
In the Scala ecosystem, there are a couple of libraries that let us interact with Postgres.
Arguably, the most popular one in the FP ecosystem is Doobie1 , having more than 1.8k
stars at the moment of writing.
Quoting the Wikipedia2 :
Doobie is a JDBC wrapper that integrates very well with Cats Effect and Fs2. Those
looking for a mature and battle-tested library with great documentation3 should go for
it.
For those looking for a non-blocking library, there is Skunk4 , a purely functional and
asynchronous Postgres library for Scala. It talks the Postgres protocol directly (no
JDBC), and it features excellent error reporting.
It has grown a lot in terms of adoption and stability over the past year, so I would
personally endorse its use in production systems. I think it will eventually replace
Doobie, it is only a matter of time.
If you are already acquainted with Doobie, you will observe that many properties are
shared with Skunk as well. Let’s now explore Skunk’s API5 , as we will be using it in
our application.
Session Pool
First, we need to connect to the Postgres server. Skunk supports acquiring a connection
using Session.single[F]( ...), which returns a Resource[F, Session[F]]. This is fine for
simple examples, but for an application, we need a pool of sessions to be able to handle
concurrent operations. What we need is the following construct.
1
https://github.com/tpolecat/doobie
2
https://en.wikipedia.org/wiki/PostgreSQL
3
https://tpolecat.github.io/doobie/docs/index.html
4
https://github.com/tpolecat/skunk
5
https://tpolecat.github.io/skunk/index.html
148
Chapter 7: Persistent layer
Session
.pooled[F](
host = "localhost",
port = 5432,
user = "postgres",
password = Some("my-pass")
database = "store",
max = 10
)
People usually get puzzled staring at this type signature, and with reason! A “resource
of resource” is not something very common out in the wild. In this case, it represents
a pool of sessions limited to a maximum of ten open sessions at a time, as specified
by max = 10. Therefore, interpreters that need database access will take a Resource[F,
Session[F]] and call use for every transaction that needs to be run. Skunk will handle
concurrent access for us.
In such cases, it might help introducing a type alias. Feel free to do so.
This is the safest usage of sessions since our Postgres interpreters will only perform
standalone operations, which might then be combined concurrently at a higher level.
Connection check
When we acquire a connection to Postgres, it’s always good to perform a check as soon
it happens, and maybe log a message about it.
This is usually a trivial task accomplished by evalTap, defined by Resource. For example,
we could query the current version of the Postgres server.
def checkPostgresConnection(
postgres: Resource[F, Session[F]]
): F[Unit] =
postgres.use { session =>
session
.unique(sql"select version();".query(text))
.flatMap { v =>
Logger[F].info(s"Connected to Postgres $v")
}
}
149
Chapter 7: Persistent layer
Session
.pooled[F](
host = "localhost",
port = 5432,
user = "postgres",
password = Some("my-pass")
database = "store",
max = 10
)
.evalTap(checkPostgresConnection)
Queries
We need to be able to retrieve rows of information from one or more database tables.
For this purpose, there exists the Query type.
Notes
A Query is a SQL statement that can return rows
We can observe a sql interpolator that parses a SQL statement into a Fragment to then
be turned into a Query by calling the query method. Lastly, we have varchar, which is
a Decoder defining a relationship between the Postgres type VARCHAR and the Scala type
String.
To learn more about it, have a look at the Schema Types6 reference. You can also
explore its source code; they can all be found under the skunk.codec package.
In order to execute the query, we need a Session[F]. E.g.
In addition to execute, there are the option and unique methods, returning F[Option[A]]
and F[A], respectively.
6
https://tpolecat.github.io/skunk/reference/SchemaTypes.html
150
Chapter 7: Persistent layer
Commands
We have seen how we can get a result from a Query. In order to insert, update, or
delete some records, we need a Command, which typically performs state mutation in the
database.
Notes
A Command is a SQL statement that does not return rows
Allegedly, the country table has only two columns: an id of type INT8, and a name of
type VARCHAR.
Skunk speaks in terms of Postgres schema types rather than ANSI types
or common aliases, thus we use INT8 here rather than BIGINT.
The return type indicates the number of arguments we need to supply in order to execute
the statement, defined as a product type (aliased ~). E.g.
session.prepare(insertCmd).use {
_.execute(1L ~ "Argentina").void
}
We have created a prepared statement by calling the prepare method, and we have got
back a Resource[F, PreparedCommand[F, A]]. Once we are ready to execute the statement,
we call use to access the inner PreparedCommand that lets us execute it by supplying the
required arguments. Finally, we call void to ignore its result, which might indicate the
number of rows inserted.
We could also do something with its result (exercise left to the reader). However, Postgres
rarely returns “0 rows inserted”. If anything goes wrong, we will more likely get an error
raised in our effect type.
Instead of creating a Command[Long ~ String] we could model it using a case class.
151
Chapter 7: Persistent layer
It lets us maintain our model as our database evolves. Skunk lets us generically derive
a Codec, which is both a Decoder and an Encoder, as demonstrated below.
The method gimap is a version of imap that maps out to a product type based on a
shapeless generic, hence the g. Or we could also do it manually.
Warning
Unfortunately, gimap does not work with newtypes
Interpreters
Now that Skunk has been introduced, let’s get to work. It has been mentioned that
Brands, Categories, Items, Orders, and Users will be persisted in PostgreSQL.
Next, let’s delve into the fine details of the interpreters.
Brands
trait Brands[F[_]] {
def findAll: F[List[Brand]]
def create(name: BrandName): F[Unit]
}
First of all, we need to define the Postgres table, or also called schema definition. We
are going to call it brands.
152
Chapter 7: Persistent layer
Once we have the schema, we need to define the codecs, queries, and commands. A good
practice is to define them in a private object in the same file. I like to add the SQL
suffix to these objects.
Let’s start explaining codecs, which are going to be defined within a private object
BrandSQL.
The brandId and brandName codecs are defined in a shared file under shop.sql.codecs,
since these are also needed by other interpreters.
We can also choose to use predefined codecs and construct each value manually but the
former is usually better for re-usability.
153
Chapter 7: Persistent layer
sql"""
INSERT INTO brands
VALUES ($codec)
""".command
In order to run these queries and commands, we need to take a Resource[F, Session[F]],
as previously explained.
object Brands {
def make[F[_]: GenUUID: MonadCancelThrow](
postgres: Resource[F, Session[F]]
): Brands[F] =
new Brands[F] {
import BrandSQL._
In the findAll query, we access the Session[F] of the pool by calling the use method,
and then call the execute method passing our previously defined query as a parameter.
We can use execute because there are no inputs to our query, indicated by its first type
Void. It intentionally returns a List[Brand] because we are assuming that the number
of brands in our database is considerably small, so it can all fit into memory. Later in
this chapter, we are going to see how we can deal with large records that might not.
In the create method, we use a prepared statement. Once we access the Session[F], we
call the prepare method passing the insert command as a parameter, which returns a
Resource[F, PreparedCommand[F, Brand]].
154
Chapter 7: Persistent layer
cmd.execute(Brand(id, name)).as(id)
}
}
Next, we call use on this resource, call execute on our prepared command (passing a
Brand as an argument), and finally call as(id) to return the BrandId created by the ID
maker described in previous chapters.
Categories
The Categories interpreter is nearly identical to the Brands one so we will skip its imple-
mentation. Still, we can have a look at its schema definition.
Following what we have learned with the Brands interpreter, can you write this one on
your own? If you get stuck, you can always refer to the source code for help.
Items
This one is very interesting because it defines five different methods. Let’s recap on its
algebra.
trait Items[F[_]] {
def findAll: F[List[Item]]
def findBy(brand: BrandName): F[List[Item]]
def findById(itemId: ItemId): F[Option[Item]]
def create(item: CreateItem): F[ItemId]
def update(item: UpdateItem): F[Unit]
}
155
Chapter 7: Persistent layer
This one is arguably our most complex table definition, as it has foreign key constraints
to other tables. Still, it should be straightforward to follow.
We are now going to define the following values within a private object ItemSQL; in this
case, we are going to do so step by step because of its length.
The first function is selectAll, which joins values from three different tables.
We could have defined a Codec as well, but we will see soon why we haven’t.
156
Chapter 7: Persistent layer
See how the first type of Query has become BrandName instead of Void? It will be the
argument of this query.
The third function is similar to the previous one, but it takes an ItemId instead of a
BrandName.
157
Chapter 7: Persistent layer
This one is fairly simple as we only need to update the price of a specific item.
object Items {
def make[F[_]: Concurrent: GenUUID](
postgres: Resource[F, Session[F]]
): Items[F] =
new Items[F] {
import ItemSQL._
158
Chapter 7: Persistent layer
session.prepare(selectById).use { ps =>
ps.option(itemId)
}
}
Let’s analyze the findBy and findById methods, which are distinct from our previous
examples.
session.prepare(selectByBrand).use { ps =>
ps.stream(brand, 1024).compile.toList
}
This is how we execute queries that have arguments. We use a prepared query, which
in this case returns a Resource[F, PreparedQuery[F, BrandName]] (similar to a prepared
command). Once we access the resource, we call the stream method, which returns an
fs2.Stream[F, Item], supplying a brand name and a chunk size. Yet, we want to return
a List[Item], so we call compile.toList, which is an effectful operation on a stream.
The avid reader might have noticed that items could possibly not fit into memory, so
forcing this stream into a list – i.e. forcing all the elements of the stream into memory –
might not be a wise decision. We have a few options here.
1. Change the algebra’s return type from F[List[Item]] to Stream[F, Item]. In this
case, the implementation would become something along these lines.
159
Chapter 7: Persistent layer
We could paginate the results before returning the HTTP response, or we could return
the stream directly. Http4s supports streams out of the box (it returns a chunked transfer
encoding7 response). Try it out yourself.
2. Keep the original return type F[List[Item]] but limit the amount of results. We
can easily achieve this by receiving a limit argument and writing the according
SQL query (e.g. LIMIT 100).
3. Change the algebra’s return type from F[List[Item]] to a custom type
F[PaginatedItems], which contains the current list of items, and a flag indicating
whether there are more items or not. It requires some extra amount of work, but
it is doable using cursors, provided by Skunk. Instead of calling p.stream, we can
call p.cursor, which gives us a Resource[F, Cursor[F, Item]].
You can already imagine how to implement it, right? Readers are encouraged to try and
solve it as an exercise.
7
https://en.wikipedia.org/wiki/Chunked_transfer_encoding
160
Chapter 7: Persistent layer
In addition to the user_id foreign key, we can see how items are going to be represented
using the native JSONB type.
Here is the Decoder (again, not a Codec) for Order, defined within the private object
OrderSQL.
We are using a new codec jsonb, which is backed by the Circe library. It takes a type
parameter A, and it requires instances of both io.circe.Encoder and io.circe.Decoder to
be in scope for A. To use this codec, you need to add the extra dependency skunk-circe
and have import skunk.circe.codec.all._ in scope.
Next up are the queries.
161
Chapter 7: Persistent layer
You can see why we haven’t defined a Codec[Order]; because creating a new order also
takes a UserId, hence our Encoder[UserId ~ Order].
Lastly, here is the Orders interpreter.
object Orders {
def make[F[_]: Concurrent: GenUUID](
postgres: Resource[F, Session[F]]
): Orders[F] =
new Orders[F] {
import OrderSQL._
def create(
userId: UserId,
paymentId: PaymentId,
items: NonEmptyList[CartItem],
total: Money
): F[OrderId] =
postgres.use { session =>
session.prepare(insertOrder).use { cmd =>
ID.make[F, OrderId].flatMap { id =>
val itMap = items.toList.map(x => x.item.uuid -> x.quantity).toMap
val order = Order(id, paymentId, itMap, total)
cmd.execute(userId ~ order).as(id)
}
162
Chapter 7: Persistent layer
}
}
}
There is nothing out of the ordinary, we have seen all of this in previous interpreters.
trait Users[F[_]] {
def find(
username: UserName
): F[Option[UserWithPassword]]
def create(
username: UserName,
password: EncryptedPassword
): F[UserId]
}
Let’s look at the codecs, queries, and commands for this one.
163
Chapter 7: Persistent layer
sql"""
SELECT * FROM users
WHERE name = $userName
""".query(codec)
object Users {
def make[F[_]: GenUUID: MonadCancelThrow](
postgres: Resource[F, Session[F]]
): Users[F] =
new Users[F] {
import UserSQL._
164
Chapter 7: Persistent layer
}
}
}
}
Now let’s look at the first function, find. We use q.option, another function on
PreparedQuery, which expects exactly zero or one result; otherwise, it raises an error.
Next, we pattern match and, if we get a result, we build a UserWithPassword which
represents a User along with the EncryptedPassword.
The second function, create, does a few things. It:
165
Chapter 7: Persistent layer
Redis4Cats8 is a purely functional and asynchronous Redis client built on top of Cats
Effect, Fs2, and Java’s Lettuce.
Quoting the official Redis website9 :
We will be using Redis to store authentication tokens and shopping carts. It seems a
great fit since we need to set expiration times for both, and this is a feature supported
natively.
Connection
RedisCommands[F, K, V] is an interface from which we can access all the available com-
mands. Both K and V are the types of keys and values, respectively, making it type-safe.
We cannot increment values of type String, for example.
The Redis.apply[F] function requires a Log[F] instance, among other constraints. This
effect comes from redis4cats and it can be derived from Logger by log4cats. For that
to work, we need a single import in scope.
import dev.profunktor.redis4cats.log4cats._
8
https://github.com/profunktor/redis4cats
9
https://redis.io/
166
Chapter 7: Persistent layer
def checkRedisConnection(
redis: RedisCommands[F, String, String]
): F[Unit] =
redis.info.flatMap {
_.get("redis_version").traverse_ { v =>
Logger[F].info(s"Connected to Redis $v")
}
}
We can acquire as many RedisCommands as we need. In our case, we will need a single
one of types String, as in the example above.
Interpreters
There is not much ceremony in getting started with Redis for Cats. Once we acquire a
RedisCommands instance, we are ready to make use of it.
Shopping Cart
Let’s first have a look at the dependencies of the ShoppingCart interpreter and later
analyze its functions in detail.
object ShoppingCart {
def make[F[_]: GenUUID: MonadThrow](
items: Items[F],
redis: RedisCommands[F, String, String],
exp: ShoppingCartExpiration
): ShoppingCart[F] = ???
}
10
https://redis.io/commands#hash
167
Chapter 7: Persistent layer
"3"
redis>
def add(
userId: UserId,
itemId: ItemId,
quantity: Quantity
): F[Unit] =
redis.hSet(userId.show, itemId.show, quantity.show) *>
redis.expire(userId.show, exp.value).void
It adds an item id (field) and a quantity (value) to the user id key, and it sets the
expiration time of the shopping cart for the user.
Next is get, which does a little bit more.
It tries to find the shopping cart for the user via the hGetAll function, which returns a
Map[String, String], or a Map[K, V], generically speaking.
If it exists, it parses both fields and values into a List[CartItem] and finally, it calculates
the total amount. The subTotal function is defined on CartItem.
168
Chapter 7: Persistent layer
USD(item.price.amount * quantity.value)
}
Warning
Do not do this at home without adding a law test!
In the next chapter, we’re going to learn more about law testing.
Next is delete, which simply deletes the shopping cart for the user.
Followed by removeItem, which removes a specific item from the shopping cart.
169
Chapter 7: Persistent layer
It retrieves the shopping cart for the user (if it exists) and it updates the quantity of
each matching item, followed by updating the shopping cart expiration.
Authentication
It has been previously mentioned that the Auth algebra might need to change, and
inevitably, it is going to happen. We are going to define two different algebras; the first
one, Auth, is the most generic one.
trait Auth[F[_]] {
def newUser(username: UserName, password: Password): F[JwtToken]
def login(username: UserName, password: Password): F[JwtToken]
def logout(token: JwtToken, username: UserName): F[Unit]
}
Our second algebra is specialized in retrieving a specific kind of user, indicated by its
second type parameter A.
trait UsersAuth[F[_], A] {
def findUser(token: JwtToken)(claim: JwtClaim): F[Option[A]]
}
The findUser function is curried to make the integration with Http4s JWT Auth much
easier. Remember that the authenticate function from JWTAuthMiddleware has the shape
JwtToken => JwtClaim => F[Option[A]].
170
Chapter 7: Persistent layer
Our function tries to find the user by token in Redis, and if there is a result, it tries to
decode the JSON as the desired User type. A token is persisted as a simple key, with its
value being the serialized user in JSON format.
Next, we have an interpreter for AdminUser.
It compares the token with the unique admin token that has been passed to the inter-
preter on initialization (more on this in Chapter 9), and in case of match, it returns the
adminUser stored in memory (remember that there is a unique admin user).
Both the admin and common smart constructors are defined in the UsersAuth companion
object.
Let’s now see what the structure of the Auth interpreter looks like and then analyze each
function step by step.
object Auth {
def make[F[_]: MonadThrow](
tokenExpiration: TokenExpiration,
tokens: Tokens[F],
users: Users[F],
redis: RedisCommands[F, String, String],
crypto: Crypto
): Auth[F] = new Auth[F] {
private val TokenExpiration = tokenExpiration.value
171
Chapter 7: Persistent layer
• Users[F] allows us to find and create new users (defined in Chapter 4).
• RedisCommands is the Redis interface.
• Tokens[F] allows us to create new JWT tokens.
trait Tokens[F[_]] {
def create: F[JwtToken]
}
trait Crypto {
def encrypt(value: Password): EncryptedPassword
def decrypt(value: EncryptedPassword): Password
}
These two interfaces can be implemented in different ways. For a concrete example,
please refer to the source code that supplements this book, as they are not relevant
enough to be part of the book.
Our first function is newUser.
Here we try to find the user in Postgres. If it doesn’t exist, we proceed with its creation;
otherwise, we raise a UserNameInUse error.
Creating a user means persisting it in Postgres, creating a JWT token, serializing the
user as JSON, and persisting both the token and the serialized user in Redis for fast
access, indicating an expiration time.
Our login function comes next.
172
Chapter 7: Persistent layer
InvalidPassword(user.name).raiseError[F, JwtToken]
case Some(user) =>
redis.get(username.show).flatMap {
case Some(t) => JwtToken(t).pure[F]
case None =>
tokens.create.flatTap { t =>
redis.setEx(
t.value, user.asJson.noSpaces, TokenExpiration
) *>
redis.setEx(username.show, t.value, TokenExpiration)
}
}
}
We try to find the user in Postgres. If it doesn’t exist, we simply raise an UserNotFound
error; if it exists, we validate that the stored encrypted password matches the one given
to the function, and raise an InvalidPassword error if they don’t match. If they do, we
search for the token by user in Redis (in case the user has already been logged in). If
we get a token, we return it; otherwise, we create a new token and persist both the user
and the token with an expiration time.
When we get an existing token, we could also extend its lifetime, i.e. updating its ex-
piration. However, this will only make sense when the expiration time of our JWTs is
greater than the one we configure for our tokens stored in Redis.
Lastly, we have the logout function, which is the simplest.
def logout(
token: JwtToken,
username: UserName
): F[Unit] =
redis.del(token.show) *>
redis.del(username.show).void
All it does is deleting the token and the user from Redis, if any.
173
Chapter 7: Persistent layer
Health check
This will indicate the connection status of both Redis and Postgres. If the status is OK,
it would be Okay; otherwise, Unreachable. Let’s have a look at its interpreter.
object HealthCheck {
def make[F[_]: Temporal](
postgres: Resource[F, Session[F]],
redis: RedisCommands[F, String, String]
): HealthCheck[F] =
new HealthCheck[F] {
For Redis, we simply ping the server; for Postgres, we run a simple query. Both actions
have a timeout of one second and are performed in parallel, using the parMapN function.
174
Chapter 7: Persistent layer
In both cases, if anything goes wrong (e.g. cannot connect to server), we return
Unreachable using the orElse combinator from ApplicativeError.
175
Chapter 7: Persistent layer
Blocking operations
We have learned about Skunk and Redis4Cats, which are both asynchronous, so we didn’t
have to deal with blocking operations. However, it is very common in the database world
to deal with such cases.
For this purpose, Cats Effect 2 provides a Blocker datatype that merely wraps an
ExecutionContext. Most compatible functional libraries that need to deal with block-
ing operations would take a Blocker instead of an implicit ExecutionContext such as
global, which could affect the performance of our application.
However, Blocker is gone in Cats Effect 3, since it ships with its own internal blocking
pool. Instead, CE3 provides the following functions.
When using blocking libraries such as Doobie, this is hidden from the user. For example,
this is how we would acquire a blocking JDBC connection.
val xa = Transactor.fromDriverManager[IO](
"org.postgresql.Driver", // driver classname
"jdbc:postgresql:world", // connect URL (driver-specific)
"postgres", // user
"" // password
)
The removal of Blocker and ContextShift are one of the few highlights of the new major
version. If you haven’t heard about the latter, you only need to know that’s now history.
However, these release notes11 give some explanation about why it was needed, in case
you are interested in the previous design.
11
https://github.com/typelevel/cats-effect/releases/tag/v3.0.0-M1
176
Chapter 7: Persistent layer
Transactions
Although we don’t need it in our application, it’s a common wanted feature when using
SQL databases. Skunk supports transactions12 , if you ever have the need.
A transaction is modeled as a Resource, not a surprise, huh?
// assume s: Session[F]
s.transaction.use { tx =>
// transactional action here
}
A transaction begins before the use block is executed and it is committed upon successful
termination. On the other hand, if it fails, the transaction will be rolled-back.
Compositionality
Now let’s say we wanted to make the creation of items, including the creation of brand
and category, an atomic transaction. We wouldn’t be able to combine the Brands,
Categories and Items services in the way they are written now, as they all run the create
function by first acquiring a session from the pool to then execute the command.
What we would need instead is to get all these SQL statements within a transactional
block, so this is something that needs to be designed differently.
The good news is that we can reuse most of it. First of all, the algebra.
trait TxItems[F[_]] {
def create(item: ItemCreation): F[ItemId]
}
Instead of taking the BrandId and CategoryId, it takes the names, as the IDs will be
created in the transactional block.
Here’s our transactional interpreter.
12
https://tpolecat.github.io/skunk/tutorial/Transactions.html
177
Chapter 7: Persistent layer
object TxItems {
import BrandSQL._, CategorySQL._, ItemSQL._
Easy, right? In order to get an atomic transaction, we only need to execute the SQL
statements within the scope of the transaction, denoted by the s.transaction.surround
region. Additionally, we managed to reuse the codecs and SQL statements for brands,
categories, and items!
This particular design also works when working with Doobie, without leaking implemen-
tation details such as ConnectionIO.
178
Chapter 7: Persistent layer
Summary
179
Chapter 8: Testing
Tests are as significant as types. While the latter prevent us from writing programs that
wouldn’t compile, they are not sufficient to leave all the incorrect programs out.
Both tests and types allow the programmer to communicate their expectations unam-
biguously. Types exist only at compile time whereas tests exist in the codebase. Yet,
neither are relevant at runtime, nor ensure that the programmer’s expectations are cor-
rect. They are not 100% bulletproof.
There are different kinds of tests. We will be focusing on unit tests and integration tests,
and see how both can be more than adequate for a business application such as the
shopping cart we are building. We will also learn how to law-check typeclass instances
as well as optics.
All of this wouldn’t be possible without property-based testing 1 , or commonly referred
to as PBTs, which we are going to use extensively.
1
http://www.scalatest.org/user_guide/property_based_testing
180
Chapter 8: Testing
In the Scala ecosystem, most of the popular test frameworks don’t operate well with
purely functional libraries such as Cats Effect. They are mostly side-effectful, forcing us
into an undesirable imperative road.
Fortunately, a light in the darkness has emerged over the past year. Lo and behold
Weaver Test2 , which natively supports functional effects.
Among other things, it provides the SimpleIOSuite trait, which can be seen as the IOApp
equivalent for tests. Its name implies that IO is supported but you should know that
pure tests are also an option. E.g.
import cats.effect.IO
import cats.syntax.all._
import weaver.SimpleIOSuite
pureTest("pure expectation") {
expect(1 =!= 2)
}
test("effectful expectation") {
for {
h <- IO.pure("hello")
n <- IO(scala.util.Random.nextInt(10))
} yield expect(h.nonEmpty) && expect(n < 10)
}
Notice how the test suite is defined as an object; this is a requirement. There is much
more to discover, and we will explore some of it but it is recommended for you to check
out the official documentation, regardless.
Notes
Test suites in Weaver must be objects!
We are going to be testing our application in IO (more on this soon). Moreover, we saw
SimpleIOSuite but there is also IOSuite for shared resources, and Checkers for property-
based testing via Scalacheck, defined by the weaver-scalacheck module. We will get to
see both of them soon.
2
https://github.com/disneystreaming/weaver-test
181
Chapter 8: Testing
MUnit3 is another Scala testing library that gained some traction over the past year
due to its simplicity. It is a great library for pure unit tests but it runs short when it
comes to functional effects. To mitigate this issue, we need to pull in munit-cats-effect4 ,
maintained by the Typelevel folks. However, the real problem shows up when we try to
use munit-scalacheck together with effectful tests, since the latter are not supported by
Scalacheck. To make this work, we need another extra library: scalacheck-effect5 .
You might see where this is going. By having three different libraries to support effectful
property-based tests, we need to accept the risk of potential binary incompatibilities
when upgrading.
Readers are encouraged to evaluate both options. Weaver was designed with functional
effects and parallelism in mind, so it is a great choice for this use case. MUnit might be
a better option for pure unit tests. In fact, Weaver maintainers recommend using both
test frameworks since they can live in harmony together.
However, to avoid the mental overhead of having to think about both, I think it’s best
to pick one and roll with it.
Why not? We can easily compose programs in IO as we can with other monads. If you
think about it, there is not much of a difference in using Id, Either, or IO to evaluate a
program with a single Monad[F] constraint. You can think of IO as another interpreter
of the typeclass constraints our functions may have; a concrete implementation.
Tests should be seen as main, another “end of the world”, where we choose an effect type.
I would argue that testing in IO is perfectly fine, and sometimes necessary (e.g. in the
presence of concurrency).
In conclusion, whether you test via Const, StateT, Either, or IO, you should think as the
concrete type as a mean to an end, not the other way around.
3
https://scalameta.org/munit/
4
https://github.com/typelevel/munit-cats-effect
5
https://github.com/typelevel/scalacheck-effect
182
Chapter 8: Testing
Generators
Scalacheck lets us generate random data, starting from a seed, for any datatype we
may have. In order to do so, we need to create a Gen[A]. For example, here we have a
generator for a simple case class.
Then we can make use of it in a property-based test, which usually have the following
shape.
Another way of getting Scalacheck to generate random data for us is via Arbitrary,
which is a typeclass that wraps a generator. In such cases, tests take a slightly different
shape.
This kind of tests require instances of org.scalacheck.Arbitrary for every value we intend
to generate. In this case, an Arbitrary[Person], which can be created in the following
way.
However, we will only use the former because I don’t think using Arbitrary is a great
idea for random data generation, as we often need different ways of creating instances
for a specific datatype. E.g. we may be interested in the following generators.
Using typeclass instances requires coherence, i.e. a single instance per type, so the only
principled way to get around this is by introducing a newtype per generator, which is
tedious and boilerplatey. Therefore, I believe the best thing we can do is to stick to use
Gen whenever we can (sometimes this is not an option, though, as we will see when we
get to law testing).
183
Chapter 8: Testing
Opinionated advice
Avoid using Arbitrary at all costs; stick to using Gen instead
Generators have multiple useful methods for generating constrained random data.
Using these functions, we can define generators for our custom data.
About forall
We have seen above that property-based tests using Scalacheck are usually expressed via
the overloaded forAll method, which can either take an explicit Gen[A] or an implicit
Arbitrary[A]. However, functional effects are not supported by this framework, as it has
been briefly mentioned before.
Notes
Scalacheck doesn’t support effectful property-based tests
Fortunately, Weaver got around this problem by providing a custom overloaded forall
– notice the lowercase a – method which operates in the same way, albeit having a
limitation of a single Gen[A] and six Arbitrary-generated values. This means that using
plain Scalacheck, this would work.
forAll(personGen, Gen.posNum[Int]) {
case (person, n) => ...
}
Though, the same will fail to compile when using Weaver. Thus, the way to go is to
create a single generator by composing multiple generators and use that instead. For
the example above, it will be as follows.
184
Chapter 8: Testing
...
}
Application data
Let’s start with defining some generic functions for IDs and stringy types.
This allows us to easily define generators for datatypes such as BrandId, PaymentId, and
BrandName.
When generating strings, we want to make sure they are non-empty. We could do this
in the following way.
However, this is considerably slow, and we need our tests to run fast. To solve this issue,
we can use the following trick (numbers 21-40 are picked arbitrarily).
All these values will be defined in a single file generators.scala, which can be imported
by every property-based test on demand.
185
Chapter 8: Testing
Next is CartTotal, which requires an Item generator, and this one requires both Brand
and Category generators. So let’s split it into two parts, defining its dependencies first.
186
Chapter 8: Testing
t <- moneyGen
} yield CartTotal(i, t)
It is noteworthy observing how we are creating random data and refining our types
without any validation whatsoever. This is what the Refined.unsafeApply method does,
and it is only right to use in tests when there is no alternative; in any other case, it
should not be used.
Unfortunately, we are not leveraging the refinement types we defined to create such
generators either. Ideally, this could be supported by refined-scalacheck. With it, we
would be able to summon generators for any refined type. E.g.
187
Chapter 8: Testing
import eu.timepit.refined.scalacheck.all._
import eu.timepit.refined.string.Ipv4
import eu.timepit.refined.types.string.NonEmptyString
import org.scalacheck.Arbitrary.arbitrary
Unfortunately, it does not work in our case since we have some custom refined types
that are unsupported. When trying to use this approach, we will more likely stumble
across the error below.
Finally, we need a few more generators for testing our HTTP routes.
import shop.http.auth.users._
We can now move onto the next section, where we get work on the application’s unit
tests.
188
Chapter 8: Testing
Business logic
Our main business logic resides in the Checkout program, so this is the critical piece of
software we need to test. Writing tests for all the possible scenarios is what we need to
figure out next.
Let’s recap on its definition.
In addition to a RetryPolicy, it takes three different algebras for which we need to provide
fake implementations to be able to test our program. We don’t want to be hitting a real
payments service, or persisting test orders in a database, for example. Although setting
up a test environment with a payment service and a database is not wrong, I would say
it is not strictly necessary, and we can instead get away with test interpreters. After all,
what we want to test are not these components but the interaction with the main piece
of logic.
We also need implementations for the implicit constraints. Most of the time, we can
use the default instance. However, in this case it will be tremendously useful to define
a few custom instances. For example, in most tests, we are not interested in seeing
what it is being logged or what it is being scheduled to run in the background. For this
particular reason, we are going to bring into the implicit scope a few no-op instances of
Background[F] and Logger[F], required by Checkout.
189
Chapter 8: Testing
We ignore whatever is being scheduled. In the case of Logger, we use the default
NoOpLogger defined by the log4cats-noop module. So, before all our tests in the
CheckoutSuite, we are going to have the following two lines of code.
Only in one test we are going to override the default Background instance, as we will be
learn soon.
Happy path
We will first define the interpreters to test the happy path. Let’s start with
PaymentClient[F].
A test client that just returns the same PaymentId it receives as an argument.
Next is ShoppingCart.
It does nothing on delete, and it returns the same CartTotal it is given on get. TestCart
is a dummy implementation that returns ??? on each method (we don’t need the other
methods for this test).
Next is Orders[F].
190
Chapter 8: Testing
val MaxRetries = 3
Now that we have defined all the test interpreters, we are ready to instantiate our
checkout program and write a test for the happy path.
test("successful checkout") {
forall(gen) {
case (uid, pid, oid, ct, card) =>
Checkout[IO](
successfulClient(pid),
successfulCart(ct),
successfulOrders(oid),
retryPolicy
).process(uid, card)
.map(expect.same(oid, _))
}
}
191
Chapter 8: Testing
import shop.generators._
Expectations
In many popular Scala test frameworks, we can run assertions anywhere in the test via
assert or a combinator alike. This is only possible because these are side-effects. Weaver
takes a completely different approach by defining expectations as plain values while the
execution is deferred to the test suite, in the same way IOApp works.
Expectations is a simple wrapper over a validated non-empty list. What makes it more
interesting is that it forms a multiplicative Monoid, meaning we can combine multiple
Expectations using the common |+| operator, and it will result in a successful test only
when all the expectations hold true.
There are also a few other combinators defined directly in the case class such as and, or
and xor. Yet another useful combinator we will be making use of is expect.all(e1, e2,
..., en), which takes a variable number of Booleans.
Now coming back to our happy-path test above, we saw expect.same, which has roughly
the following type signature.
192
Chapter 8: Testing
It makes use of typeclass-based equality (Cats’ Eq) instead of universal equality, and
it also requires a cats.Show[A] instance used to display values in the standard output.
These are two basic typeclasses we saw in previous chapters, that can, in most cases, be
automatically derived for our datatypes.
Tips
This means that the following expressions declared below are equivalent.
So how can we migrate a test that runs assertions in between expressions? Say we have
the following test.
import munit.FunSuite
test("side-effectful assertions") {
val p =
Ref.of[IO, Int](0).flatMap { ref =>
for {
_ <- ref.update(_ + 5)
_ <- ref.get.map(x => assert(x === 5))
_ <- ref.update(_ + 10)
_ <- ref.get.map(x => assert(x === 15))
_ <- ref.set(1)
_ <- ref.get.map(x => assert(x === 1))
} yield ()
}
p.unsafeToFuture()
}
Instead of running side-effectful assertions, we need to collect the values and write the
expectations at the end.
193
Chapter 8: Testing
import weaver.SimpleIOSuite
We could have done the same in our old test, though, with Weaver we can also write the
expectations as we go and combine them at the end.
import weaver.SimpleIOSuite
194
Chapter 8: Testing
This is certainly not possible when we are dealing with side-effects instead of values.
Easy, don’t you think?
We are now ready to continue working on the remaining test cases in CheckoutSuite.
Empty cart
This is one of the first lines of our process function, after we retrieve the cart.
If the cart is empty, we get an EmptyCartError, so let’s write a test for this.
All we need is a test interpreter for the ShoppingCart that returns an empty list of
items.
Next, we attempt to invoke the process function and evaluate its inner result.
test("empty cart") {
forall(gen) {
case (uid, pid, oid, _, card) =>
Checkout[IO](
successfulClient(pid),
emptyCart,
successfulOrders(oid),
retryPolicy
).process(uid, card)
.attempt
.map {
case Left(EmptyCartError) =>
success
case _ =>
failure("Cart was not empty as expected")
}
}
}
195
Chapter 8: Testing
We expect an EmptyCartError for the test to succeed; otherwise, the test is considered
failed.
If the remote payment client is unresponsive, our system needs to be resilient. Here is
where our retrying logic should be tested. First, we need to simulate an unreachable
payment client. Here is a possible interpreter.
Every time the process function is invoked, it raises an error. Let’s analyze our unit test
for this case.
Checkout[IO](
unreachableClient,
successfulCart(ct),
successfulOrders(oid),
retryPolicy
).process(uid, card)
.attempt
.flatMap {
case Left(PaymentError(_)) =>
retries.get.map {
case Some(g) =>
expect.same(g.totalRetries, MaxRetries)
case None =>
failure("expected GivingUp")
}
case _ =>
IO.pure(failure("Expected payment error"))
}
}
196
Chapter 8: Testing
}
}
Something new has come up in the fourth line: a Ref[IO, Option[GivingUp]] that is
subsequently used to create a test interpreter for our Retry effect. First of all, GivingUp
is one of the possible values of the retry.RetryDetails ADT, defined by the Cats Retry
library. Secondly, the givingUp constructor on TestRetry is implemented as follows.
object TestRetry {
def givingUp(
ref: Ref[IO, Option[GivingUp]]
): Retry[IO] = new Retry[IO] {
def retry[T](
policy: RetryPolicy[IO], retriable: Retriable
)(fa: IO[T]): IO[T] = {
@nowarn
def onError(e: Throwable, details: RetryDetails): IO[Unit] =
details match {
case g: GivingUp => ref.set(Some(g))
case _ => IO.unit
}
retryingOnAllErrors[T](policy, onError)(fa)
}
}
Whenever we get the GivingUp message – orchestrated by the retrying library – we set
the state in our mutable reference for further analysis. In any other case, it does not do
anything.
Then again, we invoke the process function followed by attempt and pattern-match on its
inner result. If we get a PaymentError, we also expect the GivingUp message, indicating
there were as many retries as MaxRetries. In any other case, we get a failed test.
If you have read the first edition, you might recall this logic was tested using a cus-
tom Logger interpreter that accumulated String messages in a Ref[IO, List[String]].
Although it wasn’t a bad approach, it was far from ideal. Dealing with stringy types
is error-prone and writing test expectations in terms of log messages feels wrong. The
Retry effect gives us the ability to do things right by writing a test interpreter that
accumulates concrete datatype values instead.
197
Chapter 8: Testing
The previous client fails every time the process method is invoked. So, how can we
simulate a client that recovers after a certain amount of retries? We need some internal
state. Let’s analyze the following recovering client implementation.
def recoveringClient(
attemptsSoFar: Ref[IO, Int],
paymentId: PaymentId
): PaymentClient[IO] =
new PaymentClient[IO] {
def process(payment: Payment): IO[PaymentId] =
attemptsSoFar.get.flatMap {
case n if n === 1 =>
IO.pure(paymentId)
case _ =>
attemptsSoFar.update(_ + 1) *>
IO.raiseError(PaymentError(""))
}
}
The Ref[IO, Int] keeps the count of the number of retries. If it equals to one, we emit
the given PaymentId; otherwise, we increment the counter and raise a PaymentError that
will hit our retrying mechanism. This is what we need to test.
Checkout[IO](
recoveringClient(cliRef, pid),
successfulCart(ct),
successfulOrders(oid),
retryPolicy
).process(uid, card)
.attempt
.flatMap {
case Right(id) =>
198
Chapter 8: Testing
retries.get.map {
case Some(w) =>
expect.same(id, oid) |+|
expect.same(0, w.retriesSoFar)
case None =>
failure("Expected one retry")
}
case Left(_) =>
IO.pure(failure("Expected Payment Id"))
}
}
}
}
object TestRetry {
retryingOnAllErrors[T](policy, onError)(fa)
}
}
def givingUp(
ref: Ref[IO, Option[GivingUp]]
): Retry[IO] =
handlerFor[GivingUp](ref)
199
Chapter 8: Testing
def recovering(
ref: Ref[IO, Option[WillDelayAndRetry]]
): Retry[IO] =
handlerFor[WillDelayAndRetry](ref)
Back to our test, we expect an OrderId out of it, indicating a successful operation.
Furthermore, we expect a WillDelayAndRetry with zero retries so far, indicating a single
retry has occurred before recovering. In any other case, the test should fail.
Failing orders
If the order fails to be created, we retry a configured number of times, specified in our
retry policy. When the maximum number of retries is reached, we return the OrderId
and schedule this action to run again in the background. In order to test this complex
case, we need a new interpreter for our Background interface.
def counter(
ref: Ref[IO, (Int, FiniteDuration)]
): Background[IO] =
new Background[IO] {
def schedule[A](fa: IO[A], duration: FiniteDuration): IO[Unit] =
ref.update { case (n, f) => (n + 1, f + duration) }
}
We are going to have a counter that checks how many actions have been submitted to
be scheduled to run in the background as well as the total amount of time that has been
allocated for such actions. This is as much as we can do since we cannot possibly know
what an IO[A] does when executed.
In addition, we need a failing interpreter for Orders.
200
Chapter 8: Testing
With all these components in place, let’s examine our test implementation, where we also
write expectations for the retrying logic. This is probably one of the most interesting
tests!
Checkout[IO](
successfulClient(pid),
successfulCart(ct),
failingOrders,
retryPolicy
).process(uid, card)
.attempt
.flatMap {
case Left(OrderError(_)) =>
(acc.get, retries.get).mapN {
case (c, Some(g)) =>
expect.same(c, 1 -> 1.hour) |+|
expect.same(g.totalRetries, MaxRetries)
case _ =>
failure(s"Expected $MaxRetries retries and reschedule")
}
case _ =>
IO.pure(failure("Expected order error"))
}
}
}
}
We await an OrderError, which we get after the maximum number of retries is reached. If
this condition is met, we expect the background counter to contain one action scheduled
and one hour of time allocated, and we also expect the number of retries to equal the
configured MaxRetries; otherwise, the test should fail.
Notice where the custom implicit instances for Background and Retry are placed in the
local scope, effectively taking priority. This is key.
201
Chapter 8: Testing
The last and less critical action of our checkout process is deleting the shopping cart
from the cache. We have mentioned that if this fails for any reason, we don’t care too
much since the entry will expire anyway, and continuing to operate the site has a much
higher priority. For such case, we need a ShoppingCart instance that fails.
All we need to check is that the process function returns an expected OrderId without
failing.
We can now say we are covered from the scenarios we could think of. In addition
to our custom cases, we are also testing different inputs to our program thanks to
property-based testing, which is sometimes underrated. Thus, we can conclude with one
of the most interesting testing piece in our application to continue testing the HTTP
components.
202
Chapter 8: Testing
HTTP
In Chapter 5, we learned about Http4s’ routes and clients, among other features. Now
we need to look at how to test these components, and I must confess, these are the kind
of tests I enjoy writing due to the versatility of this framework; it is really well thought
out.
Routes
I have claimed that a server is a function, and I literally meant it! We don’t need to
spin up a server to test our HttpRoutes since these are plain functions. Moreover, we can
seize the power of property-based testing to write accurate tests.
Here is a test for BrandRoutes, which exposes a single GET endpoint to retrieve all the
brands.
import org.http4s.client.dsl.io._
import org.http4s.implicits._
203
Chapter 8: Testing
Since this pattern will become repetitive (running routes and writing expectations on
the response), we can extract it out into another function we can reuse.
Furthermore, we can define an HttpTestSuite where this and other functions can be
placed.
We haven’t talked about it yet but dataBrands, used to build BrandRoutes, is defined as
follows.
204
Chapter 8: Testing
IO.pure(brands)
}
Upon invoking findAll, it returns the given input on construction, usually obtained via
generators. I like to refer to them as by-pass interpreters. Keep your eyes pealed as the
exact same approach will be used in the other HTTP routes.
Next is ItemRoutes, which can additionally receive a query parameter. So let’s examine
this particular test and skip the rest to avoid repetition.
forall(gen) {
case (it, b) =>
val req = GET(
uri"/items".withQueryParam("brand", b.name.value)
)
val routes = new ItemRoutes[IO](dataItems(it)).routes
val expected = it.find(_.brand.name === b.name).toList
expectHttpBodyAndStatus(routes, req)(expected, Status.Ok)
}
}
We construct our Uri using both the uri and the withQueryParam methods. Additionally,
we have dataItems, a by-pass interpreter for Items defined as follows.
Finally, let’s see how to test authenticated routes. We will only focus on CartRoutes and
skip the rest, as they are almost identical.
We first need a fake AuthMiddleware that bypasses security (it always returns a User).
205
Chapter 8: Testing
def authMiddleware(
authUser: CommonUser
): AuthMiddleware[IO, CommonUser] =
AuthMiddleware(Kleisli.pure(authUser))
Afterward, we can create our HttpRoutes and define our unit test.
forall(gen) {
case (user, ct) =>
val req = GET(uri"/cart")
val routes =
CartRoutes[IO](dataCart(ct))
.routes(authMiddleware(user))
expectHttpBodyAndStatus(routes, req)(ct, Status.Ok)
}
}
The only difference is that we need to supply an AuthMiddleware to obtain our HttpRoutes;
the rest should be reasonably straightforward at this point.
Next, we can see how to test a POST endpoint.
import org.http4s.circe.CirceEntityEncoder._
forall(gen) {
case (user, c) =>
val req = POST(c, uri("/cart")
val routes =
CartRoutes[IO](new TestShoppingCart)
.routes(authMiddleware(user))
expectHttpStatus(routes, req)(Status.Created)
}
}
206
Chapter 8: Testing
The POST.apply method takes in a body (a Cart in this case) and a Uri. There should be
an EntityEncoder[IO, Cart] in scope, otherwise, it would not compile. There is also a
new generic method expectHttpStatus, which is similar to expectHttpBodyAndStatus but
it only checks the status of the response. Lastly, we use a TestShoppingCart interpreter,
which only implements the relevant get method, as shown below.
Other HTTP methods such as GET, PUT, and DELETE also support taking a request body
A as an argument, given an EntityEncoder[F, A].
Clients
Since it is yet another algebra, we could successfully test our Checkout program with a
few different interpreters. However, you might be surprised that we can also test the
real interpreter that uses org.http4s.Client without hitting the network! How?
object Client {
def fromHttpApp[F[_]: Async](app: HttpApp[F]): Client[F]
}
This small yet so powerful function lets us create a Client[F] from an HttpApp[F], as its
name and type signature promise.
So let’s start by creating the HTTP routes that represent the remote payments API.
Then, we need to think about the HTTP responses we need to test. For such purpose,
let’s look once again at the relevant parts of the PaymentClient interpreter.
207
Chapter 8: Testing
There are two specific status codes that yield a successful response, including a PaymentId,
and one catch-all that raises a PaymentError. So the clients for the first two cases, can
be defined as follows.
The last one doesn’t require a PaymentId, so we can return 500 (Internal Server Error).
That’s all! Having a Client, we can proceed with writing the tests. The first one tests
we get a PaymentId given an HTTP Status Code 200.
test("Response Ok (200)") {
forall(gen) {
case (pid, payment) =>
val client = Client.fromHttpApp(routes(Ok(pid)))
PaymentClient
.make[IO](config, client)
.process(payment)
.map(expect.same(pid, _))
}
}
The second one is the 409 (Conflict) response but it’s almost the same as the one above
so we will skip it. Finally, we need to test the one that yields a PaymentError instead of
a PaymentId.
208
Chapter 8: Testing
PaymentClient
.make[IO](config, client)
.process(payment)
.attempt
.map {
case Left(e) =>
expect.same(PaymentError("Internal Server Error"), e)
case Right(_) =>
failure("expected payment error")
}
}
}
209
Chapter 8: Testing
Law testing
The interpreter gets all the items for the user from Redis and it calculates the total
amount.
It was previously mentioned that foldMap requires a Monoid instance, in this case for
subTotal, which is of type Money. Let’s have a look at its type signature, defined in the
Foldable typeclass.
Or more specifically.
We defined our Monoid[Money] instance in the OrphanInstances trait, based on the con-
crete USD type. Whenever we write a typeclass instance, we must test it abides by
its laws; it is quite easy to write unlawful instances, and in such cases, we end up with
unsound implementations and lose any guarantee made by the typeclass.
Fortunately, the Cats library features a module named cats-laws. It is based on Disci-
pline6 , a tiny library for law-checking. Since we use Weaver as our test framework, we
can leverage weaver-discipline in our suite.
Typeclass laws
checkAll("Monoid[Money]", MonoidTests[Money].monoid)
6
https://github.com/typelevel/discipline
210
Chapter 8: Testing
This is the case where we must have an Arbitrary instance of the type we are testing,
as that’s how Cats Laws is designed. However, if we really want to avoid that, we can
always be explicit about it.
checkAll(
"Monoid[Money]",
MonoidTests[Money].monoid(Arbitrary(moneyGen), Eq[Money])
)
We will stick to the usual implicit Arbitrary instance but it is handy to know we can be
explicit if the need arises.
MonoidTests ensures we test all the typeclass’ laws for our instance. The laws are encoded
using the “equality arrow” or <=> defined by Cats Laws. E.g. here’s MonoidLaws, which
in addition, gets all the SemigroupLaws.
[info] shop.domain.OrphanSuite
[info] + Monoid[Money]: monoid.associative 44ms
[info] + Monoid[Money]: monoid.collect0 6ms
[info] + Monoid[Money]: monoid.combine all 34ms
[info] + Monoid[Money]: monoid.combineAllOption 15ms
[info] + Monoid[Money]: monoid.intercalateCombineAllOption 16ms
211
Chapter 8: Testing
Optics laws
The Status datatype used in the HealthCheck service and the IsUUID typeclass define
isomorphisms, though, are these lawful instances? Let’s verify it!
In the same way we checked the Monoid laws, we can check the Iso laws via the
monocle-laws module.
checkAll("Iso[Status._Bool]", IsoTests(Status._Bool))
// bonus checks
checkAll("IsUUID[UUID]", IsoTests(IsUUID[UUID]._UUID))
checkAll("IsUUID[BrandId]", IsoTests(IsUUID[BrandId]._UUID))
212
Chapter 8: Testing
It also integrates with Discipline. Once again, we need Arbitrary instances for the types
we are law-checking, and particularly for IsoTests, we need a Cogen instance as well so
we can satisfy the Arbitrary[A => A] constraint.
[info] shop.domain.OpticsSuite
[info] + Iso[Status._Bool]: Iso.compose modify 31ms
[info] + Iso[Status._Bool]: Iso.consistent get with modifyId 13ms
[info] + Iso[Status._Bool]: Iso.consistent modify with modifyId 10ms
[info] + Iso[Status._Bool]: Iso.consistent replace with modify 8ms
[info] + Iso[Status._Bool]: Iso.modify id = id 4ms
[info] + Iso[Status._Bool]: Iso.round trip one way 4ms
[info] + Iso[Status._Bool]: Iso.round trip other way 4ms
[info] + IsUUID[UUID]: Iso.compose modify 14ms
[info] + IsUUID[UUID]: Iso.consistent get with modifyId 5ms
[info] + IsUUID[UUID]: Iso.consistent modify with modifyId 7ms
[info] + IsUUID[UUID]: Iso.consistent replace with modify 7ms
[info] + IsUUID[UUID]: Iso.modify id = id 4ms
[info] + IsUUID[UUID]: Iso.round trip one way 4ms
[info] + IsUUID[UUID]: Iso.round trip other way 3ms
[info] + IsUUID[BrandId]: Iso.compose modify 8ms
[info] + IsUUID[BrandId]: Iso.consistent get with modifyId 3ms
[info] + IsUUID[BrandId]: Iso.consistent modify with modifyId 6ms
[info] + IsUUID[BrandId]: Iso.consistent replace with modify 4ms
[info] + IsUUID[BrandId]: Iso.modify id = id 2ms
[info] + IsUUID[BrandId]: Iso.round trip one way 2ms
[info] + IsUUID[BrandId]: Iso.round trip other way 3ms
[info] Passed: Total 28, Failed 0, Errors 0, Passed 28
This concludes this section. Do not forget to law-test your instances at home!
213
Chapter 8: Testing
Integration tests
In this last section, we will see why integration tests are also essential. However, let’s
first be clear: What do integration tests mean, exactly?
We can interpret them in many ways. The usual meaning refers to starting up our
entire application to be tested against all the external components, which in our case
are PostgreSQL, Redis, and the remote Payment client.
However, this is tedious, and the benefits don’t always justify the cost of having such an
exclusive testing or staging environment.
A good approach is to test external interpreters in isolation. For example, we could test
the Postgres interpreters in a single test suite, and the Redis interpreters in another test
suite. If we have a real test payment client, we could also test that. In this case, we
don’t, so we are going to move forward only with the first two.
Shared resources
type Res
def sharedResource : Resource[IO, Res]
For example, if our shared resource is an HTTP Client, it might look as follows.
214
Chapter 8: Testing
It is a great advantage to be able to operate in terms of the Resource since we can use
all the functional abstractions available on it. For instance, we can forget all about the
shenanigans needed for a beforeAll and afterAll in other frameworks, which usually
involve a lot of mutability.
Instead, we can use evalTap to perform an action with the acquired resource as soon as
it becomes available – effectively replacing any beforeAll. E.g., we may want to ensure
our remote server is up and running when the application is starting up.
Moreover, we can use onFinalize to run any arbitrary action once the resource is released,
meaning all the tests are done running. This can be seen as the replacement for afterAll
in other test frameworks, whenever the resource is not needed. However, if we really
need to perform an action using the resource, we need something else.
For this purpose, we are going to create a ResourceSuite that offers extension methods
called beforeAll and afterAll, which can be reused everywhere we need it.
215
Chapter 8: Testing
res.evalTap(f)
We also place the Scalacheck configuration here since it will be our default for integration
tests. The beforeAll method is an alias for evalTap, it’s here just for consistency. Yet,
different is the situation for afterAll: we need to flatTap on the resource, create a
second Resource[IO, Unit], and run the afterAll action on its finalizer.
I believe having these extension methods creates a good user experience.
Once again, operating in terms of Resource and functional constructs makes this really
pleasant to work with.
Now if you’re wondering how afterEach, beforeEach and beforeAndAfterEach would look
like in a functional suite, here’s one way, which can also be added to our ResourceSuite.
def testBeforeAfterEach(
after: Res => IO[Unit],
before: Res => IO[Unit]
216
Chapter 8: Testing
def testAfterEach(
after: Res => IO[Unit]
): String => (Res => IO[Expectations]) => Unit =
testBeforeAfterEach(_ => IO.unit, after)
def testBeforeEach(
before: Res => IO[Unit]
): String => (Res => IO[Expectations]) => Unit =
testBeforeAfterEach(before, _ => IO.unit)
You can then define a custom method to create a test that always runs a custom action
before or after each (or both). E.g.
If you are not that lucky and can’t migrate to Weaver yet, have a look at
Resource#allocated, which returns a tuple of the acquired resource and the release
handle. Beware, though: with great power comes great responsibility.
Postgres
We are now ready to start working on our first integration test: PostgresSuite. First of
all, we define the shared resource.
217
Chapter 8: Testing
password = Some("my-password"),
database = "store",
max = 10
)
Skunk defines a pool of connections as a “resource of resource”, hence our Res type is a
Resource[IO, Session[IO]]. If you don’t remember, make sure to come back to Chapter
7, where this is explained.
Once we define how to acquire a connection, it would be great if we can ensure our tables
are empty before the execution of the tests. This is easily done either via beforeAll
(defined in our suite) or just go for evalTap. We will pick the former to get used to it.
Be aware that our test suite doesn’t spin up a Postgres server. It is our responsibil-
ity to start it up before running the integration tests. My recommendation is to use
docker-compose, which can be configured to run in our CI build too, as we will see in
Chapter 10.
We can now proceed with the definition of the actual tests, starting out with Brands.
218
Chapter 8: Testing
z <- b.create(brand.name).attempt
} yield expect.all(
x.isEmpty,
y.count(_.name === brand.name) === 1,
z.isLeft
)
}
}
We are invoking findAll and create a couple of times to later evaluate the results. We
can highlight that the first result should be empty (since there is no data), whereas the
last create action fails because the same Brand already exists.
The test for Categories is almost identical, so we will skip it. Next is Items.
val b = Brands.make[IO](postgres)
val c = Categories.make[IO](postgres)
val i = Items.make[IO](postgres)
for {
x <- i.findAll
_ <- b.create(item.brand.name)
d <- b.findAll.map(_.headOption.map(_.uuid))
_ <- c.create(item.category.name)
e <- c.findAll.map(_.headOption.map(_.uuid))
_ <- i.create(newItem(d, e))
y <- i.findAll
} yield expect.all(
x.isEmpty,
219
Chapter 8: Testing
Since we are hitting a Postgres database with key constraints, we can no longer create
an Item with an inexistent BrandId or CategoryId. Therefore, we need to make sure these
are created before creating an Item. Ultimately, we expect the first result to be empty
and that the second result contains the Item that was persisted.
Next up is Users.
forall(gen) {
case (username, password) =>
val u = Users.make[IO](postgres)
for {
d <- u.create(username, password)
x <- u.find(username)
z <- u.create(username, password).attempt
} yield expect.all(
x.count(_.id === d) === 1,
z.isLeft
)
}
}
We create a new user given the properties UserName and EncryptedPassword, retrieve the
user, and then expect the UserId to match the one we got on creation. Lastly, we try to
create the same user once again, which should raise an error.
220
Chapter 8: Testing
forall(gen) {
case (oid, pid, un, pw, items, price) =>
val o = Orders.make[IO](postgres)
val u = Users.make[IO](postgres)
for {
d <- u.create(un, pw)
x <- o.findBy(d)
y <- o.get(d, oid)
i <- o.create(d, pid, items, price)
} yield expect.all(
x.isEmpty,
y.isEmpty,
i.value.version === 4
)
}
}
Again, to create an Order, we need an existent User given the user_id constraint in our
orders table.
Redis
We have only two Redis interpreters: Auth and ShoppingCart, both taking some algebras
whose interpreters use Postgres.
Let’s start defining our test suite and defining how to acquire a Redis connection.
221
Chapter 8: Testing
Additionally, we create a no-op instance of Logger[IO] and ensure all the keys are flushed
upon acquiring a connection.
object ShoppingCart {
def make[F[_]: GenUUID: MonadThrow](
items: Items[F],
redis: RedisCommands[F, String, String],
exp: ShoppingCartExpiration
): ShoppingCart[F] = ???
}
However, the Items interpreter needs Postgres, so here we have two options: either we
use the real interpreter, or we use an in-memory interpreter. We will choose the latter
to avoid mixing Postgres tests with Redis tests.
Let’s have a look at the following in-memory implementation.
222
Chapter 8: Testing
ref.get.map {
_.values.filter(_.brand.name === brand).toList
}
We use a mutable reference as our concurrent in-memory storage. The relevant methods
are create and update. The first one creates an Item of random Brand and Category (this
is irrelevant); the second method updates the price of a given Item, if it exists.
The following values are shared across tests, so we will define them at the top of our test
suite.
223
Chapter 8: Testing
With all the pieces in place, let’s proceed to write our ShoppingCart test.
forall(gen) {
case (uid, it1, it2, q1, q2) =>
Ref.of[IO, Map[ItemId, Item]](
Map(it1.uuid -> it1, it2.uuid -> it2)
).flatMap { ref =>
val items = new TestItems(ref)
val c = ShoppingCart.make[IO](items, redis, Exp)
for {
x <- c.get(uid)
_ <- c.add(uid, it1.uuid, q1)
_ <- c.add(uid, it2.uuid, q1)
y <- c.get(uid)
_ <- c.removeItem(uid, it1.uuid)
z <- c.get(uid)
_ <- c.update(uid, Cart(Map(it2.uuid -> q2)))
w <- c.get(uid)
_ <- c.delete(uid)
v <- c.get(uid)
} yield expect.all(
x.items.isEmpty,
y.items.size === 2,
z.items.size === 1,
v.items.isEmpty,
w.items.headOption.fold(false)(_.quantity === q2)
)
}
}
}
Quite a few things going on here. We start by creating a Ref with the two Items we got
224
Chapter 8: Testing
from our generators, which is used by our TestItems interpreter. Finally, we proceed
to instantiate our ShoppingCart interpreter and write the appropriate expectations with
regards to a specific set of operations in our algebra.
Auth interpreter
object Auth {
def make[F[_]: MonadThrow](
tokenExpiration: TokenExpiration,
tokens: Tokens[F],
users: Users[F],
redis: RedisCommands[F, String, String],
crypto: Crypto
): Auth[F] = ???
}
def create(
username: UserName,
password: EncryptedPassword
): IO[UserId] =
ID.make[IO, UserId]
}
225
Chapter 8: Testing
The only internal state it has is a single UserName we pass as an argument. When the
find method is invoked with this value, we get a User back; otherwise, we get none. The
create method always creates a new UserId.
object Tokens {
def make[F[_]: GenUUID: Monad](
jwtExpire: JwtExpire[F],
config: JwtAccessTokenKeyConfig,
exp: TokenExpiration
): Tokens[F] = ???
In this case, we can use the same instance we use for the application. We only need to
provide two arguments (defined at the top of the suite).
JwtExpire.make[IO].map {
Tokens.make[IO](_, tokenConfig, tokenExp)
} // IO[Tokens[IO]]
Crypto.make[IO](PasswordSalt("test"))
Auth test
forall(gen) {
case (un1, un2, pw) =>
for {
t <- JwtExpire.make[IO].map {
Tokens.make[IO](_, tokenConfig, tokenExp)
}
c <- Crypto.make[IO](PasswordSalt("test"))
a = Auth.make(tokenExp, t, new TestUsers(un2), redis, c)
u = UsersAuth.common[IO](redis)
x <- u.findUser(JwtToken("invalid"))(jwtClaim)
226
Chapter 8: Testing
We get two different UserNames: un1 and un2. Our in-memory Users interpreter takes in
un2. Next, we try to retrieve the user using an invalid JWT token, which should return
nothing.
Moving forward, we create a new user using un1 and the given password. We check that
the JWT token we get back is valid in the following step. Next, we try to log in using
un2 and un1’s password, expecting an InvalidPassword error. Then, we expect to find
the un1 user, which has been assigned the j token. We also ensure the token has not
expired and is present in Redis. Finally, we invoke logout and then verified that the
token has been removed from Redis.
227
Chapter 8: Testing
Summary
We have learned that unit tests and law-checking are essential to any application.
Integration tests are also necessary to find bugs that would otherwise be hard to catch.
For example, Postgres or Redis specific issues, or bugs related to database codecs. I can
attest to have solved two bugs in our shopping cart application related to codecs that I
wouldn’t have spotted otherwise!
Summing up, I believe testing our interpreters is enough in our case, and we don’t need
to write integration tests for the entire application. Even though, this is good to have if
you can allow yourself the time and environment to make it happen.
In bigger applications, testing the interaction of the different components must be re-
quired to guarantee correctness. It should be assessed on a case-by-case basis.
In the next chapter, we will connect the dots to build the application.
228
Chapter 9: Assembly
We have come a long way. Having turned business requirements into technical specifi-
cations, we then designed our system using purely functional programming and finally
wrote property-based tests.
Now is the time to put all the pieces together, and here is where I would like to quote
one of my favorite song-writers that says:
In this chapter, we are going to assemble our application, and we will ultimately spin
up our HTTP server, connect to our database, and start serving requests.
229
Chapter 9: Assembly
Logging
Logging actions are seen as those invasive constructs that pollute our codebase. Yet,
these are a necessary evil if we intend to troubleshoot issues in our application.
Everyone is free to make a choice. A good practice is to only log critical information,
such as our retrying functions being invoked, but leaving everything else out.
On the one hand, because of the extra lines of dummy code. On the other hand, because
it creates unnecessary logs that need to be stored somewhere.
In our application, we are going to use Log4cats1 , which is by now the standard logging
library of the Cats ecosystem.
Whenever we need logging capability, all we need is to add a Logger constraint to our
effect type. E.g.
It is not the first time we see Logger. In Chapter 4, our retrying functions were making
use of it, even though we didn’t dive into details since it is easy to deduct its usage.
Normally, people use the Slf4j implementation, which is created as shown below.
In fact, this is how it is created in our Main class, as we will see soon.
In Chapter 7, we have seen how to turn off logging by using the NoOpLogger interpreter.
What we didn’t see is that Log4cats also comes with its own TestingLogger that might
be suitable for your needs. Make sure to check it out before you roll your own.
1
https://github.com/ChristopherDavenport/log4cats
230
Chapter 9: Assembly
There are other promising Scala logging libraries out there you might want to consider:
LogStage2 , Odin3 , and Tofu4 .
All these libraries provide structural and contextual logging, among other features.
2
https://izumi.7mind.io/latest/release/doc/logstage/index.html
3
https://github.com/valskalla/odin
4
https://docs.tofu.tf/docs/logging
231
Chapter 9: Assembly
Tracing
Alternatively, there is distributed tracing, which is a hot topic these days and might
eventually replace logging as we know it. Quoting the Open Tracing5 definition.
Ecosystem
In the Typelevel ecosystem, there are two contenders: Natchez6 and Trace4Cats7 .
The former is the bare bones support for tracing, including support for Datadog8 , Jaeger9 ,
Honeycomb10 , OpenCensus11 and LightStep12 , and is used by Skunk. The latter sup-
ports NewRelic13 , as well as some libraries like Http4s, STTP14 , and Tapir15 , and inter-
operates with Natchez. Though, there is also Natchez-Http4s16 , a recent addition to the
ecosystem.
In a few words, Natchez’s main typeclass is called Trace, and can be used as follows.
One caveat is that there are only Trace instances for Kleisli, as the concrete effect type
needs to be able to carry context. Stay tuned, though, as there are discussions (at the
moment of writing) about introducing a typeclass named FiberLocal to CE3, which will
be context-aware, and Natchez will more likely add support for it.
5
https://opentracing.io/docs/overview/what-is-tracing/
6
https://github.com/tpolecat/natchez
7
https://github.com/janstenpickle/trace4cats
8
https://www.datadoghq.com/
9
https://www.jaegertracing.io/
10
https://www.honeycomb.io/
11
https://opencensus.io/
12
https://lightstep.com/
13
https://newrelic.com/
14
https://sttp.softwaremill.com
15
https://tapir.softwaremill.com
16
https://github.com/tpolecat/natchez-http4s
232
Chapter 9: Assembly
Distributed tracing is great in theory but it involves quite a lot of boilerplate in practice.
If I had to give a recommendation, I would say consider it only for distributed applica-
tions where 100% visibility is critical. Otherwise, logging might be sufficient for your
use case.
If you’re looking for a different approach that doesn’t involve spinning up an open-
tracing server like Jaeger, you can look into Tofu’s Mid17 and Http4s Tracer18 . In the
last chapter, we will learn more about the former.
17
https://docs.tofu.tf/docs/mid
18
https://github.com/profunktor/http4s-tracer
233
Chapter 9: Assembly
Configuration
Application
Let’s get right into the configuration of our application to later analyze each part.
First of all, we define a default method taking some parameters that are different based
on the environment (test or production).
19
https://www.usenix.org/system/files/conference/osdi16/osdi16-xu.pdf
20
http://cir.is
21
https://typelevel.org/blog/2017/06/21/ciris.html
22
https://github.com/vlovgr
234
Chapter 9: Assembly
235
Chapter 9: Assembly
Both our URIs are defined as NonEmptyString. We could be more strict and define a
better refinement type but this will do for now.
Next, we see the use of the env function, which unsurprisingly, reads an environment
variable. The as function will attempt to decode the String value into the specified type,
for which we need a ConfigDecoder[String, A] instance.
We decode most of the environment variable values into custom newtypes, even though
we could decode the underlying type and then construct the newtype, as shown below.
env("SC_JWT_SECRET_KEY")
.as[NonEmptyString]
.secret
.map(JwtSecretKeyConfig(_))
Where’s the fun in doing that, though? Let’s derive ConfigDecoder instances for our
newtypes instead!
@derive(configDecoder, show)
@newtype
case class JwtSecretKeyConfig(secret: NonEmptyString)
As we have learned in Chapter 7, we can create custom typeclass derivation for newtypes,
and this is exactly what we have here.
object Decoder {
type Id[A] = ConfigDecoder[String, A]
}
env("SC_JWT_SECRET_KEY").as[JwtSecretKeyConfig].secret
Lastly, the secret function will encode this value as “secret” using the Secret datatype,
explained at the end of this section.
Another cool feature of this library is that we can compose expressions, as it uses the
ConfigValue monad. Here is an example taken from the official documentation.
env("API_PORT").or(prop("api.port")).as[UserPortNumber].option
236
Chapter 9: Assembly
The rest is just building values using case classes and newtypes.
Evaluation
The interesting part comes next, where we discern between our two different environ-
ments and load our configuration.
Again, it reads an environment variable that tells the application which environment
it is on, it attempts to decode it as our AppEnvironment datatype (ADT), it flatMaps
on the result, and it invokes our default method with the corresponding arguments.
Ultimately, it invokes the load[F] method to load the configuration into memory, which
requires Async[F].
Here is our ADT definition.
object AppEnvironment
extends Enum[AppEnvironment]
with CirisEnum[AppEnvironment] {
237
Chapter 9: Assembly
It uses the Enumeratum23 library together with the Ciris Enumeratum module.
Our configuration domain model is mostly defined as either case classes or newtypes, and
sometimes combined with refinement types. Here is the definition of some of them.
@derive(configDecoder, show)
@newtype
case class PasswordSalt(secret: NonEmptyString)
The Secret datatype, provided by Ciris, allows us to protect sensitive data from being
undesirably logged or stored. When we run the application and log the configuration in
use, we will see something along these lines.
For conciseness, we will skip the rest since they are very similar. Check out the source
code for more!
23
https://github.com/lloydmeta/enumeratum
238
Chapter 9: Assembly
Modules
In Chapter 2, we have explored the tagless final encoding and seen how modules help us
organizing our codebase. Here we are going to make use of this design pattern for our
application. Let’s start by enumerating the modules we will have.
• Services: it groups all our services, including our shared Redis and PostgreSQL
connections.
• HttpApi: it defines all our HTTP routes and middlewares.
• HttpClients: it defines our only HTTP client: the payments client.
• Programs: it defines our checkout program and retry policy.
• Security: it defines our instance of Users and all the authentication related func-
tionality.
Services
Here is the definition of our Services module, including its smart constructor.
object Services {
def make[F[_]: GenUUID: Temporal](
redis: RedisCommands[F, String, String],
postgres: Resource[F, Session[F]],
cartExpiration: ShoppingCartExpiration
): Services[F] = {
val _items = Items.make[F](postgres)
new Services[F](
cart = ShoppingCart.make[F](_items, redis, cartExpiration),
brands = Brands.make[F](postgres),
categories = Categories.make[F](postgres),
items = _items,
orders = Orders.make[F](postgres),
healthCheck = HealthCheck.make[F](postgres, redis)
) {}
}
}
239
Chapter 9: Assembly
The Services class is the interface we will be using in other modules. We make it sealed
and abstract because no other component from the outside should be able to extend it
or modify it. Also, its smart constructor initializes all required services. A simple wiring
of components.
HTTP Clients
Next is our implementation of the HttpClients module, which only contains our
PaymentClient.
import org.http4s.client.Client
object HttpClients {
def make[F[_]: JsonDecoder: MonadCancelThrow](
cfg: PaymentConfig,
client: Client[F]
): HttpClients[F] =
new HttpClients[F] {
def payment: PaymentClient[F] =
PaymentClient.make[F](cfg, client)
}
}
Programs
Next is Programs, which makes use of both the Services and HttpClients modules.
object Programs {
def make[F[_]: Background: Logger: Temporal](
checkoutConfig: CheckoutConfig,
services: Services[F],
clients: HttpClients[F]
): Programs[F] =
new Programs[F](checkoutConfig, services, clients) {}
240
Chapter 9: Assembly
Security
object Security {
def make[F[_]: Sync](
cfg: AppConfig,
postgres: Resource[F, Session[F]],
redis: RedisCommands[F, String, String]
): F[Security[F]] = {
241
Chapter 9: Assembly
)
)
for {
adminClaim <- jwtDecode[F](adminToken, adminJwtAuth.value)
content <- ApplicativeThrow[F].fromEither(
jsonDecode[ClaimContent](adminClaim.content)
)
adminUser = AdminUser(
User(UserId(content.uuid), UserName("admin"))
)
tokens <- JwtExpire.make[F].map(
Tokens.make[F](_, cfg.tokenConfig.value, cfg.tokenExpiration)
)
crypto <- Crypto.make[F](cfg.passwordSalt.value)
users = Users.make[F](postgres)
auth = Auth.make[F](
cfg.tokenExpiration, tokens, users, redis, crypto
)
adminAuth = UsersAuth.admin[F](adminToken, adminUser)
usersAuth = UsersAuth.common[F](redis)
} yield new Security[F](
auth, adminAuth, usersAuth, adminJwtAuth, userJwtAuth
) {}
}
}
242
Chapter 9: Assembly
It takes a Postgres and a Redis connection as arguments. Then, it creates the JWT
authentication schema for both AdminUser and CommonUser. Lastly, it creates all the
necessary components related to authentication.
Cryptowas introduced in Chapter 7. Its sole responsibility is to encrypt and decrypt
passwords.
trait Crypto {
def encrypt(value: Password): EncryptedPassword
def decrypt(value: EncryptedPassword): Password
}
trait JwtExpire[F[_]] {
def expiresIn(
claim: JwtClaim,
exp: TokenExpiration
): F[JwtClaim]
}
Lastly, Tokens is responsible for issuing JWT tokens, also mentioned in Chapter 8.
trait Tokens[F[_]] {
def create: F[JwtToken]
}
HTTP API
Finally, our HttpApi module groups all our HTTP routes and middlewares. Let’s start
with the smart constructor.
object HttpApi {
def make[F[_]: Async](
services: Services[F],
programs: Programs[F],
security: Security[F]
): HttpApi[F] =
new HttpApi[F](
services, programs, security
243
Chapter 9: Assembly
) {}
}
Simple and without much ceremony. Next, let’s look at its implementation.
// Auth routes
private val loginRoutes =
LoginRoutes[F](security.auth).routes
private val logoutRoutes =
LogoutRoutes[F](security.auth).routes(usersMiddleware)
private val userRoutes =
UserRoutes[F](security.auth).routes
// Open routes
private val healthRoutes =
HealthRoutes[F](services.healthCheck).routes
private val brandRoutes =
BrandRoutes[F](services.brands).routes
private val categoryRoutes =
CategoryRoutes[F](services.categories).routes
private val itemRoutes =
ItemRoutes[F](services.items).routes
// Secured routes
private val cartRoutes =
CartRoutes[F](services.cart).routes(usersMiddleware)
private val checkoutRoutes =
CheckoutRoutes[F](programs.checkout).routes(usersMiddleware)
244
Chapter 9: Assembly
// Admin routes
private val adminBrandRoutes =
AdminBrandRoutes[F](services.brands).routes(adminMiddleware)
private val adminCategoryRoutes =
AdminCategoryRoutes[F](services.categories).routes(adminMiddleware)
private val adminItemRoutes =
AdminItemRoutes[F](services.items).routes(adminMiddleware)
245
Chapter 9: Assembly
This is all. If something isn’t clear, please check out Chapter 5 once again, where we
went through all these concepts in fine detail.
We managed to group all the relevant functionality in distinct modules. Now is time to
talk about resources.
246
Chapter 9: Assembly
Resources
These are the resources we have identified in our application. Now, we need to provide a
smart constructor to create and compose all of them. In this case, we have two options.
The simplest one is to go for an Async typeclass constraint.
object AppResources {
def make[F[_]: Async](
cfg: AppConfig
): Resource[F, AppResources[F]] = ???
}
Creation of resources usually require hard constraints such as Sync or Async so this is
completely fine. However, in order to push the boundaries of the capability trait design,
we are going to go with the following constructor.
object AppResources {
def make[
F[_]: Concurrent: Console: Logger:
MkHttpClient: MkRedis: Network
](
cfg: AppConfig
): Resource[F, AppResources[F]] = ???
}
In Cats Effect 3, Concurrent does not provide FFI ability, so it can be considered a pure
typeclass. Console is one of the new standard effects provided by CE3, and Network is
one of the new capability traits provided by the fs2.io.net package. Both are used by
Skunk to create a Postgres connection. Logger comes from log4cats, and MkRedis is a
capability trait defined in redis4cats.
247
Chapter 9: Assembly
Ultimately, we have MkHttpClient, which is our custom capability trait to abstract over
the creation of an HTTP Client, which in the case of EmberClientBuilder, it requires an
Async constraint.
trait MkHttpClient[F[_]] {
def newEmber(c: HttpClientConfig): Resource[F, Client[F]]
}
object MkHttpClient {
def apply[F[_]: MkHttpClient]: MkHttpClient[F] = implicitly
Now that all the constraints have been explained, let’s have a look at the creation of the
resources.
def make[
F[_]: Concurrent: Console: Logger:
MkHttpClient: MkRedis: Network
](
cfg: AppConfig
): Resource[F, AppResources[F]] = {
def checkPostgresConnection(
postgres: Resource[F, Session[F]]
): F[Unit] =
postgres.use { session =>
session
.unique(sql"select version();".query(text))
.flatMap { v =>
Logger[F].info(s"Connected to Postgres $v")
248
Chapter 9: Assembly
}
}
def checkRedisConnection(
redis: RedisCommands[F, String, String]
): F[Unit] =
redis.info.flatMap {
_.get("redis_version").traverse_ { v =>
Logger[F].info(s"Connected to Redis $v")
}
}
def mkPostgreSqlResource(
c: PostgreSQLConfig
): SessionPool[F] =
Session
.pooled[F](
host = c.host.value,
port = c.port.value,
user = c.user.value,
password = Some(c.password.value),
database = c.database.value,
max = c.max.value
)
.evalTap(checkPostgresConnection)
def mkRedisResource(
c: RedisConfig
): Resource[F, RedisCommands[F, String, String]] =
Redis[F].utf8(c.uri.value).evalTap(checkRedisConnection)
(
MkHttpClient[F].newEmber(cfg.httpClientConfig),
mkPostgreSqlResource(cfg.postgreSQL),
mkRedisResource(cfg.redis)
).parMapN(new AppResources[F](_, _, _) {})
Resource forms a Monad, and thus an Applicative, so we can take advantage of this
property and compose all our different resources into a single one using the parMapN
function, which is available thanks to the Concurrent constraint.
The HTTP server is also modeled as a resource, but it has a few dependencies that will
249
Chapter 9: Assembly
become available once the services are built, which depend on some other resources, so
it needs to be called independently.
trait MkHttpServer[F[_]] {
def newEmber(
cfg: HttpServerConfig,
httpApp: HttpApp[F]
): Resource[F, Server]
}
object MkHttpServer {
def apply[F[_]: MkHttpServer]: MkHttpServer[F] = implicitly
250
Chapter 9: Assembly
Main
We first create a default logger. Then, we load the configuration and create a Supervisor
instance, required implicitly by Background, as explained in Chapter 4.
251
Chapter 9: Assembly
Next, we create the AppResources and proceed with the creation of the different modules.
Finally, we create the HTTP Server and run the composition of resources via useForever,
a convenient alias for use(_ => IO.never).
In fairness, that is all! We are now ready to start up our server. In an sbt session, we
can run the following command, which uses the sbt-revolver plugin.
sbt:shopping-cart> reStart
Assuming we have all our environment variables set up, and both our PostgreSQL and
Redis instances are running, we should see something like this when running it within
project core (output has been trimmed for readability):
sbt:core> reStart
[info] Application core not yet started
[info] Starting application core in the background ...
core Starting shop.Main.main()
core [io-compute-7] INFO shop.Main - Loaded config ( ...)
core [io-compute-1] INFO shop.Main - Connected to Postgres ( ...)
core [io-compute-5] INFO shop.Main - Connected to Redis ( ...)
core [io-compute-2] INFO o.h.ember.server.EmberServerBuilder ( ...)
core [io-compute-2] INFO shop.Main -
core _ _ _ _ _
core | |_| |_| |_ _ __| | | ___
core | ' \ _| _| '_ \_ _(_ -<
core |_ ||_\ __|\ __| . __/ |_|/ __/
core |_|
core HTTP Server started at /[0:0:0:0:0:0:0:0]:8080
sbt:shopping-cart> reStop
Source code
The source code of the Shopping Cart application can be found here24 .
Overview
252
Chapter 9: Assembly
253
Chapter 9: Assembly
Summary
Although it may seem simple to many of us, connecting the dots to put every piece
together so we can build an application is not that obvious sometimes, for which this
chapter should come in handy.
We have learned how to structure our application by grouping algebras in different
modules, how to handle configuration for different environments, logging, and resources
that have a life-cycle.
In all fairness, this is an essential skill-set to have out in the real world.
254
Chapter 10: Ship it!
We are almost done with our application. It is already tested and serving requests. Now
we need to deploy it in a real environment where it can run with high uptime.
There are many ways to deploy an application. We could create a fat jar, a war, or even
a simple binary file. Once we have this, we can run it using either java or bash in our
production server, for example. All of these methodologies come with pros and cons.
Nowadays, most environments are virtual machines running in our physical computer,
or more likely, in the cloud, using services such as AWS, GCP, and Azure.
While talking about virtualized environments, I must mention Docker and Kubernetes.
Docker allows us to pack our application and its dependencies in a single container
that can be shared and deployed into any environment. Kubernetes lets us orchestrate
different containers.
Given the simplicity of Docker and the exceptional support for it in Scala, we are going
to choose it for our application. If you use Kubernetes, you are expected to understand
what you can do with a Docker container. This topic is out of the scope of this book.
We are now going to focus on creating and shipping our application as a Docker image.
255
Chapter 10: Ship it!
Docker image
The easiest way to create a Docker image of a Scala application is by using the SBT
Native Packager2 plugin. It not only supports Docker but also zip and tar.gz files,
deb and rpm packages for Debian/RHEL based systems, and GraalVM3 native images,
among others.
First, we need to add it into our plugins.sbt file.
Notes
Replace VERSION with the current version of the plugin
Here we can configure the exposed port (used to access our HTTP server), the name of
our Docker image, and many other settings that are detailed in the documentation.
To create our Docker image, run the following command.
sbt docker:publishLocal
It will create a Docker image named shopping-cart and publish it into the local Docker
server. We can verify its existence as follows.
1
https://docs.docker.com/engine/docker-overview/
2
https://github.com/sbt/sbt-native-packager
3
https://www.graalvm.org/
256
Chapter 10: Ship it!
It seems we are done here. However, the size of the image should have caught your
attention. Let’s see what we can do to reduce its size.
Optimizing image
At the time of writing, the latest version of the SBT Native Packager plugin (1.8.1)
uses openjdk:8 as the base Docker image by default. Though, we can specify one, and
this will be our first and most important optimization: we do not need the JDK (Java
Development Kit) to run our application; we only need the JRE (Java Runtime Envi-
ronment).
If you do some research, you will find that there are many images we could use. We will
choose an Alpine image, which is rather small and well tested.
In our settings, we need to add the following custom image:
.settings(
dockerBaseImage := "openjdk:11-jre-slim-buster",
)
This greatly reduces the size of our image. However, there is a problem. Our generated
script is Bash-specific, which is not compatible with the Ash4 shell used by Docker, and
thus making it impossible to run our application.
Luckily, this issue goes away by enabling the Ash plugin, which tells our package manager
to generate our binary using Ash instead of Bash.
.enablePlugins(AshScriptPlugin)
There is one last optimization we can make. By default, both Linux and Windows
scripts will be created, and considering we are going to run our application in a Linux
environment, we can tell the plugin to skip the creation of the bat script file.
.settings(
makeBatScripts := Seq(),
)
4
https://en.wikipedia.org/wiki/Almquist_shell
257
Chapter 10: Ship it!
We have reduced the size of our image by half even when upgrading to Java 11, which
is known to be much heavier than Java 8. If we compare apples to apples and go with
openjdk:8u201-jre-alpine3.9 instead, the size will be reduced about four times!
Here is where we are going to stop. Still, readers are encouraged to investigate further
and make more optimizations.
Run it locally
Having a Docker image, we can now try to run it in our local machine to verify it works
as expected. Assuming a PostgreSQL and Redis services running in our same network,
we can create the following docker-compose.yml file.
version: '3.4'
services:
shopping_cart:
restart: always
image: shopping-cart:latest
network_mode: host
ports:
- "8080:8080"
environment:
- DEBUG=false
- SC_ACCESS_TOKEN_SECRET_KEY=YourToken
- SC_JWT_SECRET_KEY=YourSecret
- SC_JWT_CLAIM=YourClaim
- SC_ADMIN_USER_TOKEN=YourAdminToken
- SC_PASSWORD_SALT=YourEncryptionKey
- SC_APP_ENV=test
- SC_POSTGRES_PASSWORD=my-password
> docker-compose up
Creating app_shopping_cart_1 ... done
Attaching to app_shopping_cart_1
... more logs here ...
258
Chapter 10: Ship it!
Continuous Integration
A Continuous Integration (CI) build is almost mandatory these days. It automates the
build to keep us from deploying broken code. There are a few options in this space, and
without doubt, Github Actions5 is one of the most populars, so this will be our choice.
Dependencies
Our application needs both PostgreSQL and Redis in order to run. We are going to
provide a docker-compose file to make this easy.
services:
postgres:
restart: always
image: postgres:13.0-alpine
ports:
- "5432:5432"
environment:
- DEBUG=false
- POSTGRES_DB=store
- POSTGRES_PASSWORD=my-password
volumes:
- ./tables.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
redis:
restart: always
image: redis:6.2.0
ports:
- "6379:6379"
environment:
- DEBUG=false
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 1s
timeout: 3s
retries: 30
5
https://github.com/features/actions
259
Chapter 10: Ship it!
Our first service is postgres. It is almost self-explanatory, except for volumes. The
tables.sql is a SQL script that describes the structure of our database, and it allows
Docker to create the necessary tables on start-up. For more details, please look at the
source code.
Our second service is redis, which is much simpler than the previous one. Next, we can
start and stop our services.
To start both services.
> docker-compose up
Creating pfps-shopping-cart_postgres_1 ... done
Creating pfps-shopping-cart_redis_1 ... done
... more logs here ...
CI build
Now that we have our dependencies defined in a docker-compose.yml file, we can continue
with the configuration of our CI build using Github Actions.
In a .github/workflows/ci.yml file, we are going to have the following content.
name: Scala
on:
pull_request: {}
push:
branches:
- second-edition
jobs:
build:
runs-on: ubuntu-18:04
steps:
- uses: actions/checkout@v2.3.2
260
Chapter 10: Ship it!
- uses: olafurpg/setup-scala@v10
with:
java-version: graalvm@21.0.0
- name: Starting up Postgres & Redis
run: docker-compose up -d
- name: Tests
run: sbt 'test;it:test'
- name: Shutting down Postgres & Redis
run: docker-compose down
The build will be triggered either when some code is pushed to the second-edition
branch, or when there is any pull request.
Our build job involves checking out the code from the Git repository, setting up the
Scala tools, starting PostgreSQL and Redis, running both unit and integration tests,
and finally, stopping our services.
Notes
Github Actions is smart enough to wait for the Docker containers to
come up before running the following steps
Notice how we are able to select the Java version we want to use. In this case, we use
GraalVM CE 21.0.0, which is the one the project uses for the local environment. Yet,
we create the Docker image using a simple OpenJDK-11-JRE, mainly because it’s much
lighter than GraalVM. Different is the situation with Graal Native Image, where the
resulting Docker image can be of a significant smaller size, but it comes with a large set
of trade-offs.
As an optimization, we could also add caching support, as it has been done in our
application.
Regardless, this is all we need to have a continuous integration build set up.
Nix Shell
If you look at the source code, you’ll find a shell.nix and a different configuration for
our CI build. This is because the project’s dependencies are declared using Nix6 , but
this is optional. Readers can still choose the tool of preference (e.g. Coursier) to set up
the local environment.
Nevertheless, the advantage of using Nix to declare a reproducible development shell is
not only that we can ensure other team members get the same software versions, but
also that we can leverage this environment in the CI build as well. It looks as follows.
6
https://nixos.org/
261
Chapter 10: Ship it!
jobs:
build:
name: Build
runs-on: ubuntu-18.04
strategy:
matrix:
java:
- graalvm11-ce
steps:
- uses: actions/checkout@v2.3.2
Furthermore
We now have a Docker image and a CI build set up. Next step is to deploy our application
into our production environment. Depending on your resources, this may vary.
It is worth mentioning that, by using Github Actions, we should be able to configure
a Continuous Deployment (CD) build. This way, we would have a fully automated
deployment pipeline.
262
Chapter 10: Ship it!
For now, though, we will stop here to avoid getting too much into DevOps land, which
is way out of the scope of this book, and definitely not my area of expertise.
263
Chapter 10: Ship it!
Summary
This is the end of our Shopping Cart application development. You should now be ready
to dive into new endeavors and apply the techniques learned in this book.
The Gitter channel7 of the book will remain active, so feel free to join and ask questions,
either about the book’s topics or functional programming in general.
Once again, thanks to all of you who have supported my work. It has been a real
pleasure, and I hope this book helps you even with the smallest task at hand.
Writing a book is not easy. Writing your first book in your second language (English) is
tough. Yet, you all made it real, and I am delighted with the final result.
Eternally thankful, Gabriel.
7
https://gitter.im/pfp-scala/community
264
Bonus Chapter
Although we can consider our work with the Shopping Cart application done, there are
always new ideas emerging, cutting-edge techniques, and new libraries to try out.
This chapter will be dedicated to explore some of it. Let’s get started.
265
Bonus Chapter
MTL stands for Monad Transformers Library. Its name comes from Haskell’s MTL1 .
It encodes effects as typeclasses instead of using data structures (as Monad Transformers
do), from which we can deduce the MTL name is a historical accident at this point.
In the Scala ecosystem, there exists Cats MTL2 , which holds quite some differences with
its Haskell counterpart. The main one being having more granular typeclasses such as
Ask instead of MonadReader.
Cats MTL’s documentation is remarkable, so rather than repeating what is already there,
we are going to focus on two specific effects: Stateful and Ask.
Managing state
trait Stateful[F[_], S] {
def monad: Monad[F]
def inspect[A](f: S => A): F[A]
def modify(f: S => S): F[Unit]
def get: F[S]
def set(s: S): F[Unit]
}
It favors composition over inheritance to avoid the ambiguous implicits issue, which can
happen if the typeclass extends Monad instead of declaring it as a simple method.
Effects are encoded as typeclasses. Therefore, to make use of Stateful, we need to add
it as a constraint to our F[_].
1
https://hackage.haskell.org/package/mtl
2
https://typelevel.org/cats-mtl/
266
Bonus Chapter
b <- HasFoo[F].get
_ <- Console[F].println(b)
} yield ()
Usually, polymorphic MTL style programs are materialized using Monad Transformers,
and StateT would be our most natural choice here.
Though, for performance and ergonomic reasons, we could use a Ref-backed instance.
object StatefulRef {
def of[F[_]: Ref.Make: Monad, A](
init: A
): F[Stateful[F, A]] =
Ref.of[F, A](init).map { ref =>
new Stateful[F, A] {
def monad: Monad[F] = implicitly
In a way, this feels like cheating since the instance can only be created effectfully. Still,
we gain a lot by avoiding StateT and sticking to a simple effect like IO, so I would
personally endorse this technique.
Another reason to prefer the latter is that Ref supports concurrency, unlike StateT, which
is inherently sequential (see Chapter 1).
Tips
Prefer interfaces over dealing with raw state directly
267
Bonus Chapter
Accessing context
trait Ask[F[_], E] {
val applicative: Applicative[F]
We could also materialize it using IO directly, but it would require us to write the Ask[IO,
Ctx] instance ourselves, as we did with Stateful. Except this time there is not need for
a Ref.
268
Bonus Chapter
object ManualAsk {
def of[F[_]: Applicative, A](ctx: A): Ask[F, A] =
new Ask[F, A] {
def applicative: Applicative[F] = implicitly
269
Bonus Chapter
Optics
In a nutshell, optics are a first-class composable functional abstraction that let us ma-
nipulate data structures.
In Scala, there is a great library named Monocle3 that defines the entire hierarchy of
optics, as well as defining algebraic laws for such types.
If I am not mistaken, the word classy comes from the makeClassy function defined in
Haskell’s Lens package4 . There is another function called makeLenses, which generates
lenses for a given type. The classy variant does the same but, it additionally creates a
typeclass and an instance of that typeclass, along with some lenses.
There is also another function makeClassPrisms, which does the same but for prisms.
So it can be said we have classy optics when we can associate with each type a typeclass
full of optics for that type.
Optics are a gigantic topic, though, and one can probably write a book about it (in fact,
someone has recently done it5 ). For this reason, we are only going to discuss what is
relevant for the examples ahead: lenses and prisms.
To learn more, I recommend reading the Optics’ documentation6 .
Lenses
Lenses provide first-class access for product types. This means we can zoom-in into case
classes, for example, which are product types. We can also think of lenses as a pair of
getter and setter functions.
Given an instance of Person, we could access and modify the StreetName.
3
https://github.com/julien-truffaut/Monocle
4
https://hackage.haskell.org/package/lens
5
https://leanpub.com/optics-by-example
6
https://hackage.haskell.org/package/optics-0.1/docs/Optics.html
270
Bonus Chapter
person.copy(
address = person.address.copy(
streetName = StreetName("new st")
)
)
By using the native copy method, we can get away with the task at hand, but we can
see where this is going. Not only is it cumbersome; it doesn’t compose either.
We could use lenses instead, which can be composed with other optics. This is an
example using Monocle 3 and its new Focus API, which does not require us to create
lenses at all.
person
.focus(_.address.streetName)
.replace(StreetName("foo"))
However, we could still create the necessary lenses and use them instead. This is how
we define them using Monocle macros.
Composition of lenses
The resulting lens can be shared and used like any other value.
_PersonStreetName
.replace(StreetName("foo"))(person))
Prisms
Prisms provide first-class access for sum types, or also called co-product types. A prism
allows us to select a single branch of a sum type, e.g. Option, Either, or any other ADT.
Below we can see an example using Monocle’s prisms to manipulate an ADT.
271
Bonus Chapter
__Car.getOption(Boat) // None
__Car.getOption(Car) // Some(Car)
Given an instance of Vehicle, a prism would let us to access either branch. Thus, we
can say a prism can traverse a tree-like data structure and select a branch.
We can think of prisms as an abstraction that lets us zoom-in to a part of a value that
may not be there, therefore, returning an optional value.
Composing prisms
import monocle.std.option.some
Notice the usage of some, which is a predefined prism that allows us to compose lenses
of optional types.
As previously mentioned, optics compose. Not only we can compose prisms with prisms,
but also prisms with lenses, traversals, folds, and more.
Another compelling example where prisms shine is the same as lenses: accessors and
modifiers for case classes. Wait, case classes are product types, so you might be wonder-
ing how can prisms help here? See the example below.
272
Bonus Chapter
We have a product type Song that also contains an optional field Album, which forms a
sum type. In such cases, we can compose lenses and prisms to fulfill our needs.
The composition of a Lens and a Prism yields an Optional, which sits right between them
in the optics hierarchy. You can learn more about it in Monocle’s documentation.
Monocle 3 provides a new way to manipulate data via its Focus API. This would be the
equivalent in Scala 2.
273
Bonus Chapter
In Chapter 9, we have briefly learned about Natchez, a library that supports distributed
tracing. It was also mentioned that Tofu’s Mid and Http4s Tracer offer a different
approach to a similar problem. Here we are going to look into the former.
Tofu’s Mid
Tofu ships with a bunch of interesting features. One of them is Mid, a typeclass that adds
the superpowers of Aspect Oriented Programming (AOP)7 to our programs. It models
it with an F[A] => F[A] function.
trait Mid[F[_], A] {
def apply(fa: F[A]): F[A]
@inline def attach(fa: F[A]): F[A] = apply(fa)
}
Suppose we have a function returning F[User]. When using interpreters for Mid[F, *],
we can run actions before and after the main function is evaluated. E.g. we can log a
message before processing and one after we get the User. The best way to see this in
action is to learn by example.
Let’s start with two algebras, Metrics and Logger, and their interpreters.
trait Metrics[F[_]] {
def timed[A](key: String)(fa: F[A]): F[A]
}
object Metrics {
def make[F[_]: FlatMap: LiftIO]: Metrics[F] =
new Metrics[F] {
def timed[A](
key: String
)(fa: F[A]): F[A] =
IO.println(s"[METR] - Key: $key").to[F] >> fa
}
}
trait Logger[F[_]] {
def info(str: String): F[Unit]
}
7
https://en.wikipedia.org/wiki/Aspect-oriented_programming
274
Bonus Chapter
object Logger {
def make[F[_]: LiftIO]: Logger[F] =
new Logger[F] {
def info(str: String): F[Unit] =
IO.println(s"[INFO] - $str").to[F]
}
}
import derevo.tagless.applyK
@derive(applyK)
trait UserStore[F[_]] {
def register(username: String): F[Int]
}
Then we need implementations for all the algebras using Mid[F, *] as the effect type.
In the case of UserStore[Mid[F, *]], we get an F[Int] => F[Int] function. In our Logger,
we add messages before and after the length of the username is evaluated. On the
other hand, Metrics is a pass-through. Both implementations marked as private final
class.
In our little example, these can live within the companion object of UserStore. However,
in a real application these interpreters might get bigger, in which case, it might be better
to place them in separate files but they should remain private[package] or protected at
least.
Next we have the most interesting part that combines the different interpreters.
275
Bonus Chapter
object UserStore {
def make[F[_]: Monad](
metrics: Metrics[F],
logger: Logger[F]
): UserStore[F] =
NonEmptyList
.of[UserStore[Mid[F, *]]](
new UserLogger(logger),
new UserMetrics(metrics)
)
.reduce
.attach {
new UserStore[F] {
def register(username: String): F[Int] =
username.length.pure[F]
}
}
}
UserStore[Mid[F, *]] forms a Semigroup, thanks to the ApplyK instance, so we can com-
bine interpreters using the |+| operator, or in this case, via NonEmptyList’s reduce, defined
as below.
We first combine our interpreters in Mid[F, *] and then attach our regular interpreter.
Let’s now put all these together and make a little program.
276
Bonus Chapter
The order in which we combine the interpreters in Mid[F, *] matters! In this case,
UserLogger runs before UserMetrics. This is something to have in mind.
8
https://docs.tofu.tf/docs/mid#example
277
Bonus Chapter
Concurrency
We have extensively used Ref in our application for different purposes, and we have also
learned about Semaphore in Chapter 1. Yet, we haven’t had the opportunity to look into
many other interesting concurrent data structures, so that’s what we are going to do in
this section.
In addition to Ref, Cats Effect ships with Deferred, Queue, and Hotswap, among others.
On the other hand, the Fs2 library ships with Channel, Topic, and Signal.
Let’s jump straight into some examples that showcase different use cases.
Producer-Consumer
We can simulate the typical producer-consumer program using a Queue. In the following
example, the former produces random numbers every second whereas the latter consumes
these numbers and prints them out.
For demonstration purposes, the process will be interrupted after five seconds elapse.
(
Random.scalaUtilRandom[IO],
Queue.bounded[IO, Int](100)
).tupled.flatMap {
case (random, q) =>
val producer =
random
.betweenInt(1, 11)
.flatMap(q.offer)
.flatTap(_ => IO.sleep(1.second))
.foreverM
val consumer =
q.take.flatMap { n =>
IO.println(s"Consumed #$n")
}.foreverM
(producer, consumer)
.parTupled
.void
.timeoutTo(5.seconds, IO.println("Interrupted"))
}
278
Bonus Chapter
The same program can also be implemented using Fs2, which provides another level of
abstraction and a great DSL to work with.
Stream.eval(producer)
.concurrently(Stream.eval(consumer))
.interruptAfter(5.seconds)
.onFinalize(IO.println("Interrupted"))
Though, by using concurrently, we don’t get the same semantics we get using parTupled
above. To better understand this, let’s take a little detour into Fs2 streams.
Effectful streams
Fs2 is a library that provides purely functional, effectful, resource-safe, and concurrent
streams for Scala.
At a first glance, it may seem intimidating, but don’t let that first impression put you
off it. Many other great libraries are built on top of this giant: Http4s, Doobie, and
Skunk, to name a few.
The killer application for streams is dealing with I/O while processing data in constant
memory; it is a great choice when your data doesn’t fit into memory. However, Fs2 offers
much more, as we are going to explore in the next section.
Streams
In big applications, it is very common to run both an HTTP server and a message
broker such as Kafka or Pulsar, concurrently serving HTTP requests while consuming
and producing a stream of values.
We could try to do this at the effect level, but we would need to deal with a lot of corner
cases related to concurrency and resource safety. It is always recommended to choose a
high-level library over bare bones, and this is where, among other areas, Fs2 shines.
If we have both an HTTP server and a Kafka consumer, we can do the following.
279
Bonus Chapter
The parJoin method will non-deterministically merge a stream of streams into a single
stream; it races all the inner streams simultaneously, opening at most maxOpen streams
at any point in time.
This is more or less what any of the par functions such as parTupled do, except streams
implicate greater complexity and, among other things, have to deal with scoping (a
stream can be potentially infinite), cancelation and resource-safety.
The value of maxOpen is 2 in our example, as we want to keep the server and consumer
running concurrently.
If the processes are unrelated to one another, as it is in this case with our server and
consumer, we more likely need parJoin. In some other cases, we might want to make
them dependent on each other, for which concurrently may be a better fit.
This is exactly what we have previously done in our producer-consumer example.
As its name suggests, it runs the producer while running the consumer in the background.
Upon finalization of the former, the consumer will be interrupted and awaited for its
finalizers to run.
It could also be the other way around, depending on the desired semantics.
By compiling our stream, we can go back to IO (or any F[_] that satisfies a Compiler
constraint).
program.compile.drain // IO[Unit]
We can also combine different semantics. Say we want to run a server, a consumer, and
a producer. The server is independent of the others, whereas the producer depends on
the consumer. We can achieve this behavior by combining parJoin and concurrently.
280
Bonus Chapter
Interruption
Another particularly good use of Fs2 streams for control flow is managing interruption.
It allows us to do so in a few lines, utilizing its high-level API.
The following program interrupts the action of printing out “ping” after 3 seconds.
Stream
.repeatEval(IO.println("ping"))
.metered(1.second)
.interruptAfter(3.seconds)
.onFinalize(IO.println("pong"))
[info] ping
[info] ping
[info] pong
Using interruptAfter, we can only interrupt the stream at a specified time. If we wanted
to interrupt the stream on a given condition, we should use interruptWhen instead.
There are a few variants of the same function. Let’s see an example based on Deferred.
Stream
.eval(Deferred[IO, Either[Throwable, Unit]])
.flatMap { switch =>
Stream
.repeatEval(IO(Random.nextInt(5)))
.metered(1.second)
.evalTap(IO.println)
281
Bonus Chapter
.evalTap { n =>
switch.complete(().asRight).void.whenA(n == 0)
}
.interruptWhen(switch)
.onFinalize(IO.println("Interrupted!"))
}
.void
We first create an instance of Deferred called “switch”, and then proceed to generate
and print out random numbers from 0 to 4 infinitely. If we get the number zero, we
complete our promise, which will trigger the interruption of the whole stream.
We will see an output similar to the one below when we run it.
[info] 4
[info] 2
[info] 2
[info] 0
[info] Interrupted!
It is a powerful function that can be further composed to achieve better control flow.
Pausing a stream
Interruption – and the ability to control it – is great, but it is not always what we want.
What if we wanted to pause our stream for a while (e.g. waiting for an external result)
and then continue from where we left off?
In such a case, what we need is pauseWhen, which takes either a Signal[F, Boolean] or a
Stream[F, Boolean].
import fs2.concurrent.SignallingRef
Stream
.eval(SignallingRef[IO, Boolean](false))
.flatMap { signal =>
val src =
Stream
.repeatEval(IO.println("ping"))
.pauseWhen(signal)
.metered(1.second)
282
Bonus Chapter
val pause =
Stream
.sleep[IO](3.seconds)
.evalTap(_ => IO.println(" >> Pausing stream <<"))
.evalTap(_ => signal.set(true))
val resume =
Stream
.sleep[IO](7.seconds)
.evalTap(_ => IO.println(" >> Resuming stream <<"))
.evalTap(_ => signal.set(false))
We first create a signal to then build the rest of our program, which is composed of three
smaller programs: src, pause, and resume. The first one is the source stream that prints
out “ping” every second, which can be paused or resumed via pauseWhen(signal). The
pause program will set our signal value to true after 3 seconds (pausing src), and the
resume program does the opposite after 7 seconds.
All these small programs are put together as a stream of streams, using parJoinUnbounded
to run them concurrently.
This composed program will be interrupted after 10 seconds, no matter what. Once the
stream completes, it will also print out “pong”. You should see an output like the one
below when you run it.
[info] ping
[info] ping
[info] >> Pausing stream <<
[info] ping
[info] >> Resuming stream <<
[info] ping
[info] ping
[info] ping
[info] pong
283
Bonus Chapter
Multiple subscriptions
Now that we have an understanding of the streaming model, let’s continue with another
concurrent data structure named Topic, which supports multiple subscriptions.
The following example defines a single producer and multiple consumers.
import fs2.concurrent.Topic
(
Random.scalaUtilRandom[IO],
Topic[IO, Int]
).tupled.flatMap {
case (random, topic) =>
def consumer(id: Int) =
topic
.subscribe(10)
.evalMap(n => IO.println(s"Consumer #$id got: $n"))
.onFinalize(IO.println(s"Finalizing consumer #$id"))
val producer =
Stream
.eval(random.betweenInt(1, 11))
.evalMap(topic.publish1)
.repeat
.metered(1.second)
.onFinalize(IO.println("Finalizing producer"))
producer
.concurrently(
Stream(
consumer(1),
consumer(2),
consumer(3)
).parJoin(3)
)
.interruptAfter(5.seconds)
.onFinalize(IO.println("Interrupted"))
.compile
.drain
}
Once the producer is done, we want the consumers to be interrupted, thus we use
concurrently. However, this is irrelevant in this case, as we are interrupting the whole
stream after five seconds elapsed.
284
Bonus Chapter
Running this program should produce a similar output to the one below.
(Un)Cancelable regions
Cancelation can entail great complexity in concurrent applications. Cats Effect gives us
the tools to handle both cancelable and uncancelable regions, as well as a combination
of them.
Here’s the type signature of one the most important functions defined in Async.
The call to gate.get is semantically blocking, so what happens when we run this?
285
Bonus Chapter
}
}
Our program invokes the nope function, and it runs it in the background. Next, it waits
for 500 milliseconds and it prints out a message. Lastly, the finalizer of the background
resource will invoke the cancellation of the spawned fiber.
However, the program will hang forever on gate.get because the Deferred is never com-
pleted. To ensure we don’t get into this dead-lock, we should wrap this action using
Poll, to indicate this particular action can be canceled within the uncancelable region.
Resource safety
Cats Effect provides a Resource datatype, which allows us to perform a clean-up either
in case of completion or failure. We can revisit our Background effect implementation
and try to implement it differently.
Below is our simple Background implementation.
286
Bonus Chapter
It spawns a new fiber for every scheduled computation that is then associated to the
lifecycle of the Supervisor. We discard the resulting fiber (void) and let the supervisor
take full ownership of the process.
At this moment, we had lost control over the process since the fiber is gone (void). In
most cases, it is acceptable to fire-off computations this way, but it could easily become
a difficulty when the application starts to grow.
We could treat fibers as a resource that needs to be cleaned up instead. Let’s see a safer
implementation based on Resource, and powered by a Queue.
import cats.effect.std.Queue
import cats.effect.syntax.spawn._
q.take
.flatMap {
case (duration, fa) =>
fa.attempt >> Temporal[F].sleep(duration)
}
.background
.as(bg)
}
)
287
Bonus Chapter
The previous program is mainly implemented using Resource, and dealing with fibers,
which are low-level. Preferably, we should define our program in terms of Fs2 streams,
which already considers many corner cases we might be missing.
Since Fs2 is built on top of Cats Effect, it also understands Resource, and it can create
one from a stream.
val process =
Stream
.repeatEval(q.take)
.map { case (duration, fa) =>
Stream.eval(fa.attempt).drain.delayBy(duration)
}
.parJoinUnbounded
Stream.emit(bg).concurrently(process)
}
.compile
.resource
.lastOrError
Notice how the concurrently method replaces the manual start and cancel (in the
previous case defined via the background extension method), which already manages
interruption for us.
In the end, we perform a compile.resource.lastOrError, which is ideal when a stream
produces a single element. In this case, it is a single Background instance.
Finally, we need to change how we are using Background. It can no longer be an implicit
effect, and it should now be considered a resource.
288
Bonus Chapter
Background.resource[IO].use { bg =>
restOfTheProgram(bg)
}
This would be the most correct usage, though, it means we need to modify our entire
application to take an explicit Background. We could instead make an exception and
make it implicit, as the semantics will remain the same.
The run function takes in a state S and an input I, and returns a new state S and an
output O, within a context F.
When we don’t need any context, we can use the identity FSM.
object FSM {
def id[S, I, O](run: (S, I) => Id[(S, O)]) = FSM(run)
}
9
https://en.wikipedia.org/wiki/Finite-state_machine
289
Bonus Chapter
Gems example
The following example showcases the utility of finite state machines with a small program
that counts the different gems.
It increments the count of the given gem by one, which can be one of the following four
types.
The interesting thing about the FSM is that it is pure logic and it can be tested on its
own (try it out at home!) by feeding inputs and writing expectations on the outputs.
The Fs2 library comes with two functions that fit perfectly any FSM. See their slightly
simplified definition below.
Next, let’s say we have the following gems to be counted (though, in the real world this
could be a long-running function processing gems coming from an external source such
as a file or a Pulsar topic).
290
Bonus Chapter
source
.mapAccumulate(initial)(fsm.run)
.map(_._2)
.lastOr("No results")
.evalMap(IO.println)
In this case, it is nicely formatted thanks to a custom Show[State] instance you can find
in the source code.
This concludes state machines! If you want more, check out this blogpost10 I wrote a
while ago that showcases a larger example, among other things.
10
https://gvolpe.com/blog/fsm-fs2-a-match-made-in-heaven/
291
Bonus Chapter
Summary
292