Expressions

Jean-Rémi Desjardins

Scala By The Bay 2015

an alternative to for-comprehensions

def getPhone: Future[Phone]
def getAddress: Future[Address]
def getSpamScore: Future[SpamScore]
for {p <- getPhone
     a <- getAddress
     s <- getSpamScore} yield render(p, a, s)
phone: Future[Phone]
address: Future[Address]
spamScore: Future[SpamScore]
for {p <- phone
     a <- address
     s <- spamScore} yield render(p, a, s)
phone: Future[Phone] // 1 second
address: Future[Address] // fail after 10 milliseconds
spamScore: Future[SpamScore] // 2 seconds
for {p <- phone
     a <- address
     s <- spamScore} yield render(p, a, s)
phone: Future[Phone] // 1 second
address: Future[Address] // fail after 10 milliseconds
spamScore: Future[SpamScore] // 2 seconds
Applicative[Future].apply3(a,b,c)(combine)

bind or flatMap cannot "fail-fast"

History

See my talk at PNWScala

Bind cannot "fail-fast"

trait Monad[F[_]] {
    def bind[A,B](fa: F[A])(f : a => F[B]): F[B]
}

Expressions are an alternative to for-comprehensions that support failing fast among a few other things

Examples

Fails fast

val a: Future[A]
val b: Future[B]
val c: Future[C]

Fails fast

for {aa <- a
     bb <- b
     cc <- c} yield combine(a, b, c)
val a: Future[A]
val b: Future[B]
val c: Future[C]

Fails fast

for {aa <- a
     bb <- b
     cc <- c} yield combine(a, b, c)
Expression { combine(a,b,c) }
val a: Future[A]
val b: Future[B]
val c: Future[C]

Plays well with if and match

val a: Future[A]
val b: Future[B]
val c: Future[C]

Plays well with if and match

for {aa <- a
     bb <- b
     cc <- c} yield if (aa == something) polish(bb) else polish(cc)
val a: Future[A]
val b: Future[B]
val c: Future[C]

Plays well with if and match

for {aa <- a
     bb <- b
     cc <- c} yield if (aa == something) polish(bb) else polish(cc)
 (for (aa <- a) yield
   if (aa == something) for (bb <- b) yield polish(bb)
   else for (cc <- c) yield polish(cc)).flatMap(identity)
val a: Future[A]
val b: Future[B]
val c: Future[C]

Plays well with if and match

for {aa <- a
     bb <- b
     cc <- c} yield if (aa == something) polish(bb) else polish(cc)
 (for (aa <- a) yield
   if (aa == something) for (bb <- b) yield polish(bb)
   else for (cc <- c) yield polish(cc)).flatMap(identity)
val a: Future[A]
val b: Future[B]
val c: Future[C]
Expression { if(extract(a) == something) polish(b) else polish(c) }

Not a replacement for for-comprehensions

def time[A](task: Task[A]): Task[(Duration, A)] = for {
    t1 <- Task.delay(System.currentTimeMillis)
    a  <- task
    t2 <- Task.delay(System.currentTimeMillis)
} yield ((t2 - t1).milliseconds, a)

Example from Remotely

When to use them

Write Async code

val response: Future[HTML] = Expression {
    val phone = extract(lookupPhone(phoneString))
    val address = extract(lookupAddress(phone))
    val rep = extract(lookupReputation(phone))
    renderPage(phone, address, rep)
}

But what about a single unified notation

  • That supports:

    • Monad

    • Applicative Functor

    • Functor

    • conceivably others

 Wouldn't that be nice Professor? One single elegant equation to explain everything?

Not only does that reduce cognitive overload, but it is more powerful

  • Monad cannot support fail-fast for Futures
  • The more expressive an abstraction, the less flexible it is in it's implementation
  • Our notation should use the least powerful abstraction that is required
    • This allows maximum flexibility to implement behavior of combinators

Why Monad cannot fail fast

trait Monad[F[_]] {
    def bind[A,B](m: F[T], f: A => F[B]): F[B]
}

What does it look like

val phoneString: String = ???
val lookupPhone: String => Future[Phone] = ???
val lookupAddress: Phone => Future[Address] = ???
val lookupReputation: Phone => Future[Score] = ???
val renderPage: (Phone, Address, Int) => HTML = ???

val response: Future[HTML] = Expr {
    val phone = lookupPhone(phoneString)
    val address = lookupAddress(phone)
    val rep = lookupReputation(phone)
    renderPage(phone, address, rep)
}

Uses Scala implicit resolution to mirror Idris behavior but can be made explicit

import com.github.jedeash.Expr
import com.github.jedesah.Expr.extract

val phoneString: String = ???
val lookupPhone: String => Future[Phone] = ???
val lookupAddress: Phone => Future[Address] = ???
val lookupReputation: Phone => Future[Score] = ???
val renderPage: (Phone, Address, Int) => HTML = ???

val response: Future[HTML] = Expr {
    val phone = extract(lookupPhone(phoneString))
    val address = lookupAddress(phone)
    val rep = lookupReputation(phone)
    renderPage(phone, extract(address), extract(rep))
}

Explicit

Implicit

import com.github.jedeash.Expr
import com.github.jedesah.Expr.auto.extract

val phoneString: String = ???
val lookupPhone: String => Future[Phone] = ???
val lookupAddress: Phone => Future[Address] = ???
val lookupReputation: Phone => Future[Score] = ???
val renderPage: (Phone, Address, Int) => HTML = ???

val response: Future[HTML] = Expr {
    val phone = lookupPhone(phoneString)
    val address = lookupAddress(phone)
    val rep = lookupReputation(phone)
    renderPage(phone, address, rep)
}

Prior Art

  • Effectful*

    • ​Replacement for for-comprehension

    • Generalizes Async/Await

    • Only supports Monad

  • Scala Workflow*

    • Supports everything I do and more

    • Uses untyped macros

    • Reimplements parts of scalac including scoping which leads to minimal coverage of the language

*https://github.com/pelotom/effectful

*https://github.com/aztek/scala-workflow/

Scala Computation Expressions

  • https://github.com/jedesah/computation-expressions
  • Releasing 0.1.0 today (version and date may change)

Thought: Towards automatic code parallelization

  • One of the promises of purely functional programming
  • Tried by Facebook with FXL
  • Computation Expressions do a lot to bring us there

Thank you

Questions, comments?

Computation Expressions - Scala By the Bay 2015

By Jean-Rémi Desjardins

Computation Expressions - Scala By the Bay 2015

  • 1,400