This is the third of several posts describing the evolution of scala.concurrent.Future in Scala 2.12.x.
For the previous post, click here.
Missing canonical combinators: transform
Now, if you’re thinking “Hey Viktor, there’s already a transform-method on Future!” then you’re most definitely right. And I’ll explain myself.
When the old transform1 was added, many moons ago, it was a means of mapping over successes and failures. However, the astute reader might notice that it is asymmetric: an exception thrown when mapping over the successful case will result in a failed Future, but there is no way to turn a failed case into a successful one.
One way of looking at a scala.concurrent.Future is that it’s an “Eventually[Try[_]]”, in other words, a container which will eventually contain a scala.util.Try parameterized by some type.
What if we had a way to transform Future more generically, and symmetrically?
Perhaps its signature ought to look something like this:
def transform[S](f: Try[T] => Try[S])(implicit executor: ExecutionContext): Future[S]
So for instance, that would mean that Future.map could be implemented as follows (and is!):
def map[S](f: T => S)(implicit executor: ExecutionContext): Future[S] = transform(_.map(f))
And Future.recover is possible to implement as this (and is!):
def recover[U >: T](pf: PartialFunction[Throwable, U])(implicit executor: ExecutionContext): Future[U] =
transform { _ recover pf }
It also makes it dead simple to lift a Future[T] to a Future[Try[T]]:
val someFuture: Future[String] = …
val lifted/*: Future[Try[String]]*/ = someFuture.transform(Try(_))
Nice, right?
Benefits:
- Strictly more powerful / generic than the previous
transform-method - Straightforward to consume/transform a
FuturesTry[_]without having to useonCompleteor unboxingFuture.value - For implementors of the
Futuretrait, less methods to implement
Click here for the next part in this blog series.
Cheers,
√
1:
def transform[S](s: T => S, f: Throwable => Throwable)(implicit executor: ExecutionContext): Future[S]
