Introduction
This jump start guide barely scratches the surface of what Cats can do, but instead provides a concise hands-on introduction to the patterns you're most likely to take advantage of in your Scala project.
If you're using the constructs like Future
,
Option
or
Either
on a daily basis,
it's very likely that Cats can simplify and improve the readability of your code.
Please refer to the wiki on GitHub for guidelines on how to add the library to your project dependencies. We are sticking to version 1.0.1 in the entire post.
Let's go through the library package-wise, looking at the syntax available in each package.
Option Helpers
import cats.syntax.all._
Importing this package enables obj.some
syntax — equivalent to Some(obj)
.
The only real difference is that the value is already upcast to Option[T]
from Some[T]
.
Using obj.some
instead of Some(obj)
can sometimes e.g. improve readability when you need to provide dummy implementation for service methods for the purpose of testing.
For example, if you put the following implicit class into the scope:
import scala.concurrent.Future
implicit class ToFutureSuccessful[T](obj: T) {
def asFuture: Future[T] = Future.successful(obj)
}
then you can use the chained syntax shown below:
class Account { /* ... */ }
trait AccountService {
def getAccountById(id: Int): Future[Option[Account]]
}
class DummyAccountServiceImpl extends AccountService {
def dummyAccount: Account = ???
/* ... */
override def getAccountById(id: Int): Future[Option[Account]] = dummyAccount.some.asFuture
/* ... */
}
That's more readable than Future.successful(Some(dummyAccount))
, especially if this pattern repeats frequently.
Chaining .some.asFuture
at the end rather than putting it at the front also helps focus on what's actually being returned rather than on the expected wrapper type.
none[T]
, in turn, is shorthand for Option.empty[T]
which is just None
, but already upcast from None.type
to Option[T]
.
Providing a more specialized type sometimes helps the Scala compiler properly infer the type of expressions containing None
.
Either Helpers
import cats.syntax.all._
obj.asRight
is Right(obj)
, obj.asLeft
is Left(obj)
.
In both cases the type of returned value is widened from Right
or Left
to Either
.
Just as was the case with .some
, these helpers are handy to combine with .asFuture
to improve readability:
case class User(accountId: Long) { /* ... */ }
trait UserService {
def ensureUserExists(id: Int): Future[Either[Exception, User]]
}
class UserServiceSpec extends UserService {
def dummyUser: User = ???
/* ... */
override def ensureUserExists(id: Int): Future[Either[Exception, User]] = dummyUser.asRight.asFuture // instead of Future.successful(Right(dummyUser))
/* ... */
}
Either.fromOption(option: Option[A], ifNone: => E)
, in turn, is a useful helper for converting an Option
to an Either
.
If the provided option
is Some(x)
, it becomes Right(x)
.
Otherwise it becomes Left
with the provided ifNone
value inside.
Tuples
The apply
package provides (..., ..., ...).mapN
syntax, which allows for an intuitive construct for applying a function that takes more than one parameter to multiple effectful values (like futures).
Let's say we have 3 futures, one of type Int
, one of type String
, one of type User
and a method accepting three parameters — Int
, String
and User
.
import scala.concurrent.ExecutionContext.Implicits.global
class ProcessingResult { /* ... */ }
def intFuture: Future[Int] = { /* ... */ ??? }
def stringFuture: Future[String] = { /* ... */ ??? }
def userFuture: Future[User] = { /* ... */ ??? }
def process(value: Int, contents: String, user: User): ProcessingResult = { /* ... */ ??? }
Our goal is to apply the function to the values computed by those 3 futures.
With apply
syntax this becomes very easy and concise:
import cats.syntax.all._
def processAsync: Future[ProcessingResult] = {
(intFuture, stringFuture, userFuture).mapN {
(value, contents, user) =>
process(value, contents, user)
}
}
By default the implicit instances (namely, Functor[Future]
and
Semigroupal[Future]
) required for mapN
to work properly are always visible. They are present in the respective companion objects of the instances and hence we do not need to import them explicitly.
This above idea can be expressed even shorter, just:
def processAsync2: Future[ProcessingResult] = (intFuture, stringFuture, userFuture).mapN(process)
If any of the chained futures fails, the resulting future will also fail with the same exception as the first failing future in the chain (this is fail-fast behavior).
What's important, all futures will run in parallel, as opposed to what would happen in a for
comprehension:
def processAsync3: Future[ProcessingResult] = {
for {
value <- intFuture
contents <- stringFuture
user <- userFuture
} yield process(value, contents, user)
}
In the above snippet (which under the hood translates to flatMap
and map
calls), stringFuture
will not run until intFuture
is successfully completed,
and in the same way userFuture
will be run only after stringFuture
completes.
But since the computations are independent of one another, it's perfectly viable to run them in parallel with mapN
instead.
Traversing
import cats.syntax.all._
traverse
If you have an instance obj
of type F[A]
that can be mapped over (like Future
) and a function fun
of type A => G[B]
, then calling obj.map(fun)
would give you F[G[B]]
.
In many common real-life cases, like when F
is Option
and G
is Future
, you would get Option[Future[B]]
, which most likely isn't what you wanted.
traverse
comes as a solution here.
If you call traverse
instead of map
, like obj.traverse(fun)
, you'll get G[F[A]]
, which will be Future[Option[B]]
in our case; this is much more useful and easier to process than Option[Future[B]]
.
def updateUser(user: User): Future[User] = { /* ... */ ??? }
def updateUsers(users: List[User]): Future[List[User]] = {
users.traverse(updateUser)
}
As a side note, there is also a dedicated method Future.traverse
in the Future
companion object,
but the Cats version is far more readable and can easily work on any structure for which certain type classes are available.
sequence
sequence
represents an even simpler concept: it can be thought of as simply swapping the types from F[G[A]]
to G[F[A]]
without even mapping the enclosed value like traverse
does.
val foo: List[Future[String]] = List(Future("hello"), Future("world"))
val bar = foo.sequence // will have Future[List[String]] type
obj.sequence
is in fact implemented in Cats as obj.traverse(identity)
.
On the other hand, obj.traverse(fun)
is roughly equivalent to obj.map(fun).sequence
.
flatTraverse
If you have an obj
of type F[A]
and a function fun
of type A => G[F[B]]
, then doing obj.map(f)
yields result of type F[G[F[B]]]
— very unlikely to be what you wanted.
Traversing the obj
instead of mapping helps a little — you'll get G[F[F[B]]
instead.
Since G
is usually something like Future
and F
is List
or Option
, you would end up with Future[Option[Option[A]]
or Future[List[List[A]]]
— a bit awkward to process.
lazy val valueOpt: Option[Int] = { /* ... */ ??? }
def compute(value: Int): Future[Option[Int]] = { /* ... */ ??? }
def computeOverValue: Future[Option[Option[Int]]] = valueOpt.traverse(compute) // has type Future[Option[Option[Int]]], not good
The solution could be to map the result with a _.flatten
call like:
def computeOverValue2: Future[Option[Int]] = valueOpt.traverse(compute).map(_.flatten)
and this way you'll get the desired type G[F[B]]
at the end.
However, there is a neat shortcut for this called flatTraverse
:
def computeOverValue3: Future[Option[Int]] = valueOpt.flatTraverse(compute)
and that solves our problem for good.
Monad Transformers
OptionT
import cats.data.OptionT
An instance of OptionT[F, A]
can be thought of as a wrapper over F[Option[A]]
which adds a couple of useful methods specific to nested types that aren't available in F
or Option
itself.
Most typically, your F
will be Future
(or sometimes slick's DBIO
, but this requires having an implementation of Cats type classes like Functor
or Monad
for DBIO
).
Wrappers such as OptionT
are generally known as monad transformers.
A quite common pattern is mapping the inner value stored inside an instance of F[Option[A]]
to an instance of F[Option[B]]
with a function of type A => B
.
This can be done with rather verbose syntax like:
lazy val resultFuture: Future[Option[Int]] = ???
def mappedResultFuture: Future[Option[String]] = resultFuture.map { maybeValue =>
maybeValue.map { value =>
// Do something with the value and return String
???
}
}
With the use of OptionT
, this can be simplified as follows:
def mappedResultFuture2: OptionT[Future, String] = OptionT(resultFuture).map { value =>
// Do something with the value and return String
???
}
The above map
will return a value of type OptionT[Future, String]
.
To get the underlying Future[Option[String]]
value, simply call .value
on the OptionT
instance.
It's also a viable solution to fully switch to OptionT[Future, A]
in method parameter/return types and completely (or almost completely) ditch Future[Option[A]]
in type declarations.
There are several ways to construct an OptionT
instance.
The method headers in the table below are slightly simplified: the type parameters and type classes required by each method are skipped.
Method | Takes | Returns |
---|---|---|
OptionT.apply or OptionT(...) |
F[Option[A]] |
OptionT[F, A] |
OptionT.fromOption |
Option[A] |
OptionT[F, A] |
OptionT.liftF |
F[A] |
OptionT[F, A] |
OptionT.pure |
A |
OptionT[F, A] |
In production code you'll most commonly use the OptionT(...)
syntax in order to wrap an instance of Future[Option[A]]
into OptionT[F, A]
.
The other methods, in turn, prove useful to set up OptionT
-typed dummy values in unit tests.
We have already come across one of OptionT
's methods, namely map
.
There are several other methods available and they mostly differ by the signature of the function they accept as the parameter.
As was the case with the previous table, the expected type classes are skipped.
Method | Takes | Returns |
---|---|---|
map[B] |
A => B |
OptionT[F, B] |
subflatMap[B] |
A => Option[B] |
OptionT[F, B] |
semiflatMap[B] |
A => F[B] |
OptionT[F, B] |
flatMapF[B] |
A => F[Option[B]] |
OptionT[F, B] |
flatMap[B] |
A => OptionT[F, B] |
OptionT[F, B] |
In practice, you're most likely to use map
and semiflatMap
.
As is always the case with flatMap
and map
, you can use it not only explicitly, but also under the hood in for
comprehensions, as in the example below:
class Money { /* ... */ }
def findUserById(userId: Long): OptionT[Future, User] = { /* ... */ ??? }
def findAccountById(accountId: Long): OptionT[Future, Account] = { /* ... */ ??? }
def getReservedFundsForAccount(account: Account): OptionT[Future, Money] = { /* ... */ ??? }
def getReservedFundsForUser(userId: Long): OptionT[Future, Money] = for {
user <- findUserById(userId)
account <- findAccountById(user.accountId)
funds <- getReservedFundsForAccount(account)
} yield funds
The OptionT[Future, Money]
instance returned by getReservedFundsForUser
will enclose a None
value if any of the three composed methods returns an OptionT
corresponding to None
.
Otherwise, if the result of all three calls contains Some
, the final outcome will also contain Some
.
EitherT
import cats.data.EitherT
EitherT[F, A, B]
is the monad transformer for Either
— you can think of it as a wrapper over a F[Either[A, B]]
value.
Just as in the above section, I simplified the method headers, skipping type parameters or their context bounds and lower bounds.
Let's have a quick look at how to create an EitherT
instance:
Method | Takes | Returns |
---|---|---|
EitherT.apply or EitherT(...) |
F[Either[A, B]] |
EitherT[F, A, B] |
EitherT.fromEither |
Either[A, B] |
EitherT[F, A, B] (wraps the provided Either value into F ) |
EitherT.right or EitherT.liftF |
F[B] |
EitherT[F, A, B] (wraps value inside F[B] into Right ) |
EitherT.left |
F[A] |
EitherT[F, A, B] (wraps value inside F[B] into Left ) |
EitherT.pure |
A |
EitherT[F, A, B] (wraps value into Right and then into F ) |
Another useful way to construct an EitherT
instance is to use OptionT
's methods toLeft
and toRight
:
abstract class BaseException(message: String) extends Exception(message)
case class UserNotFoundException(message: String) extends BaseException(message)
def getUserById(userId: Int): Future[Option[User]] = { /* ... */ ??? }
def ensureUserExists(userId: Int): EitherT[Future, BaseException, User] = {
OptionT(getUserById(userId))
.toRight(left = UserNotFoundException(s"user not found, userId=$userId"))
}
toRight
is pretty analogous to the method Either.fromOption
mentioned before: just as fromOption
built an Either
from an Option
, toRight
creates an EitherT
from an OptionT
.
If the original OptionT
stores Some
value, it will be wrapped into Right
; otherwise the value provided as the left
parameter will be wrapped into a Left
.
To provide the left
value within the monad, there is corresponding toRightF
method.
toLeft
is toRight
's counterpart which wraps the Some
value into Left
and transforms None
into Right
enclosing the provided right
value.
This is less commonly used in practice, but can serve e.g. for enforcing uniqueness checks in code.
We return Left
if the value has been found, and Right
if it doesn't yet exist in the system.
The methods available in EitherT
are pretty similar to those we've seen in OptionT
, but there are some notable differences.
You might get into some confusion at first when it comes to e.g. map
.
In the case of OptionT
, it was pretty obvious what should be done: map
should go over the Option
enclosed within Future
, and then map the enclosed Option
itself.
This is slightly less obvious in case of EitherT
: should it map over both Left
and Right
values, or only the Right
value?
The answer is that EitherT
is right-biased, therefore plain map
actually deals with the Right
value.
This is unlike Either
in the Scala standard library up to 2.11, which is in turn unbiased: there's no map
available in Either
, only for its left and right projections.
Having said that, let's take a quick look at the right-biased methods that EitherT
offers:
Method | Takes | Returns |
---|---|---|
map[D] |
B => D |
EitherT[F, A, D] |
subflatMap[D] |
B => Either[A, D] |
EitherT[F, A, D] |
semiflatMap[D] |
B => F[D] |
EitherT[F, A, D] |
flatMapF[D] |
B => F[Either[A, D]] |
EitherT[F, A, D] |
flatMap[D] |
B => EitherT[F, A, D] |
EitherT[F, A, D] |
As a side note, there are also certain methods in EitherT
(that you're likely to need at some point) which map over the Left
value, like leftMap
, or over both Left
and Right
values, like fold
or bimap
.
EitherT
is very useful for fail-fast chained verifications:
case class Item(state: String)
class ItemOrder { /* ... */ }
case class ItemNotFoundException(message: String) extends BaseException(message)
case class InvalidItemStateException(message: String) extends BaseException(message)
def getItemById(itemId: Int): Future[Option[Item]] = { /* .. */ ??? }
def ensureItemExists(itemId: Int): EitherT[Future, BaseException, Item] = {
OptionT(getItemById(itemId))
.toRight(ItemNotFoundException(s"item not found, itemId = $itemId"))
}
def ensureItemStateIs(actual: String, expected: String): EitherT[Future, BaseException, Unit] = {
// Returns a Unit value wrapped into Right and then into Future if condition is true,
// otherwise the provided exception wrapped into Left and then into Future.
EitherT.cond(actual == expected, (), InvalidItemStateException(s"actual=$actual, expected=$expected"))
}
def placeOrderForItem(userId: Int, itemId: Int, count: Int): Future[ItemOrder] = { /* ... */ ??? }
def buyItem(userId: Int, itemId: Int, count: Int): EitherT[Future, BaseException, ItemOrder] = {
for {
user <- ensureUserExists(userId)
item <- ensureItemExists(itemId)
_ <- ensureItemStateIs(item.state, "AVAILABLE_IN_STOCK")
// EitherT.liftF is necessary to make EitherT[Future, BaseException, ItemOrder] out of Future[ItemOrder]
placedOrder <- EitherT.liftF(placeOrderForItem(userId, itemId, count))
} yield placedOrder
}
In the above example, we're running various checks against the item one by one.
If any of the checks fails, the resulting EitherT
will contain a Left
value.
Otherwise, if all of the checks yield a Right
(of course we mean a Right
wrapped into an EitherT
), then the final outcome will also contain Right
.
This is a fail-fast behavior: we're effectively stopping the for
comprehension flow at the first Left
-ish result.
If you're instead looking for validation that accumulates the errors (e.g. when dealing with user-provided form data), cats.data.Validated
may be a good choice.
Common issues
The Cats type class instances for standard library types are available in implicit scope and hence no longer have to be imported. If anything doesn't compile as expected, first make sure all the required Cats syntax implicits are in the scope — try importing cats.syntax.all._
and see if the problem persists. The only exception is here is Cat's own Order
and PartialOrder
type classes which are available by importing cats.implicits._
.
As mentioned before, though, it's better to use narrow imports, but if the code doesn't compile it's sometimes worth just importing the entire library to check if it solves the problem.
If you're using Futures
, make sure to provide an implicit ExecutionContext
in the scope, otherwise Cats won't be able to infer implicit instances for Future
's type classes.
IntelliJ sometimes reports errors in Cats-loaded code even though the source passes under scalac.
One such example are invocations of the methods of cats.data.Nested
class, which compile correctly under scalac, but don't type check under IntelliJ's presentation compiler.
It should work without trouble under Scala IDE, though.
As an advice for your future learning: the Applicative
type class, despite it's key significance in functional programming, comes slightly difficult to understand.
In my opinion it's much less intuitive than Functor
or Monad
, even though it actually stands right between Functor
and Monad
in the inheritance hierarchy.
The best approach to grasp Applicative
is to first understand how product
(which transforms an F[A]
and F[B]
to an F[(A, B)]
) works rather than focus on the somewhat exotic ap
operation itself.