functional io and effects

53
Functional I/O and Effects Or, if a Monad folds in a functional forest, does it make an effect? Dylan Forciea Oseberg February 28, 2017

Upload: dylan-forciea

Post on 12-Apr-2017

15 views

Category:

Software


0 download

TRANSCRIPT

Page 1: 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

Page 2: Functional IO and Effects

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

Page 3: Functional IO and Effects

What is a functional programming language?

• Functions as first class objects?• Declarative rather than imperative?• Recursion rather than loops?

Page 4: Functional IO and Effects

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

Page 5: Functional IO and 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

Page 6: Functional IO and Effects

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

Page 7: Functional IO and Effects

What does Referential Transparency buy you?

• Testability• Promotes parallel programming• Can enable optimization of code

Page 8: Functional IO and Effects

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!

Page 9: Functional IO and Effects

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

Page 10: Functional IO and Effects

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!

Page 11: Functional IO and Effects

How do we run a sequence of these actions?

• Make it a monoid, of course!• What was a monoid, again?

Page 12: Functional IO and Effects

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)

Page 13: Functional IO and Effects

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

Page 14: Functional IO and Effects

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}

Page 15: Functional IO and Effects

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

Page 16: Functional IO and Effects

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…

Page 17: Functional IO and Effects

Monad refresher

• Monads provide two operations:• def map[B](f: A => B): IO[B]• def flatMap[B](f: A => IO[B]): IO[B]

Page 18: Functional IO and Effects

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

Page 19: Functional IO and Effects

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”

Page 20: Functional IO and Effects

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() }

Page 21: Functional IO and Effects

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

Page 22: Functional IO and Effects

Can’t mix and match

GetLine().toInt + 1

<console>:15: error: value toInt is not a member of IO[String] GetLine().toInt + 1

Page 23: Functional IO and Effects

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))

Page 24: Functional IO and Effects

Houston, we have a problem!

forever(PrintLine("Hello")).runHelloHello…<Stack Overflow!>

• What happened?!

Page 25: Functional IO and Effects

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

Page 26: Functional IO and Effects

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?

Page 27: Functional IO and Effects

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

Page 28: Functional IO and Effects

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)}

Page 29: Functional IO and Effects

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…

Page 30: Functional IO and Effects

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]

Page 31: Functional IO and Effects

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

Page 32: Functional IO and Effects

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())

Page 33: Functional IO and Effects

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

Page 34: Functional IO and Effects

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

Page 35: Functional IO and Effects

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

Page 36: Functional IO and Effects

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

Page 37: Functional IO and Effects

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

Page 38: Functional IO and Effects

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))) }}

Page 39: Functional IO and Effects

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))

Page 40: Functional IO and Effects

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))

Page 41: Functional IO and Effects

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))

Page 42: Functional IO and Effects

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))

Page 43: Functional IO and Effects

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))

Page 44: Functional IO and Effects

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))

Page 45: Functional IO and Effects

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))

Page 46: Functional IO and Effects

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))

Page 47: Functional IO and Effects

Run the example

SuspendPrintLine

Result

PrintLine("Type a number") .flatMap(_ => GetLine) .map(s => s.toInt) .map(i => i + 1) .flatMap(r => PrintLine(r.toString))

Page 48: Functional IO and Effects

How about forever?def forever[B](a: IO[A]): IO[B] = a flatMap(_ => forever(a))run(forever(PrintLine("Hello")))

SuspendPrintLine

FlatMap

Page 49: Functional IO and Effects

Now it works!

run(forever(PrintLine("Hello")))HelloHello…<Keeps going!>

Page 50: Functional IO and Effects

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!

Page 51: Functional IO and Effects

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

Page 52: Functional IO and Effects

Any Questions?

Page 53: Functional IO and Effects

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