class: center, middle
cats
![](./cats.png) Mark de Jong --- # whoami?
## Mark de Jong - Freelance software consultant - Functional programming enthusiast ### Website [http://vectos.net](http://vectos.net) --- # Agenda - Anatomy of a Type Class - Basic type classes - Type classes which describe effects - Data types inside cats --- # Anatomy of a Type Class - why ? - In functional programming we separate data and behavior - Common behaviour is encoded in a type class - Add behavior without extending (ad-hoc polymorphism/Open-Closed principle) - Write polymorphic methods with type class constraints - Create type class _instances_ with type class constraints --- # Anatomy of a Type Class - definition A trait/interface which describes some behavior for a type ```scala // The "serialize to JSON" behaviour is encoded in this trait // This is our type class trait JsonWriter[A] { def write(value: A): Json } // Define a very simple JSON AST sealed trait Json case class JsObject(get: Map[String, Json]) extends Json case class JsString(get: String) extends Json case class JsNumber(get: Double) extends Json case object JsNull extends Json ``` --- # Anatomy of a Type Class - instances ```scala case class Person(name: String, email: String) object JsonWriterInstances { implicit val stringWriter: JsonWriter[String] = new JsonWriter[String] { def write(value: String): Json = JsString(value) } implicit val personWriter: JsonWriter[Person] = new JsonWriter[Person] { def write(value: Person): Json = JsObject(Map( "name" -> JsString(value.name), "email" -> JsString(value.email) )) } } ``` --- # Anatomy of a Type Class - use We can write polymorphic methods with constraints ```scala object Json { def toJson[A](value: A)(implicit w: JsonWriter[A]): Json = w.write(value) } import JsonWriterInstances._ Json.toJson(Person("Dave", "dave@example.com")) // res0: Json = JsObject( // get = Map( // "name" -> JsString(get = "Dave"), // "email" -> JsString(get = "dave@example.com") // ) // ) ``` --- # Anatomy of a Type Class - apply syntax Most type classes in cats have defined a specialized apply like this ```scala object JsonWriter { def apply[A](implicit ev: JsonWriter[A]): JsonWriter[A] = ev } ``` Remember `def toJson[A](value: A)(implicit w: JsonWriter[A]): Json` ? Also using that constraint. --- # Anatomy of a Type Class - instances with constraints Create type class instance with constraints ```scala implicit def optionWriter[A : JsonWriter]: JsonWriter[Option[A]] = new JsonWriter[Option[A]] { def write(value: Option[A]): Json = value match { case Some(v) => JsonWriter[A].write(v) case None => JsNull } } Json.toJson(Option("Test")) // res1: Json = JsString(get = "Test") Json.toJson(Option.empty[String]) // res2: Json = JsNull ``` --- # Anatomy of a Type Class - implicitly Debug implicits by using `implicitly` ```scala import JsonWriterInstances._ implicitly[JsonWriter[String]] // res3: JsonWriter[String] = repl.MdocSession$App$JsonWriterInstances$$anon$1@6ef58fe1 ``` ```scala implicitly[JsonWriter[Int]] // error: could not find implicit value for parameter e: repl.MdocSession.App.JsonWriter[Int] // implicitly[JsonWriter[Int]] // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``` --- # Anatomy of a Type Class - summary Instances can be defined at - Companion object of the type class - Companion object of the type you want to summon it for - Objects/trait and you have to explicitly import those Debug it with missing instances with - implicitly - cmd + shift + p in IntelliJ IDEA --- # Let's use cats Get started usually with some imports! ```scala // Contains the basic type import cats._ // Contains the type class instances and syntax import cats.implicits._ // Contains data type like NonEmptyList, Kleisli, etc import cats.data._ ``` --- # Basic type classes - Show A type class for printing strings. `toString` can be dangerous to use everywhere! You might leak data. Also this gives us `Show` instances for _refined_ types. ```scala trait Show[A] { def show(value: A): String } ``` ```scala Show[Int].show(1) // res5: String = "1" Show[Double].show(123.0333d) // res6: String = "123.0333" ``` --- # Basic type classes - Eq A type class for equality. Better alternative to `==`, note there is `===` syntax available via `import cats.implicits._` ```scala trait Eq[A] { def eq(x: A, y: A): Boolean } ``` ```scala Eq[Int].eqv(1, 1) // res7: Boolean = true "abc" === "dec" // res8: Boolean = false "abc" =!= "abcc" // res9: Boolean = true ``` --- # Basic type classes - Order A type class for order, liking `Ordering`. ```scala trait Order[A] extends Eq[A] { def compare(x: A, y: A): Int def eqv(x: A, y: A): Boolean = compare(x, y) == 0 } ``` ```scala import scala.concurrent.duration._ Order[Int].compare(1, 2) // res10: Int = -1 Order[Int].compare(2, 1) // res11: Int = 1 Order[Int].compare(1, 1) // res12: Int = 0 // syntax from cats.implicits 1.second > 2.seconds // res13: Boolean = false 1.days <= 2.days // res14: Boolean = true ``` --- # Basic type classes - Semigroup A type class for adding things together ```scala trait Semigroup[A] { def combine(x: A, y: A): A } ``` ```scala // syntax for `combine` 1.second |+| 2.seconds // res15: FiniteDuration = 3 seconds "abc" |+| "def" // res16: String = "abcdef" 1 |+| 3 // res17: Int = 4 List(1, 2, 3) |+| List(4, 5, 6) // res18: List[Int] = List(1, 2, 3, 4, 5, 6) ``` --- # Basic type classes - Monoid A semigroup extended with a identity element ```scala trait Monoid[A] extends Semigroup[A] { val empty: A } ``` ```scala // syntax for `combine` 1.second |+| Monoid[FiniteDuration].empty // res19: FiniteDuration = 1 second "abc" |+| Monoid[String].empty // res20: String = "abc" 1 |+| Monoid[Int].empty // res21: Int = 1 List(1, 2, 3) |+| Monoid[List[Int]].empty // res22: List[Int] = List(1, 2, 3) ``` --- # Semigroup (combine data) ```scala import java.util.Currency case class Money(repr: Map[Currency, Double]) object Money { def single(amount: Double, currency: Currency): Money = Money(Map(currency -> amount)) implicit val semigroup: Semigroup[Money] = new Semigroup[Money] { def combine(x: Money, y: Money): Money = Money(x.repr |+| y.repr) } } def usd = Currency.getInstance("USD") Money.single(2d, usd) |+| Money.single(4d, usd) // res23: Money = Money(repr = Map(USD -> 6.0)) ``` --- # Semigroup (combine effects) ```scala trait Mailer[F[_]] { self => def mail(msg: String): F[Unit] def and(other: Mailer[F])(implicit F: Applicative[F]): Mailer[F] = new Mailer[F] { def mail(msg: String): F[Unit] = self.mail(msg) *> other.mail(msg) } } object Mailer { implicit def semigroup[F[_] : Applicative]: Semigroup[Mailer[F]] = new Semigroup[Mailer[F]] { def combine(x: Mailer[F], y: Mailer[F]): Mailer[F] = x and y } } ``` --- # Basic type classes - Semigroup/Monoid Semigroup instances are also defined for other types. Like `Option`, `Future`, `ConnectionIO`, `DBIO`, `ZIO`, `ZManaged` etc ```scala implicit def optionSemigroup[A : Semigroup]: Semigroup[Option[A]] = new Semigroup[Option[A]] { def combine(x: Option[A], y: Option[A]): Option[A] = for { a <- x b <- y } yield Semigroup[A].combine(a, b) } ``` ```scala Option(Money.single(2d, usd)) |+| Option(Money.single(2d, usd)) // res24: Option[Money] = Some(value = Money(repr = Map(USD -> 4.0))) ``` --- # Basic type classes - Hierarchy Typeclasses extend from each other like seen with - Eq and Order - Semigroup and Monoid. For effects this is described in the hierarchy like seen here. ![](./hierarchy.png) --- # Basic type classes - Functor Transform a effect ![](./functor.svg) ```scala trait Functor[F[_]] { def map[A, B](fa: F[A])(f: A => B): F[B] } ``` ```scala Functor[Option].map(Some(2))(_ * 2) // res25: Option[Int] = Some(value = 4) Functor[List].map(List(1, 2 , 3))(_ * 2) // res26: List[Int] = List(2, 4, 6) ``` --- # Basic type classes - Apply To run computations in parallel (not dependant on eachother) ```scala trait Apply[F[_]] extends Functor[F] { def ap[A, B](ff: F[A => B])(fa: F[A]): F[B] } ``` ```scala val someF: Option[Int => Int] = Some(_ * 2) // someF: Option[Int => Int] = Some(value =
) Apply[Option].ap(someF)(Some(2)) // res27: Option[Int] = Some(value = 4) ``` --- # Basic type classes - Applicative Extends `Apply` to lift a value of context `F[_]` ```scala trait Applicative[F[_]] extends Apply[F] { def pure[A](a: A): F[A] final def unit: F[Unit] = pure(()) } ``` ```scala val optTwo = Applicative[Option].pure(2) // optTwo: Option[Int] = Some(value = 2) val optThree = Applicative[Option].pure(3) // optThree: Option[Int] = Some(value = 3) (optTwo, optThree).mapN { _ * _ } // res28: Option[Int] = Some(value = 6) (optTwo, optThree).tupled // res29: Option[(Int, Int)] = Some(value = (2, 3)) ``` --- # Basic type classes - Applicative Instances for - Base types: Option, Either, List, etc - Asynchronous types: Future, ZIO, etc - Cats types: Validated - Doobie: Get (decoder), ConnectionIO - Circe: Decoder --- # Basic type classes - Monad To run computations in sequence (when there is dependencies) ![](./option-monad.svg) ```scala trait Monad[F[_]] extends Applicative[F] { def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] } ``` ```scala for { a <- Option(3) b <- Option.empty[Int] } yield a + b // res30: Option[Int] = None ``` --- # Basic type classes - Monad Instances for - Base types: Option, Either, List, etc - Asynchronous types: Future, ZIO, etc - Cats types: Writer, Reader, State, etc - Doobie: ConnectionIO - Circe: Decoder --- # Data types inside cats There are a few data types in cats, like in the std library of scala: - `NonEmptyList` - list which cannot be empty - `NonEmptySet` - set which cannot be empty - `NonEmptyMap` - map which cannot be empty - `Validated` - variant of either where, the left side needs to ba `Semigroup` - `EitherT` - a effectful version of `Either` - `OptionT` - a effectful version of `Option` - `Kleisli` or `ReaderT` - a effectful function `A => F[B]` - `StateT` - model stateful computations `S => (S, A)` - `WriterT` - log computations `(L, A)` where `L` is a `Monoid` --- # NonEmptyList example NonEmptyXXX variants are non empty ```scala // construct a NonEmptyList val nel = NonEmptyList.of("123", "abc") // nel: NonEmptyList[String] = NonEmptyList(head = "123", tail = List("abc")) // safe head operation nel.head // res31: String = "123" // from a dynamic list, this yields an `Option` NonEmptyList.fromList(List.empty[String]) // res32: Option[NonEmptyList[String]] = None ``` --- # Validated example Parallel accumulation of errors using `Applicative` ```scala case class RegistrationData(username: String, age: Int) sealed trait ValidationError case object UsernameHasSpecialCharacters extends ValidationError case object AgeIsInvalid extends ValidationError // ValidatedNel[ValidationError, A] is an alias // for `Validated[NonEmptyList[ValidationError], A]` type ValidationResult[A] = ValidatedNel[ValidationError, A] def validateUserName(userName: String): ValidationResult[String] = if (userName.matches("^[a-zA-Z0-9]+$")) userName.validNel else UsernameHasSpecialCharacters.invalidNel def validateAge(age: Int): ValidationResult[Int] = if (age >= 18 && age <= 75) age.validNel else AgeIsInvalid.invalidNel (validateUserName("#"), validateAge(3)).mapN(RegistrationData) // res33: ValidationResult[RegistrationData] = Invalid( // e = NonEmptyList( // head = UsernameHasSpecialCharacters, // tail = List(AgeIsInvalid) // ) // ) ``` --- # EitherT `EitherT[F[_], L, R]` is just a `F[Either[L, R]]`, it's an `Either` wrapped in a monadic context. - Where `F[_]` is a `Monad` - Where `L` is the left side and `R` is the right side - `EitherT` has a `Monad` and `Semigroup` instance available ```scala import scala.concurrent._ import scala.concurrent.duration._ implicit def ec: ExecutionContext = scala.concurrent.ExecutionContext.global def fromEitherF[L, R](either: Future[Either[L, R]]): EitherT[Future, L, R] = EitherT(either) def fromOptionF[L, A](opt: Future[Option[A]], ifEmpty: => L): EitherT[Future, L, A] = EitherT(opt.map(_.fold[Either[L, A]](Left(ifEmpty))(Right(_)))) def programEither: EitherT[Future, String, Int] = for { a <- fromEitherF(Future.successful(Right(1))) b <- fromOptionF(Future.successful(Option.empty[Int]), "Oh oh") } yield a + b Await.ready(programEither.value, 2.seconds) // res34: Future[Either[String, Int]] = Future(Success(Left(Oh oh))) ``` --- # OptionT `OptionT[F[_], A]` is just a `F[Option[A]]`, it's an `Option` wrapped in a monadic context. - Where `F[_]` is a `Monad` - `OptionT` has a `Monad` and `Semigroup` instance available ```scala def programOpt = OptionT(Future.successful(Option.empty[Int])) orElse OptionT(Future.successful(Option(1))) Await.ready(programOpt.value, 2.seconds) // res35: Future[Option[Int]] = Future(Success(Some(1))) ``` --- # Kleisli A `Kleisli` represents a function `A => F[B]`. Where `A` is the environment. This is seen in ZIO as well, but can also serve other applications. Like in http4s or seen here: ```scala def parse: Kleisli[Option,String,Int] = Kleisli((s: String) => if (s.matches("-?[0-9]+")) Some(s.toInt) else None) def reciprocal: Kleisli[Option,Int,Double] = Kleisli((i: Int) => if (i != 0) Some(1.0 / i) else None) def parseAndReciprocal: Kleisli[Option,String,Double] = reciprocal.compose(parse) parseAndReciprocal.run("2") // res36: Option[Double] = Some(value = 0.5) ``` --- # The Identity Monad The identity monad is defined as: ```scala type Id[A] = A val idMonad = new Monad[Id] { def pure[A](x: A): Id[A] = x def flatMap[A, B](fa: Id[A])(f: A => Id[B]): Id[B] = f(fa) } ``` --- # ReaderT and Reader A `ReaderT` is an alias ```scala type ReaderT[F[_], A, B] = Kleisli[F, A, B] ``` And `Reader` is a `ReaderT`, omitting the effectul part. ```scala type Reader[A, B] = ReaderT[Id, A, B] ``` `ReaderT` is being used in ZIO for the environment. --- # StateT ```scala final case class StateT[F[_], S, A](run: S => F[(S, A)]) ``` It's basically a `S => (S, A)` - Where `S` is the state and `A` the return type. - This can be used to model purely stateful programs without using `var`. - Examples are interpreters or in-memory implementations of repositories. - `StateT` has a `Monad` and `Semigroup` instances as well, and an alias `State` which uses `Id` --- # State - sample ```scala final case class Data( depots: List[Depot], couriers: List[Courier] ) object Database { def all[A](at: Lens[Data, List[A]]): State[Data, List[A]] = State.get.map(at.get) def find[A, B](at: Lens[Data, List[A]])(f: A => Boolean): State[Data, Option[A]] = all(at).map(_.find(f)) } def findByCode(depotCode: DepotCode): State[Data, Option[Depot]] = Database.find(Data.depots)(_.depotCode === depotCode) ``` --- # WriterT ```scala final case class WriterT[F[_], L, A](run: F[(L, A)]) ``` It's basically a `(L, A)` - Where `L` is the log type and `A` the return type. - The `L` is constrained to have a `Monoid` instance. - Applications are to traverse trees of data and capture data along the way - Or log data while running effects. - `WriterT` has a `Monad` and `Semigroup` instances as well, and an alias `Writer` which uses `Id` --- # There is more, read the book and learn! ![](./book.png) [https://www.scalawithcats.com/](https://www.scalawithcats.com/) --- # Fin ### Credits - Illustrations from [scala with cats](https://www.scalawithcats.com/) ### Slides [http://fristi.github.com/cats-deck](http://fristi.github.com/cats-deck) ### Libraries mentioned - cats at [https://github.com/typelevel/cats](https://github.com/typelevel/cats)