by Jean-Rémi Desjardins
A need for Futures that "fail fast"
History
Turns out this does not fail-fast
val a = Future { sleep(1.second); 5 }
val b = Future { sleep(5.seconds); 10}
val c = Future { sleep(3.seconds); fail()}
for {aa <- a
bb <- b
cc <- c} yield combine(a, b, c)
Desugared
val a = Future { sleep(1.second); 5 }
val b = Future { sleep(5.seconds); 10}
val c = Future { sleep(3.seconds); fail()}
a.flatMap { aa =>
b.flatMap { bb =>
c.map { cc =>
combine(aa, bb, cc)
}
}
}
flatMap()
trait Future[+A] {
def flatMap(f: A => Future[B]): Future[B]
}
zip()
trait Future[+A] {
def flatMap(f: A => Future[B]): Future[B]
def zip[B](fb: Future[B]): Future[(A,B)]
}
Desugared
val a = Future { sleep(1.second); 5 }
val b = Future { sleep(5.seconds); 10}
val c = Future { sleep(3.seconds); fail()}
a.zip(b).zip(c) { case ((aa, bb), cc) =>
combine(aa, bb, cc)
}
What about Async/await?
val a = Future { sleep(1.second); 5 }
val b = Future { sleep(5.seconds); 10}
val c = Future { sleep(3.seconds); fail()}
async { combine(await(a), await(b), await(c)) }
How did we solve it?
See talk at PNWScala
Was not satisfied!
Expressions are an alternative to for-comprehensions
Features
-
Uses the least powerful interface
-
Plays well with if and match statements
-
Unified notation for all abstractions
-
Customizable
Examples
Failing fast
for {aa <- a
bb <- b
cc <- c} yield combine(a, b, c)
Expression { combine(a,b,c) }
val a: Future[A] = wait(5)
val b: Future[B] = fail(1)
val c: Future[C] = wait(3)
wait(5)
fail(1)
Interacting with if
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] = wait(1)
val b: Future[B] = wait(5)
val c: Future[C] = wait(2)
Expression { if(extract(a) == something) polish(b) else polish(c) }
if (aa == something) => wait(5)
if (aa != something) => wait(5)
if (aa == something) => wait(5)
if (aa != something) => wait(2)
if (aa == something) => wait(5)
if (aa == something) => wait(5)
Using another abstraction
val a: Option[A]
val b: Option[B]
val c: Option[C]
Expression { if(extract(a) == something) polish(b) else polish(c) }
val a: Err \/ A
val b: Err \/ B
val c: Err \/ C
val a: Writer[A]
val b: Writer[B]
val c: Writer[C]
val a: Task[A]
val b: Task[B]
val c: Task[C]
val a: IO[A]
val b: IO[B]
val c: IO[C]
val a: List[A]
val b: List[B]
val c: List[C]
How it works
Based on Scalaz
trait Functor[F[_]] {
def map[A,B](fa: F[A])(f: A => B): F[B]
}
trait Apply[F[_]] extends Functor[F] {
// looks like something we've seen before?
def apply2[A,B,C](fa: F[A], fb: F[B])(f: (A,B) => C): F[C]
//derived
def map[A,B](fa: F[A])(f: A => B): F[B] = derived
}
trait Applicative[F[_]] extends Apply[F] {
def point[A](a: A): F[A]
}
trait Monad[F[_]] extends Applicative[F] {
// looks like something we've seen before?
def bind[A,B](fa: F[A])(f: A => F[B]): F[B]
//derived
def apply2[A,B,C](fa: F[A], fb: F[B])(f: (A,B) => C): F[C] = derived
}
Simple code transformation
Expression { foo(extract(a), b, extract(c) }
Applicative[_].apply2(a,c)(foo(_,b,_))
val a: Future[String]
val b: String
val c: Future[String]
def foo(one: String, two: String, three: String)
Importance of using the least powerful interface
Monad is required
Expression { foo(extract(bar(extract(a))), b, extract(c)) }
Monad[_].apply(Monad[_].bind(a)(bar),c)(foo(_,b,_))
val a: Future[String]
val b: String
val c: Future[String]
def foo(one: String, two: String, three: String)
def bar(g: String): Future[String]
if statement
Expression { if (extract(a)) extract(b) else extract(c) }
Monad[_].bind(a)(if(_) b else c)
val a: Future[String]
val b: String
val c: Future[String]
match statement
Expression {
extract(a) match {
case 1 => extract(b)
case 2 => extract(c) + 1
}
}
Monad[_].bind(a)(_ match {
case 1 => b
case 2 => instance.map(c)(cc => cc + 1)
}
val a: Future[String]
val b: Future[String]
val c: Future[String]
match statement can get hairy
Expression {
val fooY = extract(foo)
extract(bar) match {
case `fooY` => extract(b)
case 2 => extract(c) + 1
}
}
val fooY = getFoo
Monad[_].bind(Monad[_].apply(fooY, bar)((_,_)){ case (x$1, x$2) => x$1 match {
case `x$2` => b
case 2 => instance.map(c)(cc => cc + 1)
}
val foo: Future[String]
val bar: Future[String]
val b: Future[String]
val c: Future[String]
match statement can get REALLY hairy
Expression {
val fooY = extract(foo)
val bizY = extract(biz)
extract(bar) match {
case `fooY` => extract(a)
case `bizY` => extract(c)
case 2 => extract(c) + 1
}
}
{
val fooY = foo
val bizY = biz
Monad[_].bind(Monad[_].apply(fooY, barY)((_,_)){ case (x$1, x$2) => x$1 match {
case `x$2` => a
case _ => Monad[_].bind(biz){ x$3 => x$1 match {
case `x$3` => b
case 2 => instance.map(c)(cc => cc + 1)
}
}
}
Similar Projects
-
-
Alternative to for-comprehension and/or generalization of Async/Await
-
Does not use the least powerful interface (cannot fail-fast)
-
-
-
Many features (all Expressions features + context manipulation)
-
Uses untyped macros
-
Reimplements parts of scalac including scoping which leads to minimal coverage of the language
-
-
-
Only works with Scala Futures
-
Does not fail-fast
-
Limitations
Know to work (somewhat)
- function applications
- if-else statement
- function currying
- string interpolation
- blocks
- basic match statements
Know limitations
-
pattern matching in value definitions
-
advanced match statements
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)
}
Large blocks of code within a Monad
val response: Option[HTML] = Expression {
val firstFruit = json.fruits(0)
extract(firstFruit.as[String]) match {
case "apple" => "good"
case "peach" =>
if (extract(firstFruit.color.as[String]) == "brown") "bad"
else "good"
}
}
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
Future work
- Use Scala Meta
- Improve ScalaCheck function generation
- Implement Context manipulation
- Support nested abstractions
- Fix known limitations
- Generalize tests
Context Manipulation
a: Writer[String, Int] = 8.set("This is a magic value")
b: Writer[String, Int] = random().set("I am a random value")
c: Writer[String, String] = "Hello World".set("Hello World...")
Expression {
val div = if (b == 0) {
ctx :++> "We avoided a division by zero, yay!"
5
} else a / b
c * div
}
Thank you
Questions, comments?
Expressions
By Jean-Rémi Desjardins
Expressions
- 1,244