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
- Who has code they brought with them to refactor?
- For those that haven't, here is a toy example to work with https://github.com/Tapad/intro-to-io-monad
References
Introduction to the IO Monad
By Eli Jordan
Introduction to the IO Monad
- 897