catslib
package catslib
- Alphabetic
- Public
- Protected
Value Members
- object ApplicativeSection extends AnyFlatSpec with Matchers with Section
Applicative
extendsApply
by adding a single method,pure
:Applicative
extendsApply
by adding a single method,pure
:def pure[A](x: A): F[A]
- object ApplyHelpers
- object ApplySection extends AnyFlatSpec with Matchers with Section
Apply
extends theFunctor
type class (which features the familiarmap
function) with a new functionap
.Apply
extends theFunctor
type class (which features the familiarmap
function) with a new functionap
. Theap
function is similar tomap
in that we are transforming a value in a context (a context being theF
inF[A]
; a context can beOption
,List
orFuture
for example). However, the difference betweenap
andmap
is that forap
the function that takes care of the transformation is of typeF[A => B]
, whereas formap
it isA => B
:Here are the implementations of
Apply
for theOption
andList
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) }
- object CatsLibrary extends Library
Cats is a library which provides abstractions for functional programming in the Scala programming language.
- object EitherSection extends AnyFlatSpec with Matchers with Section
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.
Either
vsValidated
In general,
Validated
is used to accumulate errors, whileEither
is used to short-circuit a computation upon the first error. For more information, see the [Validated
vsEither
](https://typelevel.org/cats/datatypes/validated.html#validated-vs-either) section of theValidated
documentation. - object EitherStyle
- object EitherStyleWithAdts
- object EvalSection extends AnyFlatSpec with Matchers with Section
Eval is a data type for controlling synchronous evaluation.
Eval is a data type for controlling synchronous evaluation. Its implementation is designed to provide stack-safety at all times using a technique called trampolining. There are two different factors that play into evaluation: memoization and laziness. Memoized evaluation evaluates an expression only once and then remembers (memoizes) that value. Lazy evaluation refers to when the expression is evaluated. We talk about eager evaluation if the expression is immediately evaluated when defined and about lazy evaluation if the expression is evaluated when it’s first used. For example, in Scala, a lazy val is both lazy and memoized, a method definition def is lazy, but not memoized, since the body will be evaluated on every call. A normal val evaluates eagerly and also memoizes the result. Eval is able to express all of these evaluation strategies and allows us to chain computations using its Monad instance.
- object FoldableSection extends AnyFlatSpec with Matchers with Section
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
orSet
), these methods will fold together (combine) the values contained in the collection to produce a single result. Most collection types havefoldLeft
methods, which will usually be used by the associatedFoldable[_]
instance.Foldable[F]
is implemented in terms of two basic methods:foldLeft(fa, b)(f)
eagerly foldsfa
from left-to-right.foldRight(fa, b)(f)
lazily foldsfa
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
andfoldRight
,Foldable
has a number of other useful functions. - object FunctorSection extends AnyFlatSpec with Matchers with Section
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 shapeF[*]
, such asOption
,List
andFuture
. (This is in contrast to a type likeInt
which has no hole, orTuple2
which has two holes (Tuple2[*,*]
)).The
Functor
category involves a single operation, namedmap
:def map[A, B](fa: F[A])(f: A => B): F[B]
This method takes a function
A => B
and turns anF[A]
into anF[B]
. The name of the methodmap
should remind you of themap
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)
Creating Functor instances
We can trivially create a
Functor
instance for a type which has a well behavedmap
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 aFunctor
forFunction1[In, *]
we can useandThen
to implementmap
: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 theIn
type, leaving just one hole for the return type, resulting inFunction1[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. - object IdentitySection extends AnyFlatSpec with Matchers with Section
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 typeId[A]
, and vice-versa.import cats._ val x: Id[Int] = 1 val y: Int = x
- object MonadHelpers
- object MonadSection extends AnyFlatSpec with Matchers with Section
Monad
extends theApplicative
type class with a new functionflatten
.Monad
extends theApplicative
type class with a new functionflatten
. 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]
). - object MonoidSection extends AnyFlatSpec with Matchers with Section
Monoid
extends theSemigroup
type class, adding anempty
method to semigroup'scombine
.Monoid
extends theSemigroup
type class, adding anempty
method to semigroup'scombine
. Theempty
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]
withcombine
defined as string concatenation, thenempty = ""
.Having an
empty
defined allows us to combine all the elements of some potentially empty collection ofT
for which aMonoid[T]
is defined and return aT
, rather than anOption[T]
as we have a sensible default to fall back to. - object OptionTSection extends AnyFlatSpec with Matchers with ScalaFutures with Section
OptionT is a Monad Transformer that has two type parameters F and A.
OptionT is a Monad Transformer that has two type parameters F and A. F is the wrapping Monad and A is type inside Option. As a result, OptionT[F[_], A] is a light wrapper on an F[Option[A]]. As OptionT is also a monad, it can be used in a for-comprehension and be more convenient to work with than using F[Option[A]] directly.
- object SemigroupSection extends AnyFlatSpec with Matchers with Section
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 anotherSemigroup
.import cats.Semigroup
- object TraverseHelpers
- object TraverseSection extends AnyFlatSpec with Matchers with Section
In functional programming it is very common to encode "effects" as data types - common effects include
Option
for possibly missing values,Either
andValidated
for possible errors, andFuture
for asynchronous computations.In functional programming it is very common to encode "effects" as data types - common effects include
Option
for possibly missing values,Either
andValidated
for possible errors, andFuture
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 anInt
, validating a login, or asynchronously fetching website information for a user.import scala.concurrent.Future def parseInt(s: String): Option[Int] = ??? trait SecurityError trait Credentials def validateLogin(cred: Credentials): Either[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 singleUser
. We certainly could write one that takes aList[User]
and fetch the profile for all of them, though it would be a bit strange since fetching a single user would require us to either wrap it in aList
, 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 aList[User]
? We could try familiar combinators likemap
.def profilesFor(users: List[User]): List[Future[Profile]] = 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 onFuture
for every single one. It would be nicer instead if we could get the aggregate result in a singleFuture
, say aFuture[List[Profile]]
.As it turns out, the
Future
companion object has atraverse
method on it. However, that method is specialized to standard library collections andFuture
s - there exists a much more generalized form that would allow us to parse aList[String]
or validate credentials for aList[User]
.Enter
Traverse
.The type class
At center stage of
Traverse
is thetraverse
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
isList
, andG
isOption
,Either
, orFuture
. For the profile example,traverse
says given aList[User]
and a functionUser => Future[Profile]
, it can give you aFuture[List[Profile]]
.Abstracting away the
G
(still imaginingF
to beList
),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 aList
) as it goes.In the most general form,
F[_]
is some sort of context which may contain a value (or several). WhileList
tends to be among the most general cases, there also existTraverse
instances forOption
,Either
, andValidated
(among others). - object ValidatedHelpers
- object ValidatedSection extends AnyFlatSpec with Matchers with Section
Imagine you are filling out a web form to sign up for an account.
Imagine you are filling out a web form to sign up 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 ascala.util.Either
. 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 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
.Parallel validation
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 aRead
type class - we provide instances only forString
andInt
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 would look like the following, 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 ourValidated
values where the error can be combined, say, aList[ConfigError]
. It is more common however to use aNonEmptyList[ConfigError]
—theNonEmptyList
statically guarantees we have at least one value, which aligns with the fact that if we have anInvalid
, then we most certainly have at least one error. This technique is so common there is a convenient method onValidated
calledtoValidatedNel
that turns anyValidated[E, A]
value to aValidated[NonEmptyList[E], A]
. Additionally, the type aliasValidatedNel[E, A]
is provided.Time to parse.
import cats.SemigroupK import cats.data.NonEmptyList import cats.implicits._ implicit val nelSemigroup: Semigroup[NonEmptyList[ConfigError]] = SemigroupK[NonEmptyList].algebra[ConfigError] implicit val readString: Read[String] = Read.stringRead implicit val readInt: Read[Int] = Read.intRead