Introduction to the
IO Monad

Game Plan

  • Referential transparency
     
  • Referentially transparent effects
     
  • What is the IO monad?
     
  • Production ready IO monads
     
  • Workshop

Referential Transparency

val x = ${expr}
(x, x)
(${expr}, ${expr})

are these programs the same..?

It depends on ${expr}

Referential Transparency

val x = "hello"
(x, x)
("hello", "hello")

are these programs the same..?

Yes!

Referential Transparency

val x = println("hello")
(x, x)
(println("hello"), println("hello"))

are these programs the same..?

No!

Referential Transparency

  • Every expression is either referentially transparent or it's not.
     
  • If its not, its a side effect
     
  • If it is, its a pure expression 

Effects


   println("hello")

How can we make this referentially transparent?

Effects

  • The "trick" here is to describe the act of printing to the console using a pure value.
     
  • Using                  is one way. e.g.
     
  • Can we easily express the following example using this representation?
() => A
() => println("hello")
def greeter: Unit = {
  println("Whats your name?")
  val name = StdIn.readLine()
  println(s"Hello $name")
}

Lets Try

type Action[A] = () => A

def printLine(s: String): Action[Unit] =
  () => println(s)

val readLine: Action[String] =
  () => StdIn.readLine()

def greeter: Action[Unit] = {
  val prompt = printLine("Whats your name?")
  val readName = readLine
  val printGreeting = printLine("Hello " + ???)
  ???
}

Sequencing

  • To implement the example, we need to be able to sequence actions. e.g.
def after[A, B](
  action: Action[A],
  fn: A => Action[B]): Action[B] = // ...
implicit class AndThenOps[A](a: Action[A]) {
  def andThen[B](fn: A => Action[B]): Action[B] = 
    after(a, fn)
}
  • Plus some syntactic sugar

after

def after[A, B](
  action: Action[A], 
  fn: A => Action[B]
): Action[B] =
  () => fn(action.apply()).apply()

Implementing the example

def greeter: Action[Unit] = {
  printLine("Whats your name?")
    .andThen(_ => readLine)
    .andThen(name => printLine(s"Hello $name"))
}
def greeter: Action[Unit] = {
  printLine("Whats your name?")
    .flatMap(_ => readLine)
    .flatMap(name => printLine(s"Hello $name"))
}

equivalently

def greeter: Action[Unit] = 
  for {
    _    <- printLine("Whats your name?")
    name <- readLine
    _    <- printLine(s"Hello $name")
  } yield ()
def greeter: Unit = {
  println("Whats your name?")
  val name = StdIn.readLine()
  println(s"Hello $name")
}

Comparison

Monads

case class IO[A](unsafeRun: () => A) {
  def map[B](f: A => B): IO[B] =
    IO(() => f(unsafeRun()))

  def flatMap[B](f: A => IO[B]): IO[B] =
    IO(() => f(unsafeRun()).unsafeRun())
}

object IO {
  def pure[A](a: A): IO[A] = IO(() => a)
}

Recap

  • Conceptually the IO monad is quite simple. We were able to implement it in just a few lines.
     
  • We can work with side effects in a referentially transparent way by describing the effect using pure values.
     
  • The IO monad provides all the required combinators to write imperative code in a functional style

Impacts

  • Easy to adopt in existing code bases, since the structure of the code is similar.
     
  • Referential transparency makes refactoring much simpler. It is just a syntactic transformation.
     
  • functional-imperative is not the best we can do, we can use the same concepts from the IO monad in our own code. Create data-structures that describe your application state, transform the data-structures, reify the transformed data-structure

In Practice

  • Don't use an IO monad like what was demonstrated here. Use a production hardened one such as cats-effect, monix or ZIO.
     
  • These libraries support several other capabilities too, such as asynchrony, concurrency, resource safety and cancellation

Workshop

References

Made with Slides.com