Applicative
extends Apply
by adding a single method, pure
:
Apply
extends the Functor
type class (which features the familiar map
function) with a new function ap
.
Apply
extends the Functor
type class (which features the familiar map
function) with a new function ap
. The ap
function is similar to map
in that we are transforming a value in a context (a context being the F
in F[A]
;
a context can be Option
, List
or Future
for example).
However, the difference between ap
and map
is that for ap
the function that
takes care of the transformation is of type F[A => B]
, whereas for map
it is A => B
:
Here are the implementations of Apply
for the Option
and List
types:
import cats._ implicit val optionApply: Apply[Option] = new Apply[Option] { def ap[A, B](f: Option[A => B])(fa: Option[A]): Option[B] = fa.flatMap (a => f.map (ff => ff(a))) def map[A,B](fa: Option[A])(f: A => B): Option[B] = fa map f def product[A, B](fa: Option[A], fb: Option[B]): Option[(A, B)] = fa.flatMap(a => fb.map(b => (a, b))) } implicit val listApply: Apply[List] = new Apply[List] { def ap[A, B](f: List[A => B])(fa: List[A]): List[B] = fa.flatMap (a => f.map (ff => ff(a))) def map[A,B](fa: List[A])(f: A => B): List[B] = fa map f def product[A, B](fa: List[A], fb: List[B]): List[(A, B)] = fa.zip(fb) }
Cats is an experimental library intended to provide abstractions for functional programming in Scala.
Foldable type class instances can be defined for data structures that can be folded to a summary value.
Foldable type class instances can be defined for data structures that can be folded to a summary value.
In the case of a collection (such as List
or Set
), these methods will fold
together (combine) the values contained in the collection to produce a single
result. Most collection types have foldLeft
methods, which will usually be
used by the associated Foldable[_]
instance.
Foldable[F]
is implemented in terms of two basic methods:
foldLeft(fa, b)(f)
eagerly folds fa
from left-to-right.foldRight(fa, b)(f)
lazily folds fa
from right-to-left.These form the basis for many other operations, see also: A tutorial on the universality and expressiveness of fold
First some standard imports.
import cats._ import cats.implicits._
Apart from the familiar foldLeft
and foldRight
, Foldable
has a number of other useful functions.
A Functor
is a ubiquitous type class involving types that have one
"hole", i.e.
A Functor
is a ubiquitous type class involving types that have one
"hole", i.e. types which have the shape F[?]
, such as Option
,
List
and Future
. (This is in contrast to a type like Int
which has
no hole, or Tuple2
which has two holes (Tuple2[?,?]
)).
The Functor
category involves a single operation, named map
:
def map[A, B](fa: F[A])(f: A => B): F[B]
This method takes a function A => B
and turns an F[A]
into an
F[B]
. The name of the method map
should remind you of the map
method that exists on many classes in the Scala standard library, for
example:
Option(1).map(_ + 1) List(1,2,3).map(_ + 1) Vector(1,2,3).map(_.toString)
We can trivially create a Functor
instance for a type which has a well
behaved map
method:
import cats._ implicit val optionFunctor: Functor[Option] = new Functor[Option] { def map[A,B](fa: Option[A])(f: A => B) = fa map f } implicit val listFunctor: Functor[List] = new Functor[List] { def map[A,B](fa: List[A])(f: A => B) = fa map f }
However, functors can also be created for types which don't have a map
method. For example, if we create a Functor
for Function1[In, ?]
we can use andThen
to implement map
:
implicit def function1Functor[In]: Functor[Function1[In, ?]] = new Functor[Function1[In, ?]] { def map[A,B](fa: In => A)(f: A => B): Function1[In,B] = fa andThen f }
This example demonstrates the use of the
kind-projector compiler plugin
This compiler plugin can help us when we need to change the number of type
holes. In the example above, we took a type which normally has two type holes,
Function1[?,?]
and constrained one of the holes to be the In
type,
leaving just one hole for the return type, resulting in Function1[In,?]
.
Without kind-projector, we'd have to write this as something like
({type F[A] = Function1[In,A]})#F
, which is much harder to read and understand.
The identity monad can be seen as the ambient monad that encodes the effect of having no effect.
The identity monad can be seen as the ambient monad that encodes the
effect of having no effect. It is ambient in the sense that plain pure
values are values of Id
.
It is encoded as:
type Id[A] = A
That is to say that the type Id[A] is just a synonym for A. We can
freely treat values of type A
as values of type Id[A]
, and
vice-versa.
import cats._ val x: Id[Int] = 1 val y: Int = x
Monad
extends the Applicative
type class with a
new function flatten
.
Monad
extends the Applicative
type class with a
new function flatten
. Flatten takes a value in a nested context (eg.
F[F[A]]
where F is the context) and "joins" the contexts together so
that we have a single context (ie. F[A]
).
Monoid
extends the Semigroup
type class, adding an
empty
method to semigroup's combine
.
Monoid
extends the Semigroup
type class, adding an
empty
method to semigroup's combine
. The empty
method must return a
value that when combined with any other instance of that type returns the
other instance, i.e.
(combine(x, empty) == combine(empty, x) == x)
For example, if we have a Monoid[String]
with combine
defined as string
concatenation, then empty = ""
.
Having an empty
defined allows us to combine all the elements of some
potentially empty collection of T
for which a Monoid[T]
is defined and
return a T
, rather than an Option[T]
as we have a sensible default to
fall back to.
A semigroup for some given type A has a single operation
(which we will call combine
), which takes two values of type A, and
returns a value of type A.
A semigroup for some given type A has a single operation
(which we will call combine
), which takes two values of type A, and
returns a value of type A. This operation must be guaranteed to be
associative. That is to say that:
((a combine b) combine c)
must be the same as
(a combine (b combine c))
for all possible values of a,b,c.
There are instances of Semigroup
defined for many types found in the
scala common library. For example, Int
values are combined using addition
by default but multiplication is also associative and forms another Semigroup
.
import cats.Semigroup
In functional programming it is very common to encode "effects" as data types - common effects
include Option
for possibly missing values, Xor
and Validated
for possible errors, and
Future
for asynchronous computations.
In functional programming it is very common to encode "effects" as data types - common effects
include Option
for possibly missing values, Xor
and Validated
for possible errors, and
Future
for asynchronous computations.
These effects tend to show up in functions working on a single piece of data - for instance
parsing a single String
into an Int
, validating a login, or asynchronously fetching website
information for a user.
import cats.data.Xor import scala.concurrent.Future def parseInt(s: String): Option[Int] = ??? trait SecurityError trait Credentials def validateLogin(cred: Credentials): Xor[SecurityError, Unit] = ??? trait Profile trait User def userInfo(user: User): Future[Profile] = ???
Each function asks only for the data it actually needs; in the case of userInfo
, a single User
. We
certainly could write one that takes a List[User]
and fetch profile for all of them, would be a bit strange.
If we just wanted to fetch the profile of just one user, we would either have to wrap it in a List
or write
a separate function that takes in a single user anyways. More fundamentally, functional programming is about
building lots of small, independent pieces and composing them to make larger and larger pieces - does this
hold true in this case?
Given just User => Future[Profile]
, what should we do if we want to fetch profiles for a List[User]
?
We could try familiar combinators like map
.
def profilesFor(users: List[User]) = users.map(userInfo)
Note the return type List[Future[Profile]]
. This makes sense given the type signatures, but seems unwieldy.
We now have a list of asynchronous values, and to work with those values we must then use the combinators on
Future
for every single one. It would be nicer instead if we could get the aggregate result in a single
Future
, say a Future[List[Profile]]
.
As it turns out, the Future
companion object has a traverse
method on it. However, that method is
specialized to standard library collections and Future
s - there exists a much more generalized form
that would allow us to parse a List[String]
or validate credentials for a List[User]
.
Enter Traverse
.
At center stage of Traverse
is the traverse
method.
trait Traverse[F[_]] { def traverse[G[_] : Applicative, A, B](fa: F[A])(f: A => G[B]): G[F[B]] }
In our above example, F
is List
, and G
is Option
, Xor
, or Future
. For the profile example,
traverse
says given a List[User]
and a function User => Future[Profile]
, it can give you a
Future[List[Profile]]
.
Abstracting away the G
(still imagining F
to be List
), traverse
says given a collection of data,
and a function that takes a piece of data and returns an effectful value, it will traverse the collection,
applying the function and aggregating the effectful values (in a List
) as it goes.
In the most general form, F[_]
is some sort of context which may contain a value (or several). While
List
tends to be among the most general cases, there also exist Traverse
instances for Option
,
Xor
, and Validated
(among others).
Imagine you are filling out a web form to signup for an account.
Imagine you are filling out a web form to signup for an account. You input your username and password and submit. Response comes back saying your username can't have dashes in it, so you make some changes and resubmit. Can't have special characters either. Change, resubmit. Passwords need to have at least one capital letter. Change, resubmit. Password needs to have at least one number.
Or perhaps you're reading from a configuration file. One could imagine the configuration library you're using returns
a scala.util.Try
, or maybe a scala.util.Either
(or cats.data.Xor
). Your parsing may look something like:
case class ConnectionParams(url: String, port: Int) for { url <- config[String]("url") port <- config[Int]("port") } yield ConnectionParams(url, port)
You run your program and it says key "url" not found, turns out the key was "endpoint". So you change your code and re-run. Now it says the "port" key was not a well-formed integer.
It would be nice to have all of these errors be reported simultaneously. That the username can't have dashes can be validated separately from it not having special characters, as well as from the password needing to have certain requirements. A misspelled (or missing) field in a config can be validated separately from another field not being well-formed.
Enter Validated
.
Our goal is to report any and all errors across independent bits of data. For instance, when we ask for several pieces of configuration, each configuration field can be validated separately from one another. How then do we enforce that the data we are working with is independent? We ask for both of them up front.
As our running example, we will look at config parsing. Our config will be represented by a
Map[String, String]
. Parsing will be handled by a Read
type class - we provide instances
just for String
and Int
for brevity.
trait Read[A] { def read(s: String): Option[A] } object Read { def apply[A](implicit A: Read[A]): Read[A] = A implicit val stringRead: Read[String] = new Read[String] { def read(s: String): Option[String] = Some(s) } implicit val intRead: Read[Int] = new Read[Int] { def read(s: String): Option[Int] = if (s.matches("-?[0-9]+")) Some(s.toInt) else None } }
Then we enumerate our errors - when asking for a config value, one of two things can go wrong: the field is missing, or it is not well-formed with regards to the expected type.
sealed abstract class ConfigError final case class MissingConfig(field: String) extends ConfigError final case class ParseError(field: String) extends ConfigError
We need a data type that can represent either a successful value (a parsed configuration),
or an error. It'd look like in the following example, which cats provides in cats.data.Validated
.
sealed abstract class Validated[+E, +A] object Validated { final case class Valid[+A](a: A) extends Validated[Nothing, A] final case class Invalid[+E](e: E) extends Validated[E, Nothing] }
Now we are ready to write our parser.
import cats.data.Validated import cats.data.Validated.{Invalid, Valid} case class Config(map: Map[String, String]) { def parse[A : Read](key: String): Validated[ConfigError, A] = map.get(key) match { case None => Invalid(MissingConfig(key)) case Some(value) => Read[A].read(value) match { case None => Invalid(ParseError(key)) case Some(a) => Valid(a) } } }
Everything is in place to write the parallel validator. Recall that we can only do parallel validation if each piece is independent. How do we enforce the data is independent? By asking for all of it up front. Let's start with two pieces of data.
def parallelValidate[E, A, B, C](v1: Validated[E, A], v2: Validated[E, B])(f: (A, B) => C): Validated[E, C] = (v1, v2) match { case (Valid(a), Valid(b)) => Valid(f(a, b)) case (Valid(_), i@Invalid(_)) => i case (i@Invalid(_), Valid(_)) => i case (Invalid(e1), Invalid(e2)) => ??? }
We've run into a problem. In the case where both have errors, we want to report both. But we have
no way of combining the two errors into one error! Perhaps we can put both errors into a List
,
but that seems needlessly specific - clients may want to define their own way of combining errors.
How then do we abstract over a binary operation? The Semigroup
type class captures this idea.
import cats.Semigroup def parallelValidate[E : Semigroup, A, B, C](v1: Validated[E, A], v2: Validated[E, B])(f: (A, B) => C): Validated[E, C] = (v1, v2) match { case (Valid(a), Valid(b)) => Valid(f(a, b)) case (Valid(_), i@Invalid(_)) => i case (i@Invalid(_), Valid(_)) => i case (Invalid(e1), Invalid(e2)) => Invalid(Semigroup[E].combine(e1, e2)) }
Perfect! But.. going back to our example, we don't have a way to combine ConfigError
s. But as clients,
we can change our Validated
values where the error can be combined, say, a List[ConfigError]
. It is
more common however to use a NonEmptyList[ConfigError]
- the NonEmptyList
statically guarantees we
have at least one value, which aligns with the fact that if we have an Invalid
, then we most
certainly have at least one error. This technique is so common there is a convenient method on Validated
called toValidatedNel
that turns any Validated[E, A]
value to a Validated[NonEmptyList[E], A]
.
Additionally, the type alias ValidatedNel[E, A]
is provided.
Time to parse.
import cats.SemigroupK import cats.data.NonEmptyList import cats.std.list._ implicit val nelSemigroup: Semigroup[NonEmptyList[ConfigError]] = SemigroupK[NonEmptyList].algebra[ConfigError] implicit val readString: Read[String] = Read.stringRead implicit val readInt: Read[Int] = Read.intRead
In day-to-day programming, it is fairly common to find ourselves writing functions that can fail.
In day-to-day programming, it is fairly common to find ourselves writing functions that can fail. For instance, querying a service may result in a connection issue, or some unexpected JSON response.
To communicate these errors it has become common practice to throw exceptions. However, exceptions are not tracked in any way, shape, or form by the Scala compiler. To see what kind of exceptions (if any) a function may throw, we have to dig through the source code. Then to handle these exceptions, we have to make sure we catch them at the call site. This all becomes even more unwieldy when we try to compose exception-throwing procedures.
val throwsSomeStuff: Int => Double = ??? val throwsOtherThings: Double => String = ??? val moreThrowing: String => List[Char] = ??? val magic = throwsSomeStuff.andThen(throwsOtherThings).andThen(moreThrowing)
Assume we happily throw exceptions in our code. Looking at the types, any of those functions
can throw any number of exceptions, we don't know. When we compose, exceptions from any of
the constituent functions can be thrown. Moreover, they may throw the same kind of exception
(e.g. IllegalArgumentException
) and thus it gets tricky tracking exactly where that
exception came from.
How then do we communicate an error? By making it explicit in the data type we return.
Xor
vs Validated
In general, Validated
is used to accumulate errors, while Xor
is used to short-circuit a computation upon the first error. For more information, see the Validated
vs Xor
section of the Validated
documentation.
Either
Xor
is very similar to scala.util.Either
- in fact, they are *isomorphic* (that is,
any Either
value can be rewritten as an Xor
value, and vice versa).
sealed abstract class Xor[+A, +B] object Xor { final case class Left[+A](a: A) extends Xor[A, Nothing] final case class Right[+B](b: B) extends Xor[Nothing, B] }
Just like Either
, it has two type parameters. Instances of Xor
either hold a value
of one type parameter, or the other. Why then does it exist at all?
Taking a look at Either
, we notice it lacks flatMap
and map
methods. In order to map
over an Either[A, B]
value, we have to state which side we want to map over. For example,
if we want to map Either[A, B]
to Either[A, C]
we would need to map over the right side.
This can be accomplished by using the Either#right
method, which returns a RightProjection
instance. RightProjection
does have flatMap
and map
on it, which acts on the right side
and ignores the left - this property is referred to as "right-bias."
val e1: Either[String, Int] = Right(5) e1.right.map(_ + 1) val e2: Either[String, Int] = Left("hello") e2.right.map(_ + 1)
Note the return types are themselves back to Either
, so if we want to make more calls to
flatMap
or map
then we again must call right
or left
.
Applicative
extendsApply
by adding a single method,pure
:def pure[A](x: A): F[A]