Chapter 7 in FP in Scala deals with creating a purely functional library for handling concurrency. To that end, it defines a type
type Par[A] = ExecutorService => Future[A]
and a set of useful functions such as fork
def fork[A] (a: => Par[A]): Par[A] =
es => es.submit(new Callable[A] {
def call = a(es).get
})
One of the exercises is about the function sequence with the following signature
def sequence[A](ps: List[Par[A]]): Par[List[A]]
The solution using foldRight is straightforward. However the authors included two other versions as answers, one of which states the following
// This implementation forks the recursive step off to a new logical thread,
// making it effectively tail-recursive. However, we are constructing
// a right-nested parallel program, and we can get better performance by
// dividing the list in half, and running both halves in parallel.
// See `sequenceBalanced` below.
def sequenceRight[A](as: List[Par[A]]): Par[List[A]] =
as match {
case Nil => unit(Nil)
case h :: t => map2(h, fork(sequenceRight(t)))(_ :: _)
}
I am not quite sure what is meant by "effectively tail recursive". From the definition of fork it is clear that it accepts a by name parameter and so map2(h, fork(sequenceRight(t)))(_ :: _) will not evaluate the second parameter (until the executor service is provided). But that doesn't tell me how and why it is "effectively tail recursive".
Let's take some List(a, b, c). After passing it into sequenceRight it will turn into:
map2(
a,
fork(
map2(
b,
fork(
map2(
c,
fork(unit(Nil)
)(_ :: _)
)
)(_ :: _)
)
)(_ :: _)
This isn't tail recursive at all and compiler cannot treat it as one. However, when you would evaluate how it would be executed:
fork would make whatever you pass to it async, so it would return immediately,map2 implementation will not block the execution until fork is executed to apply the function passed to map2, instead it would asynchronously transform the result calculated in fork to prepend the valueFuture let you treat ExecutorService+Future like a trampolineAs a result what actually happens is:
sequenceRight(List(a, b, c)) call `map2(a, fork(sequenceRight(List(b, c))(_ :: _)
a will complete when it will complete but we can hold it as value even nowfork(sequenceRight(List(b, c)) is scheduled, but we aren't waiting until it complete, we can pass it around alreadyFuture that will combine the result of the 2 above (and return it) without waiting for any of them completing!Future is returned immediately! It still runs, but this one call is finished!Futuresc and fork(unit(Nil)) completes, rResult :: Nil is computedbResult :: cResult :: NilIn other words tail-recursive refers to recursive calls being rewritten into while loop to avoid stack overflow. But stack overflow is only an issue if recursive calls are being made synchronously. If you are returning something async, then the backtracing is shoved to ExecutionServices and Futures' observers, so they are hidden in a heap. From that point of view they solve the same issue of stack-overflow as tail-recursive calls, so "in spirit" they could be considered somewhat similar.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With