This is a study note of the concept of FP based on 5-parts tutorial.

1 Motivation and Types

Movitivations

  • Small scopes
  • DI for loose coupling and easy testing
  • Prviate/safe constructor
  • Clean APIs
  • Good domain modles

A good type system helps you

  • describe the stuff: use ADT
  • describe the relationship between stuff: use restrict type in upstream
  • describe the context of stuff: effects

Scala uses case class for prodcut type and allows sealed trait with case object and case class to define sum types. The mixture of sum and prodcut types is algebraic data type (ADT). Avoid subtyping concept in FP.

For generics, the more kinds of things something can potentially be, the less we can reason abou twhat it actually is. For example, def foo(s: String): String is less constrained than defe foo[A](a: A): A because you know very few about the generic version and the only function you can write is identity.

Referential transparency enables you to change a variable with its expression and vice versa. Code can be refactored and be understood locally.

A FP application can be viewed as three nested parts:

  • the outmost is the outside world
  • the app boundary that has side-effection functions that should be very strict aobut its inputs/outptus
  • the core has pure functions

Pure functions are determinstic, have no side effects and are total. If also means no null, no reflection and no exceptions.

An effect is whatever distinguishes F[A] from A. An effect is also called a context, a program in F, and a computation.

Pros of FP in Scala

  • expressive domain model
    • 1st class functions
    • concise generics
    • ADTs and pattern matching
    • typeclasses over inheritance (with macro of simplicity)
  • can be referentially transparent
  • reasonably type safe
  • model context

Cons of FP in Scala

  • different mental model
  • mixed FP/OOP
  • doesn’t stop you from writing bad code
  • runtime DI doesn’t

2 Effects

Impure things

  • partiality
  • exceptions
  • nondeterministism
  • DI
  • mutable state
  • side effects

An effects is an impure thing, a box or a context. The extra things for a pure computation. It has a shape of F[A]. Programming with effects involves

2.1 Operate on things inside of a context: Functor

  • a Functor has a map method, don’t care about the structure of F but keep the structure.
  • def map[A, B](x: F[A](f: A => B): F[B]
  • def lift[A, B](f: A => B): F[A] => F[B] = fa => map(fa)(f). It is called lift because it works at a higher abstraction level.
  • Functor is a typeclass
  • you can constraint a program to require Functor
  • you cannot: unwarp a functor, smoosh two functor together like F[F[A]] or F[A] + F[A].
  • Functors compose: If F and G have functors, then F[G[_]] or G[F[_]] is a functor.

2.2 Put thing inside of a context: Monad

A monad has the following methods

  • put a thing inside a context A => M[A]. It is called pure, unit, or point.
  • unwrap a thing F[F[A]] => F[A]. It is called flatten or join
  • sequence effects: F[A] => (A => F[B]) => F[B]. It is called bind, flatmap or >>=. The call depends on the previous value. It is used to chain together success cases and short-circuit if something is wrong. It is a much better way than the try/catch.

All the monads should have the same effect type. It is used to handle success/faliure or contextal staff like reader, writer etc. In a chain of operations, each operation depends on the one before it.

2.3 Work with things nested inside of a context: Applicative

Applicative is used in a china of effectful computatoins that have no relationship with each other. The operation itself is inside a context. The method name is often called <*>, ap, zip or product. It runs multipel indepedent effects concurrently.

1
2
3
4
trait Applicative[F[_]] extends Functor[F] {
  def product[A, B](fa: F[A], fb: F[B]): F[(A, B)]
  def pure[A](a: A): F[A]
}

The application operation depends on the effect. For List, it gets all combinations of values in each list. For Task, it runs multiple tasks concurrently.

2.4 Inverting Containers: Traverse

To transform F[M[A]] to M[F[A]], use Traverse:

1
2
3
trait Traverse[F[_]] {
  def traverse[G[_]: Applicative, A, B](fa: F[A])(f: A => G[B]): G[F[B]]
}

The inner class must have a type of Applicative. If an effect has a Traverse then it has a sequence method to deal with nested effects.

2.5 Combine effects via unpacking and packing

Check if there is method like combineAll, foldMap.

2.6 Nested Monad

Use nested for comprehension or use Monda transformers.

2.7 Abstract Over the Context: Higher Kinded Types

A F[_] is a type constructor. You can think the * as a hole in the type.

  • Kinds describe the number of holes in a type
  • The kind of an ordinary type like Int or Char is *, zero hole.
  • The kind of unary type constructor like Maybe or List is * -> *, one hole.
  • The kind of binary type consructor like Either is * -> * -> *, two holes.

3 Typeclasses

Typeclasses, better to be named type capabilities or type behaviors, are used to

  • extend libs with new functionality
  • allow ad-hoc polymorphism, no hierarchy/inheritance
  • typeclasses can have an OO hierarchy

A type class is decoupled from the data type and it

  • is a trait, holds no state
  • has a type parameter, can be a higher-kinded type parameter
  • has at least one abstract method
  • may contain generalized methods
  • may extend other typeclasses

There should be one implementation of a typeclass for a given type in a scope, this is called typeclass coherence. The typeclass defines a has-a relationship for the type that implement the trait.

Typeclass suppose composition that if A is a Monoid, you can define F[A] in a generic way. Typeclass laws are properties that must hold for a typeclass to be valid for the type that implement it. It helps compiler to work properly.

You can use typeclasses to both describe the relationships between stuff and restrain the context. Functor, Monad, Applicative are type classes on an effect that restrict the capabilities of an effect.

The differences between a typelcass and an abstract interfaces are

  • A type-class instance is decoupled from the data type.
  • Typeclasses have laws.
  • Typeclasses are composable by compiler.

4 Practical Effect Manipulation

Use Traverse to invert two effects: F[G[X]] to G[F[X]].

1
2
3
4
5
6
trait Traverse[F[_]] extends Functor[F] {
  def traverse[G[_]: Applicative, A, B](fa: F[A])(f: A => G[B]): G[F[B]]

  def map[A, B](fa: F[A])(f: A => B): F[B] =
    traverse(fa)(a => Id(f(a))).value
}

Traversable allows you to transform elements inside the structure like a functor, producing applicative effects along the way, and lift those potentially multiple instances of applicative structure outside of the traversable structure. It is commonly described as a way to traverse a data structure, mapping a function inside a structure while accumulating the applicative contexts along the way.

5 Basics of Tagless Final and ZIO

Effects are

  • of type F[A], representing whaever differentiates A from F[A]
  • stackable, such as ReaderT[IO] and the entire stack has a type of F[A], an enviornment comprise one or more effects.

Procedure effects are side-effecting, non-deterministic or partial interaction with the real world. A funcitonal effect is an immutable data structure that describes procedure effects. Functional effects are later interpretted in some runtime into the procedure effects they describe. A functional effect

  • a context we operates in that restrict ways you can operate using DSL
  • a description of procedure effects
  • has referential transparency
  • executed by one of many possible interpreters

There are tradeoffs in the DSL and interpreters.

The following code is used to bake a bread:

1
2
3
4
5
6
package bakery.algebras
import bakery.models.Bread
trait Oven[F[_]] {
  def preheat: F[Oven[F]]
  def bake(bread: Bread): F[Bread]
}

The [_] in F[_] denotes the idea of an effect over something. The trait defeines a generic interface of the abrstract “device” or “sevice and is called an algebra in FP.

The model is defined as the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package bakery.models
import Bread._

case class Bread private (size: BreadSize) {
  def grow: Bread = this.copy(size = BreadSize(size.value * 2))
}

object Bread {
  case class BreadSize(value: Int) extends AnyVal
  def prepare: Bread = Bread(BreadSize(1))
}

The error is defined as algebraic data type (ADT):

1
2
3
4
5
6
package bakery.errors

import scala.util.control.NoStackTrace

sealed trait BakingError extends NoStackTrace
case object ColdOvenError extends BakingError

The actual ovens are instances of the Oven type, are called interpreters. the algebra doesn’t assume anything about that other than defin a set of actions on an interpreter instance. A version could be:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package bakery.interpreters

import cats.effect.Sync
import bakery.algebras.Oven
import bakery.errors.ColdOvenError
import bakery.models.Bread

final class ElectricOven[F[_]: Sync] private (temperature: Int)
    extends Oven[F] {
  override def preheat: F[Oven[F]] =
    Sync[F]
      .delay({
        new ElectricOven[F](temperature = 100)
      })

  override def bake(bread: Bread): F[Bread] = {
    if (temperature == 0) {
      Sync[F].raiseError(ColdOvenError)
    } else {
      Sync[F].point(bread.grow)
    }
  }
}

object ElectricOven {
  def make[F[_]: Sync]: ElectricOven[F] = new ElectricOven[F](temperature = 0)
}

The F[_]: Sync chooses the type of effect, also called boundaries or capabilities. It means that the effect type needs to be an instance of a Sync typeclass. In cats, it is the capability of suspending the execution of side-effecting code. Here it can delay, raise error and point a function.

To run the code, we need to choose a concrete instance of the sync type class to be the runtime. The followinmg use the IO type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package bakery

import cats.effect.{ExitCode, IO, IOApp}
import cats.implicits._
import bakery.errors.ColdOvenError
import bakery.interpreters.ElectricOven
import bakery.models.Bread

object MainIO extends IOApp {
  override def run(args: List[String]): IO[ExitCode] = {
    val unbakedBread = Bread.prepare
    val coldOven = ElectricOven.make[IO]

    val bakingProgram = for {
      _ <- IO(println("Preheating the electric oven... done!"))
      preheatedOven <- coldOven.preheat
      _ <- IO(println(s"Baking the $unbakedBread..."))
      bakedBread <- preheatedOven.bake(unbakedBread)
      _ <- IO(println(s"The bread grows twice in size!"))
    } yield bakedBread

    bakingProgram
      .recoverWith({
        case ColdOvenError =>
          IO(
            println("The oven was cold, your bread might not be good to eat :(")
          ) >> IO(unbakedBread)
      })
      .flatMap(bread => IO(println(s"Here's your bread: $bread")))
      .as(ExitCode.Success)
  }
}

The runtime can be changed to ZIO, as the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package bakery

import bakery.errors.ColdOvenError
import bakery.interpreters.ElectricOven
import bakery.models.Bread
import zio._
import zio.console._
import zio.interop.catz._

object MainZIO extends CatsApp {

  override def run(args: List[String]): ZIO[zio.ZEnv, Nothing, Int] = {
    val unbakedBread = Bread.prepare
    val coldOven = ElectricOven.make[Task]

    val bakingProgram = for {
      _ <- putStrLn("Preheating the electric oven... done!")
      preheatedOven <- coldOven.preheat
      _ <- putStrLn(s"Baking the $unbakedBread...")
      bakedBread <- preheatedOven.bake(unbakedBread)
      _ <- putStrLn(s"The bread grows twice in size!")
    } yield bakedBread

    bakingProgram
      .catchAll({
        case ColdOvenError =>
          for {
            _ <- putStrLn(
              "The oven was cold, your bread might not be good to eat :("
            )
          } yield unbakedBread
      })
      .flatMap(bread => putStrLn(s"Here's your bread: $bread"))
      .map(_ => 0)
  }
}

The tagless final has four steps:

  • define algebra
  • define domain model
  • define interpreter
  • choose a ruttime and an effect type