29
What Referential Transparency can do for you

What Referential Transparency can do for you

Embed Size (px)

Citation preview

Page 1: What Referential Transparency can do for you

What Referential Transparency can do for you

Page 2: What Referential Transparency can do for you

What exactly is referential transparency?

Page 3: What Referential Transparency can do for you

Referential transparency is mostly synonymous with purity, meaning

lack of side-effects

Page 4: What Referential Transparency can do for you

As functional programmers we want to avoid side-effects.

But what exactly is a side-effect?

Page 5: What Referential Transparency can do for you

No mutability?

Always returns the same thing given the same

input?

Definition of a pure function

Page 6: What Referential Transparency can do for you

def sum(list: List[Int]): Int = { launchNukes() list.foldLeft(0)(_ + _)}

Is this pure?

Page 7: What Referential Transparency can do for you

def sum(list: List[Int]): Int = { var z = 0 for (i <- list) { z += i } z}

Is this pure?

Page 8: What Referential Transparency can do for you

Referential transparency just means that exchanging a term for any other

term that refers to the same entity does not change the program.

Page 9: What Referential Transparency can do for you

def sum(list: List[Int]): Int = { var z = 0 for (i <- list) { z += i } z}

Is this referentially transparent?

Page 10: What Referential Transparency can do for you

sum(List(1,2,3)) + sum(List(1,2,3))

val x = sum(List(1,2,3))x + x

Are these two programs equivalent?

Page 11: What Referential Transparency can do for you

- Testability! Pure functions are extremely easy to test

- Know when and where our effects occur.

- Both you and the compiler now have guarantees that your code can be

reasoned with, this makes refactoring a walk in the park.

Why is this useful?

Page 12: What Referential Transparency can do for you

- Memoization; If we can guarantee a function to always return the same

thing given the same input, the compiler can easily memoize the result.

- Parallelization; Without sharing state, we can easily run multiple functions in

parallel without having to worry about deadlocks or data races.

- Common Subexpression Elimination; The compiler can know exactly when

multiple expressions evaluate to the same value and can optimize this away

Potential optimizations

Page 13: What Referential Transparency can do for you

Effectful and non-effectful code should be separated!

Page 14: What Referential Transparency can do for you

- C++’s constexpr

- Fortran’s PURE FUNCTION

- D’s pure

Examples

Page 15: What Referential Transparency can do for you

impure def launchNukes(): Unit = { ... //call impure code here}

impure def main(): Unit = { sum(List(1,2,3)) launchNukes() }

Impurity annotations!

Page 16: What Referential Transparency can do for you

Then what about async?

Page 17: What Referential Transparency can do for you

async impure def launchNukes(): Unit = { ... //call impure code here}

async impure def main(): Unit = { sum(List(1,2,3)) await launchNukes()}

Async annotations!

Page 18: What Referential Transparency can do for you

We’d need to add language feature for every different kind of effect

Page 19: What Referential Transparency can do for you

Effects shouldn’t be “to the side”, they

should be first class values that we can

supply to our runtime.

Realization:

Page 20: What Referential Transparency can do for you

type Effect = () => Unittype Effects = List[SideEffect]

def main(): Effects = List( () => println(“Launching Nukes”), () => launchNukes())

How should we model our effects?

Page 21: What Referential Transparency can do for you

type Effects = List[Any => Any]def performSideEffects(effs: Effects): Unit = { effs.foldLeft(())((acc, cur) => cur(acc))}def main(): Effects = List( _ => localStorage.getItem("foo"), foo => println(foo), _ => new Date().getFullYear(), year => println(year) //no access to foo here)

Hmmmm...

Page 22: What Referential Transparency can do for you

trait Effect[A] { def chain[B](f: A => Eff[B]): Eff[B]}

def main(): Effect[Unit] = localStorage.getItem("foo") .chain(foo => putStrLn(foo))

Let’s try again!

Page 23: What Referential Transparency can do for you

trait IO[A] { def flatMap[B](f: A => IO[B]): IO[B]}

def main(): IO[Unit] = for { foo <- localStorage.getItem("foo") _ <- putStrLn(foo) date <- Date.currentYear() _ <- putStrLn(s"It's $date, $foo")} yield ()

Expressed differently

Page 24: What Referential Transparency can do for you

We accidentally invented Monads!

Page 25: What Referential Transparency can do for you

class SimpleIO[A](unsafeRun: () => A) extends IO[A] { def flatMap[B](f: A => SimpleIO[B]): SimpleIO[B] = SimpleIO(() => f(unsafeRun()).unsafeRun())}

object SimpleIO { def pure(a: A): SimpleIO[A] = SimpleIO(() => a)}

Most simple implementation

Page 26: What Referential Transparency can do for you

Monads allow us to treat computations as values that can be passed around as first-class citizens

within the pure host language

Page 27: What Referential Transparency can do for you

Where can I find this?

- cats-effect IO

- Monix Task

- fs2

Page 28: What Referential Transparency can do for you

Eliminating only some side-effects can

only get us so far.

Only by fully embracing purity can we

achieve the safety we’re looking for.

Conclusion

Page 29: What Referential Transparency can do for you

Thank you for listening!

Twitter: @LukaJacobowitz