Hi everyone,
Thanks for raising this conversation, Matthew.
Fortunately/disappointingly (depending how you want to view it) I’ve already implemented most, if not all, of the viable optimizations here, which also include JMH benches: https://github.com/viktorklang/scala-futures/tree/wip-optimizations-√
It’s still a work in progress, however I am rather confident that there’ll be some nice, non-breaking, performance improvements coming out of this.
Sorry for the long story below, it’s only relevant if you enjoy some Future/Promise backstory/rationale:
I, personally, have realized that it is very important that, before I suggest what I consider to be improvements, what something is trying to achieve, as I can make something like Future blazing fast if I am willing to compromise on resource-safety, fairness, determinism, memory-footprint, extensibility, compatibility etc.
What Future/Promise has achieved—from my experience using it, and my interactions with users online and offline—is ubiquity. From what I can tell, it is used by practically every Scala developer out there, which is rather cool, but it also means that it must change only in very responsible ways.
For the casual reader of this thread, you may not know why the following things are as they are, so I thought I’d take the time to outline, from memory, what is intended to be achieved by the following decisions:
ExecutionContext: by having the piece of code which wants to compute things having to specify where, leads to: determinism (no longer racing between completer and invoker), resource-safety (added logic cannot poison the pools which produce the values), fairness (which is up to the ExecutionContext implementation to deliver), extensibility (it’s easy to integrate with most execution engines / thread pools), compatibility (it has very few methods so easy to keep compatible)
Future/Promise: By having a separation between read-capability and write-capability, it is much easier to reason about what code wants to be able to do, and what code does.
Absence of cancellation: This was very consciously decided, if Future can be cancelled it is no longer read-only, which means that any reader can mess up the other readers’ reads if their Future is shared. This leads to tons of defensive copying, and worse, it is no longer clear in the code what will happen, or which defensive copies are actually needed.
Also, semantically, a Future is a placeholder for a value that might not yet exist, and as such, it doesn’t really make sense to make it cancellable—a Task is something which could be cancelled, or perhaps something like a SubmittedTask, anyway, I digress.
I guess what I’m trying to say is, I think Future will be possible to improve, performance wise, in some cases by quite a lot, and in some cases perhaps rather modestly. All of this without breaking source compatibility. (And I’d be extremely cautious to introduce user-breaking changes, just because it is so widely used.)
Also, I’d think a Task-like abstraction/construct in the stdlib would be a good thing, to provide for that nice bridge between a lazy and a strict construct.
Cheers,
√