Download - Functional IO and Effects
Functional I/O and Effects
Or, if a Monad folds in a functional forest, does it make an effect?
Dylan ForcieaOseberg
February 28, 2017
Topics
• What does it mean for a language to be functional?• Creating a type to represent IO operations• Adding monoid operations to the IO type• Adding monad operations to the IO type• How a naïve approach for executing the monad is insufficient,
and how trampolines can fix it
What is a functional programming language?
• Functions as first class objects?• Declarative rather than imperative?• Recursion rather than loops?
Referential Transparency
• Yes!• But Referential Transparency is also key to some advantages
of functional programming• Any expression can be replaced with its value without
changing the meaning of the program• This alludes to the fact that you should avoid side effects
Example of Referential Transparency
def average(x: Int, y: Int, z: Int): Int = (x + y + z) / 3
average(1, 2, 3) (1 + 2 + 3) / 3 (6) / 3 2
Running this a second time with the same input values will always return the same result
Counterexample of Referential Transparency
var cumulativeValue: Int = 0var numberSummed: Int = 0def cumulativeAverage(x: Int): Int = { numberSummed = numberSummed + 1 cumulativeValue += x cumulativeValue / numberSummed}
cumulativeAverage(10) cumulativeValue = 10, numberSummed = 1, result = (10/1) = 10
cumulativeAverage(20) cumulativeValue = 30, numberSummed = 2, result = (30/2) = 15
cumulativeAverage(20) cumulativeValue = 50, numberSummed = 3, result = (50/3) = 16
What does Referential Transparency buy you?
• Testability• Promotes parallel programming• Can enable optimization of code
Referential Transparency and IO
def addOneToUserInput(): Int = scala.io.StdIn.readInt() + 1
The return value of executing this function will be different depending on what the user inputs!
Wrapping IO in a Type
• Separate IO into a type and define actions inside• Denotes which pieces of the code are actions• Defer running an action until later• Describe a program containing effects purely functionally,
and then run it
Wrapping IO in a Type
trait IO[A] { def run: A }
def PrintLine(msg: String): IO[Unit] = new IO[Unit] { def run: Unit = println(msg) }
PrintLine(“Hello, world!”).run Hello, world!
How do we run a sequence of these actions?
• Make it a monoid, of course!• What was a monoid, again?
Monoid Refresher• A monoid is a combination of:• Some type, A• A seed (or "zero") for that type• An associative binary operator.
• Concretely:trait Monoid[A] { def op(a1: A, a2: A): A def zero: A}
• In addition to the associativity of op, for any A the following must hold:• op(a, zero) == a == op(zero, a)
Monoid Refresher Example
• For integer addition:object IntMonoid extends Monoid[Int] { def op(a: Int, b: Int): Int = a + b def zero: Int = 0}
• You can use operations like fold to combine these:• List(1, 2, 3).fold(IntMonoid.zero)(IntMonoid.op)• (((0 + 1) + 2) + 3) = 6
IO as a Monoidtrait IO[A] { self => def run: A def ++[B](io: IO[B]): IO[B] = new IO[B] { def run: B = { self.run; io.run } }}
object IO { def zero: IO[Unit] = new IO[Unit] { def run: Unit = { } } def ++[A, B](io1: IO[A], io2: IO[B]): IO[B] = io1 ++ io2}
IO Monoid Example Usage(PrintLine("Hello!") ++ PrintLine("World!")).run
Hello!World!
val ioList = List(PrintLine("One"), PrintLine("Two"), PrintLine("Three"))ioList.fold(IO.zero)(IO.++).run
OneTwoThree
That’s great, but…
• We can chain together output effects • But, how can we actually perform any operations on input
effects?• Monads!• Wait, another one of those big words…
Monad refresher
• Monads provide two operations:• def map[B](f: A => B): IO[B]• def flatMap[B](f: A => IO[B]): IO[B]
Monad refresher
• map transforms a value from domain A to a value from domain B inside the monad• flatMap transforms a value from domain A into a Monad
containing a value from domain B, and then returns a monad containing a value from domain B
How does this help?
• map takes the result of an IO monad and perform computations on the value• flatMap takes the result of an IO monad, and then perform IO
for output• Like control flow for the IO “program”
IO as a Monadtrait IO[A] { self => def run: A def ++[B](io: IO[B]): IO[B] = new IO[B] { def run: B = { self.run; io.run } } def map[B](f: A => B): IO[B] = new IO[B] { def run: B = f(self.run) } def flatMap[B](f: A => IO[B]): IO[B] = new IO[B] { def run: B = f(self.run).run }}
def PrintLine(msg: String): IO[Unit] = new IO[Unit] { def run: Unit = println(msg) }def GetLine(): IO[String] = new IO[String] { def run: String = scala.io.StdIn.readLine() }
Lets put it together!PrintLine("Type a number") .flatMap(_ => GetLine()) .map(s => s.toInt) .map(i => i + 1) .flatMap(r => PrintLine(r.toString)) .run
Type a number 1
2
Can’t mix and match
GetLine().toInt + 1
<console>:15: error: value toInt is not a member of IO[String] GetLine().toInt + 1
Some useful operations
def doWhile[A](a: IO[A])(cond: A => IO[Boolean]): IO[Unit] = for { a1 <- a ok <- cond(a1) _ <- if (ok) doWhile(a)(cond) else IO.zero} yield ()
def forever[A, B](a: IO[A]): IO[B] = a flatMap(_ => forever(a))
Houston, we have a problem!
forever(PrintLine("Hello")).runHelloHello…<Stack Overflow!>
• What happened?!
Not tail recursivedef flatMap[B](f: A => IO[B]): IO[B] = new IO[B] { def run: B = f(self.run).run }def forever[B](a: IO[A]): IO[B] = a flatMap(_ => forever(a))
• forever keeps on calling flatMap• Every time flatMap is called, we end up one level lower in
recursion• Since the function definition says we need to keep track of
the results of f(this.run) to call run on it, we keep on adding on to the call stack every time
How do we fix this?• Use Trampolining• Create a series of Algebraic Data Types (ADTs) to describe
how the operation will run• We can make running our IO monad operations tail
recursive• First things first…• What is tail recursion?• What is an ADT?
Tail Recursion
• Don’t want to keep track of state after calling function recursively• If the last function run is the recursive call, then it is in tail
position• We can skip adding a stack frame
Tail recursion exampledef factorial(n: BigInt): BigInt = if (n == 0) 1 else n * factorial(n-1)
def factorial_tailrec(n: BigInt): BigInt = { def factorial1(n: BigInt, acc: BigInt): BigInt = if (n == 0) acc else factorial1(n - 1, n * acc)
factorial1(n, 1)}
Algebraic Data Types
• This is a data type that can be one of several things, and each thing can contain a defined set of data
• We can use pattern matching to perform operations on the ADT
• This is probably more apparent if we just give an example…
List ADT definition
sealed trait List[+A]
case class Cons[+A](head: A, tail: List[A]) extends List[A]case object Nil extends List[Nothing]
List ADT Pattern Matchingval a = Cons(1, Nil)val b = Cons(2, a)val c = Cons(3, b)
def add(list: List[Int]): Int = list match { case Cons(head, tail) => head + add(tail) case Nil => 0 }
add(c)6 3 2 1
Describing IO monad operations with an ADTsealed trait IO[A] { self =>
def flatMap[B](f: A => IO[B]): IO[B] = FlatMap(self, f) def map[B](f: A => B): IO[B] = flatMap(a => (Return(f(a))))}
case class Return[A](a: A) extends IO[A]case class Suspend[A](resume: () => A) extends IO[A]case class FlatMap[A, B](sub: IO[A], k: A => IO[B]) extends IO[B]
def PrintLine(s: String): IO[Unit] = Suspend(() => Return(println(s)))def GetLine: IO[String] = Suspend(() => scala.io.StdIn.readLine())
Our example from earlier!
PrintLine("Type a number") .flatMap(_ => GetLine) .map(s => s.toInt) .map(i => i + 1) .flatMap(r => PrintLine(r.toString))
SuspendPrintLine Prompt
Our example from earlier!
PrintLine("Type a number") .flatMap(_ => GetLine) .map(s => s.toInt) .map(i => i + 1) .flatMap(r => PrintLine(r.toString))
SuspendPrintLine Prompt
FlatMap
SuspendGetLine
Our example from earlier!
PrintLine("Type a number") .flatMap(_ => GetLine) .map(s => s.toInt) .map(i => i + 1) .flatMap(r => PrintLine(r.toString))
SuspendPrintLine Prompt
FlatMap
SuspendGetLine
Returns.toInt
FlatMap
Our example from earlier!
PrintLine("Type a number") .flatMap(_ => GetLine) .map(s => s.toInt) .map(i => i + 1) .flatMap(r => PrintLine(r.toString))
SuspendPrintLine Prompt
FlatMap
SuspendGetLine
Returns.toInt
Returni + 1
FlatMap
FlatMap
Our example from earlier!
PrintLine("Type a number") .flatMap(_ => GetLine) .map(s => s.toInt) .map(i => i + 1) .flatMap(r => PrintLine(r.toString))
SuspendPrintLine Prompt
FlatMap
SuspendGetLine
Returns.toInt
Returni + 1
SuspendPrintLine
ResultFlatMap
FlatMap
FlatMap
Now we need the Interpreter…def run[A](io: IO[A]): A = io match { case Return(a) => a case Suspend(r) => r() case FlatMap(x, f) => x match { case Return(a) => run(f(a)) case Suspend(r) => run(f(r())) case FlatMap(y, g) => run(y.flatMap(a => g(a).flatMap(f))) }}
Run the example
SuspendPrintLine Prompt
FlatMap
SuspendGetLine
Returns.toInt
Returni + 1
SuspendPrintLine
ResultFlatMap
FlatMap
FlatMapPrintLine("Type a number") .flatMap(_ => GetLine) .map(s => s.toInt) .map(i => i + 1) .flatMap(r => PrintLine(r.toString))
Run the example
SuspendPrintLine Prompt
FlatMap
SuspendGetLine
Returns.toInt
Returni + 1
SuspendPrintLine
Result
FlatMap
FlatMap FlatMap
PrintLine("Type a number") .flatMap(_ => GetLine) .map(s => s.toInt) .map(i => i + 1) .flatMap(r => PrintLine(r.toString))
Run the example
SuspendPrintLine Prompt
FlatMap
SuspendGetLine
Returns.toInt
Returni + 1
SuspendPrintLine
Result
FlatMap
FlatMap
FlatMap
PrintLine("Type a number") .flatMap(_ => GetLine) .map(s => s.toInt) .map(i => i + 1) .flatMap(r => PrintLine(r.toString))
Run the example
SuspendPrintLin
e Prompt
FlatMap
SuspendGetLine
Returns.toInt
Returni + 1
SuspendPrintLine
Result
FlatMap
FlatMap
FlatMap
PrintLine("Type a number") .flatMap(_ => GetLine) .map(s => s.toInt) .map(i => i + 1) .flatMap(r => PrintLine(r.toString))
Run the example
SuspendPrintLin
e Prompt
FlatMap
SuspendGetLine
Returns.toInt
Returni + 1
SuspendPrintLine
Result
FlatMap
FlatMap
FlatMap
PrintLine("Type a number") .flatMap(_ => GetLine) .map(s => s.toInt) .map(i => i + 1) .flatMap(r => PrintLine(r.toString))
Run the example
SuspendGetLine
Returns.toInt
Returni + 1
SuspendPrintLine
Result
FlatMap
FlatMap
FlatMap
PrintLine("Type a number") .flatMap(_ => GetLine) .map(s => s.toInt) .map(i => i + 1) .flatMap(r => PrintLine(r.toString))
Run the example
Returns.toInt
Returni + 1
SuspendPrintLine
Result
FlatMap
FlatMap
PrintLine("Type a number") .flatMap(_ => GetLine) .map(s => s.toInt) .map(i => i + 1) .flatMap(r => PrintLine(r.toString))
Run the example
Returni + 1
SuspendPrintLine
Result
FlatMap
PrintLine("Type a number") .flatMap(_ => GetLine) .map(s => s.toInt) .map(i => i + 1) .flatMap(r => PrintLine(r.toString))
Run the example
SuspendPrintLine
Result
PrintLine("Type a number") .flatMap(_ => GetLine) .map(s => s.toInt) .map(i => i + 1) .flatMap(r => PrintLine(r.toString))
How about forever?def forever[B](a: IO[A]): IO[B] = a flatMap(_ => forever(a))run(forever(PrintLine("Hello")))
SuspendPrintLine
FlatMap
Now it works!
run(forever(PrintLine("Hello")))HelloHello…<Keeps going!>
Takeaways
• Like a Free Monad! (A story for another day…)• You don’t have to use the same interpreter... you can
represent IO as ADTs and not perform IO at all• Effects are purposeful and partitioned off within a type• Bonus – it happens to be a monad!
Other libraries• Slick• Describe SQL operations• Execute them once they are prepared
• Akka Streams• Describe a dataflow with a graph• Push data through the graph components• In Scala, this is an advanced flavor of the IO monad described here
Any Questions?
References
• Functional Programming in Scala, Chiusano and Bjarnason• https://www.slideshare.net/InfoQ/purely-functional-io - Nice
presentation by Runar Bjarnason• http://blog.higher-order.com/assets/scalaio.pdf