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 === 1

31.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 reported

31.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 o

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