Upload
luka-jacobowitz
View
283
Download
2
Embed Size (px)
Citation preview
What Referential Transparency can do for you
What exactly is referential transparency?
Referential transparency is mostly synonymous with purity, meaning
lack of side-effects
As functional programmers we want to avoid side-effects.
But what exactly is a side-effect?
No mutability?
Always returns the same thing given the same
input?
Definition of a pure function
def sum(list: List[Int]): Int = { launchNukes() list.foldLeft(0)(_ + _)}
Is this pure?
def sum(list: List[Int]): Int = { var z = 0 for (i <- list) { z += i } z}
Is this pure?
Referential transparency just means that exchanging a term for any other
term that refers to the same entity does not change the program.
def sum(list: List[Int]): Int = { var z = 0 for (i <- list) { z += i } z}
Is this referentially transparent?
sum(List(1,2,3)) + sum(List(1,2,3))
val x = sum(List(1,2,3))x + x
Are these two programs equivalent?
- 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?
- 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
Effectful and non-effectful code should be separated!
- C++’s constexpr
- Fortran’s PURE FUNCTION
- D’s pure
Examples
impure def launchNukes(): Unit = { ... //call impure code here}
impure def main(): Unit = { sum(List(1,2,3)) launchNukes() }
Impurity annotations!
Then what about async?
async impure def launchNukes(): Unit = { ... //call impure code here}
async impure def main(): Unit = { sum(List(1,2,3)) await launchNukes()}
Async annotations!
We’d need to add language feature for every different kind of effect
Effects shouldn’t be “to the side”, they
should be first class values that we can
supply to our runtime.
Realization:
type Effect = () => Unittype Effects = List[SideEffect]
def main(): Effects = List( () => println(“Launching Nukes”), () => launchNukes())
How should we model our effects?
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...
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!
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
We accidentally invented Monads!
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
Monads allow us to treat computations as values that can be passed around as first-class citizens
within the pure host language
Where can I find this?
- cats-effect IO
- Monix Task
- fs2
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
Thank you for listening!
Twitter: @LukaJacobowitz