Skip to content

cats

  • ROT: import cats._; import cats.data._; import cats.implicits
  • All of the following constructs are same:
implicitly[Applicative[Option]].pure(1)
Applicative[Option].pure(1)
1.pure[Option]

Validated

  • prefer not using Validated directly, but instead indirectly via Parallel
  • Validated.condNec(c, a, e): ValidatedNec[E, A]: create a validated based on condition
  • Validated.fromOption(o, e): Validated[E, A]: create validated from an option
  • .toValidatedNec: convert Validated[E, A] to ValidatedNec[E, A]
  • map: transform valid values
  • validateBoth: aggregate valid data
  • flatMap: chain validation rules, fail-fast by reporting the first failure
  • validateEach (traverse): validate all values and return either accumulated error messages or valid values
  • returns, for example, Validated[Seq[A]] instead of Seq[Validated[A]]
  • .parMapN: converts Eithers to Validated, combines with mapN and converts back the result to Either

Show

  • use java style:
implicit val aShow: Show[A] = Show.fromToString

Applicative

Implement ap in terms of map2

def map2(va: Validated[A], Validated[B])(f: (A, B) => C): Validated[C] = ???
def ap(vf: Validated[A => B])(va: Validated[A]): Validated[B] = map2(vf, va)((f, a) => f(a))

Implement map2 in terms of ap

def ap(vf: Validated[A => B])(va: Validated[A]): Validated[B] = ???
def map2(va: Validated[A], Validated[B])(f: (A, B) => C): Validated[C] =
val g: A => B => C = f.curried // = A => B => f(a, b)
val fbc = ap(pure(g))(va)
ap(fbc)(vb)
// ap(ap(pure(f.curried))(va))(vb)
  • Tip: use traverse_ (instead of tranverse) to ignore the result and produce Unit
  • >> is lazy v/s *> is strict

State

  • conceptually, it's a function that takes an initial state and returns an updated state and a value
class State[S, A](val run: S => (S, A))
  • to obtain the next state's value, use run method, whic h returns an Eval, and then access the value: state.run(s).value
  • useful methods:
def get[S]: State[S, S] = State(s => (s, s))  // return current state
def set[S](s: S): State[S, Unit] = State(_ => (s, ()))   // set or overwrite state
def modify[S](f: S => S): State[S, Unit] = State(s => (f(s), ()))   // update state using a function
def inspect[S](f: S => T): State[S, T] = State(s => (s, f(s)))  // apply a function to state without modifying it
  • map: maps the return value from state without affecting the state
  • flatMap: threads the state to the next and passes the return value as the flatMap argument
  • x.flatMap(_ => y) === x >> y
val t1 = State.modify[Int](_ * 2)
val t2 = State.modify[Int](_ * 3)
(t1 >> t1).run(10).value // (60, ())

Eval

useful functions:

def now[A](a: A): Eval[A] = ???        // eager, no memo === val
def later[A](a: => A): Eval[A] = ???   // lazy, memo === lazy val
def always[A](a: => A): Eval[A] = ???  // lazy, no memo === def
def defer[A](a: => Eval[A]) = ???      // defers the computation of Eval

Monad Transformers

  • allow composing monads: ReaderT[Option, AccountRepo, A] === Reader[AccountRepo, Option[A]], combines two monads that a function needs: Option[A] and Reader[AccountRepo, A]
  • They are built inside-out. That is, any T monad-transformer represents the inner monad of the stack. E.g. OptionT[F[_], A] === F[Option[A]]
  • Nested monad transformers
trait AccountRepo
type ErrorOr[A] = Either[String, A]
type ErrorOrOpt[A] = OptionT[ErrorOr, A] // === Either[String, Option[A]]
type AccountOp[A] = ReaderT[ErrorOr, AccountRepo, A]

ReaderT

  • useful methods:
trait AccountRepo
type ErrorOr[A] = Either[String, A]
type AccountOp[A] = ReaderT[ErrorOr, AccountRepo, A]
val dummyRepo: AccountRepo = new AccountRepo()

ReaderT.liftF[ErrorOr, AccountRepo, Int](5.asRight[String]) // lift F[A] -> ReaderT[F, R, A]
ReaderT((_: AccountRepo) => 5.asRight[String]) // construct from F[A]
5.pure[AccountOp]  // A -> ReaderT[F, R, A]
// ReaderT[ErrorOr, R, A] => ReaderT[Option, R, A]
5.pure[AccountOp].mapF {
  case Right(a) => Some(a)
  case Left(e) => None
}
  • monadic methods
5.pure[AccountOp].map(_ + 1) // === 6.pure[AccountOp]
5.pure[AccountOp].flatMap(n => (n+1).pure[AccountOp]) // === 6.pure[AccountOp]

OptionT

type ErrorOrOpt = OptionT[ErrorOr, A] // === ErrorOr[Option[A]] == Either[String, Option[A]]

// useful methods:
5.pure[ErrorOrOpt] // === OptionT(Option(5).asRight[String])
5.pure[ErrorOrOpt].flatMap(n => (n+1).pure[ErrorOrOpt])
5.pure[ErrorOrOpt].subflatMap(n => Option(n+1))  // works only on inner monad (Option)
5.pure[ErrorOrOpt].semiflatMap(n => Right(n+1))  // works on outer monad
5.pure[ErrorOrOpt].flatMapF(n => Right(Some(n+1)))  // similar to flatMap and lift combined

Error Handling

flowchart LR
 A{Error type} --> B{Domain Error}
 B  --> B1[Accumulate]
 B1 --> B11[[Validated]]
 B  --> B2[Fail Fast]
 B2 --> B21[[Either/ADT]]
 A  --> C{Technical Error}
 C  --> C1[Recoverable]
 C1 --> C11[[ IO, raiseError, handleErrorWith ]]
 C  --> C2[Fatal]
 C2 --> C21[[ Fail ]]
  • .handleError(f: (E) => A) converts an exception into success of the same type

Monadic Type Hierarchy

classDiagram
 Semigroup <|-- Monoid
 Functor <|-- Apply
 Apply <|-- Applicative
 class Semigroup{
  combine()
 }
 class Monoid{
  empty()
 }
 class Functor {
  map()
 }
 class Apply {
  ap()
 }
 class Applicative {
  pure()
 }
  • Semigroup: Associative binary operation combine(A, A) => A, A forms Semigroup under combine
  • Monoid: A Semigroup plus an identity element combine(a, id) == a
  • Very useful for any reducing operation in parallel
  • Functor: Any container that can be mapped over
  • they must obey following laws
    • Identity: when mapped over by identity function, the result matches the original container
    • Composition: A functor mapped over by composition of two functions is same as functor mapped over by individual functions
  • Combine N things:

  • All same types?:

  • N > 1 Always?: Semigroup

  • Else: Monoid

  • Types are different, obtained independently?: Applicative/Apply

  • Else: Use Monad

  • Apply and Bind are Applicative and Monad without point (pure)

  • Free Functor Hierarchy
  • Free Functors: describe programs that change value, ie given A return B
  • Free Applicatives: describe programs that build data, eg DSLs, parsers, codecs
  • Free Monads: describe programs that build programs
  • State monad: tutorial to replace foldLeft with traverse