Package

catslib

Permalink

package catslib

Visibility
  1. Public
  2. All

Value Members

  1. object ApplicativeSection extends FlatSpec with Matchers with Section

    Permalink

    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

    Permalink
  3. object ApplySection extends FlatSpec with Matchers with Section

    Permalink

    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

    Permalink

    Cats is an experimental library intended to provide abstractions for functional programming in Scala.

  5. object FoldableSection extends FlatSpec with Matchers with Section

    Permalink

    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.

  6. object FunctorSection extends FlatSpec with Matchers with Section

    Permalink

    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.

  7. object IdentitySection extends FlatSpec with Matchers with Section

    Permalink

    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
  8. object MonadHelpers

    Permalink
  9. object MonadSection extends FlatSpec with Matchers with Section

    Permalink

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

  10. object MonoidHelpers

    Permalink
  11. object MonoidSection extends FlatSpec with Matchers with Section

    Permalink

    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.

  12. object SemigroupSection extends FlatSpec with Matchers with Section

    Permalink

    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
  13. object TraverseHelpers

    Permalink
  14. object TraverseSection extends FlatSpec with Matchers with Section

    Permalink

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

  15. object ValidatedHelpers

    Permalink
  16. object ValidatedSection extends FlatSpec with Matchers with Section

    Permalink

    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.

    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 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 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.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
  17. object XorSection extends FlatSpec with Matchers with Section

    Permalink

    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.

    Why not 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.

  18. object XorStyle

    Permalink
  19. object XorStyleWithAdts

    Permalink

Ungrouped