stateless-future
is a set of DSL for asynchronous programming, in the pure functional favor.
import scala.concurrent.duration._
import scala.util.control.Exception.Catcher
import com.qifun.statelessFuture.Future
val executor = java.util.concurrent.Executors.newSingleThreadScheduledExecutor
// Manually implements a stateless future, which is the asynchronous version of `Thread.sleep()`
def asyncSleep(duration: Duration) = new Future[Unit] {
import scala.util.control.TailCalls._
def onComplete(handler: Unit => TailRec[Unit])(implicit catcher: Catcher[TailRec[Unit]]) = {
executor.schedule(new Runnable {
def run() {
handler().result
}
}, duration.length, duration.unit)
done()
}
}
// Without the keyword `new`, you have the magic version of `Future` constructor,
// which enables the magic postfix `await`.
val sleep10seconds = Future {
var i = 0
while (i < 10) {
println(s"I have sleeped $i times.")
// The magic postfix `await` invokes the asynchronous method `asyncSleep`.
// It looks like normal `Thread.sleep()`, but does not block any thread.
asyncSleep(1.seconds).await
i += 1
}
i
}
// When `sleep10seconds` is running, it could report failture to this catcher
implicit def catcher: Catcher[Unit] = {
case e: Exception => {
println("An exception occured when I was sleeping: " + e.getMessage)
}
}
// A stateless future instance is lazy, only evaluating when you query it.
println("Before the evaluation of the stateless future `sleep10seconds`.")
for (total <- sleep10seconds) {
println("After the evaluation of the stateless future `sleep10seconds`.")
println(s"I sleeped $total times in total.")
executor.shutdown()
}
Run it and you will see the output:
Before evaluation of the stateless future `sleep10seconds`.
I have sleeped 0 times.
I have sleeped 1 times.
I have sleeped 2 times.
I have sleeped 3 times.
I have sleeped 4 times.
I have sleeped 5 times.
I have sleeped 6 times.
I have sleeped 7 times.
I have sleeped 8 times.
I have sleeped 9 times.
After evaluation of the stateless future `sleep10seconds`.
I sleeped 10 times in total.
There are two sorts of API to use a stateless future, the for-comprehensions style API and "A-Normal Form" style API.
The for-comprehensions style API for stateless-future
is like the for-comprehensions for scala.concurrent.Future.
for (total <- sleep10seconds) {
println("After evaluation of the stateless future `sleep10seconds`")
println(s"I sleeped $total times in total.")
executor.shutdown()
}
A notable difference between the two for-comprehensions implementation is the required implicit parameter. A scala.concurrent.Future
requires an ExecutionContext
, while a stateless future requires a Catcher
.
import scala.util.control.Exception.Catcher
implicit def catcher: Catcher[Unit] = {
case e: Exception => {
println("An exception occured when I was sleeping: " + e.getMessage)
}
}
"A-Normal Form" style API for stateless futures is like the pending proposal scala.async.
val sleep10seconds = Future {
var i = 0
while (i < 10) {
println(s"I have sleeped $i times")
// The magic postfix `await` invokes asynchronous method like normal `Thread.sleep()`,
// and does not block any thread.
asyncSleep(1.seconds).await
i += 1
}
i
}
The Future
functions for stateless futures correspond to async
method in Async
, and the await
postfixes to stateless futures corresponds to await
method in Async
.
Regardless of the familiar veneers between stateless futures and scala.concurrent.Future
, I have made some different designed choices on stateless futures.
The stateless futures are pure functional, they will never store result values or exceptions. Instead, stateless futures evaluate lazily, and they do the same work for every time you invoke foreach
or onComplete
. The behavior of stateless futures is more like monads in Haskell than futures in Java.
Also, there is no isComplete
method in stateless futures. As a result, the users of stateless futures are forced not to share futures between threads, not to check the states in futures. They have to care about control flows instead of threads, and build the control flows by defining stateless futures.
By the way, stateless futures can be easy adapted to other stateful future implementation, and then the users can use the other future's stateful API on the adapted futures. For example, you can perform scala.concurrent.Await.result
on a stateless future which is implicitly adapted to a Future.ToConcurrentFuture
. By this approach, I have ported the most of scala.async
test cases for stateless futures.
There are too many threading models and implimentations in the Java/Scala world, java.util.concurrent.Executor
, scala.concurrent.ExecutionContext
, javax.swing.SwingUtilities.invokeLater
, java.util.Timer
, ... It is very hard to communicate between threading models. When a developer is working with multiple threading models, he must very carefully pass messages between threading models, or he have to maintain bulks of synchronized
methods to properly deal with the shared variables between threads.
Why does he need multiple threading models? Because the libraries that he uses depend on different threading modes. For example, you must update Swing components in the Swing's UI thread, you must specify java.util.concurrent.ExecutionService
s for java.nio.channels.CompletionHandler
, and, you must specify scala.concurrent.ExecutionContext
s for scala.concurrent.Future
and scala.async.Async
. Oops!
Think about somebody who uses Swing to develop a text editor software. He wants to create a state machine to update UI. He have heard the cool scala.async
, then he uses the cool "A-Normal Form" expression in async
to build the state machine that updates UI, and he types import scala.concurrent.ExecutionContext.Implicits._
to suppress the compiler errors. Everything looks pretty, except the software always crashes.
Fortunately, stateless-future
depends on none of these threading model, and cooperates with all of these threading models. If the poor guy tries stateless future, replacing async { }
to stateless-future
's Future { }
, deleting the import scala.concurrent.ExecutionContext.Implicits._
, he will find that everything looks pretty like before, and does not crash any more. That's why threading-free model is important.
There were two Future
implementations in Scala standard library, scala.actors.Future
and scala.concurrent.Future
. scala.actors.Future
s are not designed to handling exceptions, since exceptions are always handled by actors. There is no way to handle a particular exception in a particular subrange of an actor.
Unlike scala.actors.Future
s, scala.concurrent.Future
s are designed to handle exceptions. But, unfortunately, scala.concurrent.Future
s provide too many mechanisms to handle an exception. For example:
import scala.concurrent.Await
import scala.concurrent.ExecutionContext
import scala.concurrent.duration.Duration
import scala.util.control.Exception.Catcher
import scala.concurrent.forkjoin.ForkJoinPool
val threadPool = new ForkJoinPool()
val catcher1: Catcher[Unit] = { case e: Exception => println("catcher1") }
val catcher2: Catcher[Unit] = {
case e: java.io.IOException => println("catcher2")
case other: Exception => throw new RuntimeException(other)
}
val catcher3: Catcher[Unit] = {
case e: java.io.IOException => println("catcher4")
case other: Exception => throw new RuntimeException(other)
}
val catcher4: Catcher[Unit] = { case e: Exception => println("catcher4") }
val catcher5: Catcher[Unit] = { case e: Exception => println("catcher5") }
val catcher6: Catcher[Unit] = { case e: Exception => println("catcher6") }
val catcher7: Catcher[Unit] = { case e: Exception => println("catcher7") }
def future1 = scala.concurrent.future { 1 }(ExecutionContext.fromExecutor(threadPool, catcher1))
def future2 = scala.concurrent.Future.failed(new Exception)
val composedFuture = future1.flatMap { _ => future2 }(ExecutionContext.fromExecutor(threadPool, catcher2))
composedFuture.onFailure(catcher3)(ExecutionContext.fromExecutor(threadPool, catcher4))
composedFuture.onFailure(catcher5)(ExecutionContext.fromExecutor(threadPool, catcher6))
try { Await.result(composedFuture, Duration.Inf) } catch { case e if catcher7.isDefinedAt(e) => catcher7(e) }
Is any sane developer able to tell which catchers will receive the exceptions?
There are too many concepts about exceptions when you work with scala.concurrent.Future
. You have to remember the different exception handling strategies between flatMap
, recover
, recoverWith
and onFailure
, and the difference between scala.concurrent.Future.failed(new Exception)
and scala.concurrent.future { throw new Exception }
.
scala.async
does not make things better, because scala.async
will produce compiler errors for every await
in try
statements.
Fortunately, you can get rid of all those concepts if you switch to stateless-future
. There is no catcher
implicit parameter in flatMap
or map
in stateless Futures, nor onFailure
nor recover
method at all. You just simply try
, and things get done. See the examples to learn that.
Tail call optimization is an important feature for pure functional programming. Without tail call optimization, many recursive algorithm will fail at run-time, and you will get the well-known StackOverflowError
.
The Scala language provides scala.annotation.tailrec
to automatically optimize simple tail recursions, and scala.util.control.TailCalls
to manually optimize complex tail calls.
stateless-future
project internally bases on scala.util.control.TailCalls
, and automatically performs tail call optimization in the magic Future
blocks, without any additional special syntax.
See this example. It just works, and no StackOverflowError
or OutOfMemoryError
occures. Note that if you port this example for scala.async
it will throw OutOfMemoryError
or TimeoutException
.
There was a continuation plugin for Scala. The continuation plugin also provide a DSL to define control flows like stateless-future
or scala.async
. I created the following table to compare the three DSL:
stateless-future | scala.concurrent.Future and scala.async | scala.util.continuations | |
---|---|---|---|
Stateless | Yes | No | Yes |
Threading-free | Yes | No | Yes |
Exception handling in "A-Normal Form" | Yes | No | No |
Tail call optimization in "A-Normal Form" | Yes | No | No |
Pattern matching in "A-Normal Form" | Yes | Yes | Buggy |
Lazy val in "A-Normal Form" | No, because of some underlying scala.reflect bugs | Only for those not contain await
|
Buggy |
Put these lines in your build.sbt
if you use Sbt:
libraryDependencies += "com.qifun" %% "stateless-future" % "0.1"
stateless-future
should work with Scala 2.10.3, 2.10.4, or 2.11.0.
lazy val
s in magic Future
block are not supported.val
with same name in one Future
block, the last val
may be referred unexpectly.Clone stateless-future-test and run the test cases to check these limitations.
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。
1. 开源生态
2. 协作、人、软件
3. 评估模型