functional io and effects

Post on 12-Apr-2017

15 Views

Category:

Software

0 Downloads

Preview:

Click to see full reader

TRANSCRIPT

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

top related