Functional Programming Abstractions & Cats
NonEmptyList, NonEmptyChain, Validated, Ior, EitherT, OptionT, Kleisli (reader), State, Writer, Reader, Const, Chain, Nested.
31.1 Core abstractions
Laws / meaning combine(x, y) identity, composition Apply / Applicative[F] ap, pure, mapN flatMap, pure sequencing, left/right identity, associativity foldLeft, foldMap traverse, sequence MonadError[F, E] raiseError, handleErrorWith Eq, Order, Show, Hash type-safe equality/order/printing
31.2 Defining them yourself
trait Monoid[A]:
def empty: A
extension (x: A) def |+|(y: A): A
given Monoid[Int] with
def empty = 0
extension (x: Int) def |+|(y: Int) = x + y
given [A]: Monoid[List[A]] with
def empty = Nil
extension (x: List[A]) def |+|(y: List[A]) = x ++ y
def combineAll[A](xs: List[A])(using m: Monoid[A]): A = xs.foldLeft(m.empty)(_ |+| _)
trait Monad[F[_]]:
def pure[A](a: A): F[A]
extension [A](fa: F[A])
def flatMap[B](f: A => F[B]): F[B]
def map[B](f: A => B): F[B] = fa.flatMap(a => pure(f(a)))31.3 Cats in practice
//> using dep org.typelevel::cats-core:2.13.0
import cats.*
import cats.syntax.all.*
import cats.data.*
1 |+| 2 // 3
Map("a" -> 1) |+| Map("a" -> 2, "b" -> 3) // Map(a -> 3, b -> 3)
(Option(1), Option(2)).mapN(_ + _) // Some(3)
List(Option(1), Option(2)).sequence // Some(List(1, 2))
List("1", "2", "x").traverse(_.toIntOption) // None
1.some, none[Int], "err".asLeft[Int], 5.asRight[String]
Eq[Int].eqv(1, 1); 1 === 131.4 Error accumulation with Validated
type V[A] = ValidatedNel[String, A]
def validName(n: String): V[String] = if n.nonEmpty then n.validNel else "empty name".invalidNel
def validAge(a: Int): V[Int] = if a >= 0 then a.validNel else "negative age".invalidNel
(validName(""), validAge(-1)).mapN(User(_, "x@y", _))
// Invalid(NonEmptyList(empty name, negative age)) — all errors reported31.5 Data types in cats.data
NonEmptyList, NonEmptyChain, Validated, Ior, EitherT, OptionT, Kleisli (reader), State, Writer, Reader, Const, Chain, Nested.
val program: State[Int, String] = for
a <- State.get[Int]
_ <- State.set(a + 1)
b <- State.get[Int]
yield s"was $a, now $b"
program.run(10).value // (11, "was 10, now 11")
// Monad transformers
def findUser(id: Int): Future[Option[User]] = ???
def findOrders(u: User): Future[Option[List[Order]]] = ???
val result: OptionT[Future, List[Order]] = for
u <- OptionT(findUser(1))
o <- OptionT(findOrders(u))
yield o31.6 Tagless final
Program against abstract F[_]; choose IO, ZIO, or Id (for tests) at the edge.
trait UserRepo[F[_]]:
def find(id: Int): F[Option[User]]
class UserService[F[_]: Monad](repo: UserRepo[F]):
def nameOf(id: Int): F[String] = repo.find(id).map(_.fold("unknown")(_.name))31.7 Free monads (overview)
Represent programs as data (Free[Algebra, A]), interpret later with natural transformations (~>). Powerful but heavy — tagless final is more common today.