2. Handling Effects Functionally
• What are effects
• Reading / Writing from a database
• Reading / Writing to Files or Console
• Performing Mutation of variables
!2
3. Handling Effects is hard
• Effects are hard to test.
• Introduce Concurrency Problems
• Make refactoring Hard
!3
4. What we need
• Safely manipulate effects
• Test code with Effects with ease.
• Build a description of the program
• Description can be changed by runtime.
• Treat code as a data structure
• Separate composition from declaration
!4
5. Why is IO a Monad
• Monads are about sequencing computation and when dealing with
effects, sequencing is required.
!5
6. Origin of IO
• In a Haskell program all roads lead to an IO
• Compare that to Scala
main::IO()
main = putStrLn "Hello World"
def main(args: Array[String]) : Unit = {
println(“Hello World”)
}
!6
7. Future as IO
• Futures are not lazy
• Do not separate specification from execution
• Futures are not referentially transparent
val program = for {
x <- Future { println(“foo”); 10 }
y <- Future{ println(“foo”); 10}
} yield x + y
val result = Await.result(program, Duration.Inf)
println(result)
val future = Future { println(“foo”); 10}
val program = for {
x <- future
Y <- future
} x + y
val result = Await.result(program, Duration.Inf)
println(result)
!7
8. Previous attempts at IO
• ScalaZ 7 had IO and Task
• Other libraries like fs2, monix etc also came up with Task implementations
• Slick has DBIO
!8
10. Referential Transparency
import cats.effect._
import cats.syntax.all._
object CatsIOEx3 extends IOApp {
def run(args: List[String]) : IO[ExitCode]
= {
val program = for {
x <- IO {println("foo"); 10}
y <- IO {println("foo"); 10}
} yield x + y
program.as(ExitCode.Success)
}
}
import cats.effect._
import cats.syntax.all._
object CatsIOEx4 extends IOApp {
def run(args: List[String]) : IO[ExitCode] =
{
val io = IO { println("foo"); 10}
val program = for {
x <- io
y <- io
} yield x + y
program.as(ExitCode.Success)
}
}
!10
11. Stack Safety
IO is trampolined in its flatMap evaluation. This means that you can safely
call flatMap in a recursive function of arbitrary depth without blowing up
the stack.
def fib(n: Int, a: BigDecimal = 0, b: BigDecimal = 1) : IO[BigDecimal] = {
IO(a + b).flatMap{b2 =>
if (n > 1)
fib(n - 1, b, b2)
else IO.pure(b2)
}
}
!11
12. Resource cleanup
Traditionally we use try/catch/finally as a mechanism of resource cleanup.
The problem with this is that its tied to “exception handling” which is not
considered functional
val program = IO(new BufferedReader(new FileReader(new File("~/temp/names.txt")))).bracket {in =>
var content: String = ""
var line = in.readLine()
while(line != null) {
content += line
line = in.readLine()
}
IO(content)
} {in =>
IO(in.close())
}
!12
13. Error Handling
Error Handling is typically done with try/catch/finally. But its hard to
implement retry semantics with try/catch/finally
def retryWithBackOff[A](io: IO[A], initDelay: FiniteDuration, maxRetries: Int) :
IO[A] = {
io.handleErrorWith{err =>
if (maxRetries > 1)
IO.sleep(initDelay) *> retryWithBackOff(io, initDelay * 2, maxRetries -
1)
else
IO.raiseError(err)
}
}
!13
14. Thread Shifting
In Scala we have a best practice of having 2 thread pools. One is a
bounded thread pool for CPU intensive tasks. One is a unbounded pool
for IO waits. With IO its easy to switch between these connection pools
object CatsIOEx9 extends App {
val Main = ExecutionContext.global
val BlockingIO = ExecutionContext.fromExecutor(Executors.newCachedThreadPool())
val program = for {
_ <- IO { println("what is your name")}
_ <- IO.shift(BlockingIO)
name <- IO { readLine }
_ <- IO.shift(Main)
} yield s"Hello $name"
val output = program.unsafeRunSync
println(output)
}
!14
15. Parallelism
We can also run multiple IOs at once and then process the results when all
of them have completed.
object CatsIOEx10 extends IOApp {
def run(args: List[String]) : IO[ExitCode] = {
val io1 = IO.delay(5 seconds) *> IO(20)
val io2 = IO.delay(2 seconds) *> IO(10)
val io3 = IO.delay(1 seconds) *> IO(50)
val program = (io1, io2, io3).parMapN{ (a, b, c) => a + b + c}
program.flatMap(x => IO{println(s"result $x")}).as(ExitCode.Success)
}
}
!15
16. Parallelism
Just like futures, its possible to take a list of IOs and convert into a single
IO (like Future.sequence).
object CatsIOEx11 extends IOApp {
def run(args: List[String]) : IO[ExitCode] = {
val io1 = IO.delay(5 seconds) *> IO(20)
val io2 = IO.delay(2 seconds) *> IO(10)
val io3 = IO.delay(1 seconds) *> IO(50)
val ioList = NonEmptyList.of(io1, io2, io3)
ioList.parSequence.flatMap{list => IO{println(s"sum: ${list.foldLeft(0)(_ +
_)}")}}.as(ExitCode.Success)
}
}
!16
17. Cats IO in the real world
The following libraries use the Cats IO Monad
• Doobie
• Http4s
• Sttp
• Monix
• FS2
• PureConfig
!17