p

catslib

package catslib

Ordering
  1. Alphabetic
Visibility
  1. Public
  2. Protected

Value Members

  1. object ApplicativeSection extends AnyFlatSpec with Matchers with Section

    Applicative extends Apply by adding a single method, pure:

    Applicative extends Apply by adding a single method, pure:

    def pure[A](x: A): F[A]
  2. object ApplyHelpers
  3. object ApplySection extends AnyFlatSpec with Matchers with Section

    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)
    }
  4. object CatsLibrary extends Library

    Cats is a library which provides abstractions for functional programming in the Scala programming language.

  5. 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 vs Validated

    In general, Validated is used to accumulate errors, while Either is used to short-circuit a computation upon the first error. For more information, see the [Validated vs Either](https://typelevel.org/cats/datatypes/validated.html#validated-vs-either) section of the Validated documentation.

  6. object EitherStyle
  7. object EitherStyleWithAdts
  8. 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.

  9. 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 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.

  10. 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 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)

    Creating Functor instances

    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.

  11. 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 type Id[A], and vice-versa.

    import cats._
    
    val x: Id[Int] = 1
    val y: Int = x
  12. object MonadHelpers
  13. object MonadSection extends AnyFlatSpec with Matchers with Section

    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]).

  14. object MonoidSection extends AnyFlatSpec with Matchers with Section

    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.

  15. 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.

  16. 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 another Semigroup.

    import cats.Semigroup
  17. object TraverseHelpers
  18. 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 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, Either 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 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 single User. We certainly could write one that takes a List[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 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]): 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 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 Futures - 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.

    The type class

    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, Either, 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, Either, and Validated (among others).

  19. object ValidatedHelpers
  20. 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 a scala.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 a Read type class - we provide instances only 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 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 ConfigErrors. 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.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

Ungrouped