-1

I am Java developer and learning Scala at the moment. It is generally admitted, that Java is more verbose then Scala. I just need to call 2 or more methods concurrently and then combine the result. Official Scala documentation at docs.scala-lang.org/overviews/core/futures.html suggests to use for-comprehention for that. So I used that out-of-the-box solution straightforwardly. Then I thought how I would do it with CompletableFuture and was surprised that it produced more concise and faster code, then Scala's Future

Let's consider a basic concurrent case: summing up values in array. For simplicity, let's split array in 2 parts(hence it will be 2 worker threads). Java's sumConcurrently takes only 4 LOC, while Scala's version requires 12 LOC. Also Java's version is 15% faster on my computer.

Complete code, not benchmark optimised. Java impl.:

public class CombiningCompletableFuture {
    static int sumConcurrently(List<Integer> numbers) throws ExecutionException, InterruptedException {
        int mid = numbers.size() / 2;
        return CompletableFuture.supplyAsync( () -> sumSequentially(numbers.subList(0, mid)))
                .thenCombine(CompletableFuture.supplyAsync( () -> sumSequentially(numbers.subList(mid, numbers.size())))
                , (left, right) -> left + right).get();
    }

    static int  sumSequentially(List<Integer> numbers) {
        try {
            Thread.sleep(TimeUnit.SECONDS.toMillis(1));
        } catch (InterruptedException ignored) {     }
        return numbers.stream().mapToInt(Integer::intValue).sum();
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        List<Integer> from1toTen = IntStream.rangeClosed(1, 10).boxed().collect(toList());
        long start = System.currentTimeMillis();
        long sum = sumConcurrently(from1toTen);
        long duration = System.currentTimeMillis() - start;
        System.out.printf("sum is %d in %d ms.", sum, duration);
    }
}

Scala's impl.:

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.{Await, Future}
import scala.concurrent.duration._


object CombiningFutures extends App {
  def sumConcurrently(numbers: Seq[Int]) = {
    val splitted = numbers.splitAt(5)
    val leftFuture = Future {
      sumSequentally(splitted._1)
    }
    val rightFuture = Future {
      sumSequentally(splitted._2)
    }
    val totalFuture = for {
      left <- leftFuture
      right <- rightFuture
    } yield left + right
    Await.result(totalFuture, Duration.Inf)
  }
  def sumSequentally(numbers: Seq[Int]) = {
    Thread.sleep(1000)
    numbers.sum
  }

  val from1toTen = 1 to 10
  val start = System.currentTimeMillis
  val sum = sumConcurrently(from1toTen)
  val duration = System.currentTimeMillis - start
  println(s"sum is $sum in $duration ms.")
}

Any explanations and suggestions how to improve Scala code without impacting readability too much?

Fedor
  • 559
  • 1
  • 7
  • 19
  • 1
    I don't understand the significance of this question... as I can not help but notice that you have put too much effort in somehow fitting that Java code in 4 lines and similar effort in somehow expanding Scala code into 15 lines. You are free to like Java futures comparable to Scala futures as all these are nothing but abstractions and one should decide bases on their own preferences. Its just that title `Java CompletableFuture is more concise and faster then Scala Future` makes me wonder about the whole point of asking this "question" (if we can even call it a question). – sarveshseri Jan 20 '18 at 17:10
  • There are some challenges with the way your code is written: it doesn't use the same data structures for the computation(s), it doesn't use the same thread pool and configuration, it doesn't use a proper benchmarking setup, and the code hasn't been optimized for both implementations. – Viktor Klang Jan 20 '18 at 18:11
  • guys, I understand the code is not benchmark optimised. I just need to call 2 or more methods concurrently and then combine the result. Official Scala documentation at https://docs.scala-lang.org/overviews/core/futures.html suggests to use ```for-comprehention``` for that. So I used that out-of-the-box solution straightforwardly. Then I thought how I would do it with ```CompletableFuture``` and was surprised that it produced less verbose solution. – Fedor Jan 20 '18 at 18:42
  • If you count `thenCombine(CompletableFuture.supplyAsync( () -> sumSequentially(numbers.subList(mid, numbers.size())))` as one line. Then yes, very concise. – sarveshseri Jan 20 '18 at 19:27

2 Answers2

4

A verbose scala version of your sumConcurrently,

def sumConcurrently(numbers: List[Int]): Future[Int] = {
  val (v1, v2) = numbers.splitAt(numbers.length / 2)
  for {
    sum1 <- Future(v1.sum)
    sum2 <- Future(v2.sum)
  } yield sum1 + sum2
}

A more concise version

def sumConcurrently2(numbers: List[Int]): Future[Int] = numbers.splitAt(numbers.length / 2) match {
  case (l1, l2) => Future.sequence(List(Future(l1.sum), Future(l2.sum))).map(_.sum)
}

And all this is because we have to partition the list. Lets say we had to write a function which takes a few lists and returns the sum of their sum's using multiple concurrent computations,

def sumConcurrently3(lists: List[Int]*): Future[Int] =
  Future.sequence(lists.map(l => Future(l.sum))).map(_.sum)

If the above looks cryptic... then let me de-crypt it,

def sumConcurrently3(lists: List[Int]*): Future[Int] = {
  val listOfFuturesOfSums = lists.map { l => Future(l.sum) }
  val futureOfListOfSums = Future.sequence(listOfFuturesOfSums)
  futureOfListOfSums.map { l => l.sum }
}

Now, whenever you use the result of a Future (lets say the future completes at time t1) in a computation, it means that this computation is bound to happen after time t1. You can do it with blocking like this in Scala,

val sumFuture = sumConcurrently(List(1, 2, 3, 4))

val sum = Await.result(sumFuture, Duration.Inf)

val anotherSum = sum + 100

println("another sum = " + anotherSum)

But what is the point of all that, you are blocking the current thread while for the computation on those threads to finish. Why not move the whole computation into the future itself.

val sumFuture = sumConcurrently(List(1, 2, 3, 4))

val anotherSumFuture = sumFuture.map(s => s + 100)

anotherSumFuture.foreach(s => println("another sum = " + s))

Now, you are not blocking anywhere and the threads can be used anywhere required.

Future implementation and api in Scala is designed to enable you to write your program avoiding blocking as far as possible.

sarveshseri
  • 13,738
  • 28
  • 47
  • Thanks for you suggestion. However, both ```sumConcurrently2``` and ```sumConcurrently3``` return ```Future[Int]``` while ```Int``` is expected. I assume some blocking operation required. Also both alternatives look cryptic. For me, at least. – Fedor Jan 20 '18 at 18:35
  • What's the driver behind returning a plain Int, you'd most likely gain most from using parallel streams in Java and parallel collections in Scala? – Viktor Klang Jan 20 '18 at 18:38
  • I explore approaches on how to execute long-running tasks(you see Thread.sleep) in parallel and combine the result, summing up array is just for demonstration. It also could be remote calls,e.t.c. – Fedor Jan 20 '18 at 18:49
1

For the task at hand, the following is probably not the tersest option either:

def sumConcurrently(numbers: Vector[Int]): Future[Int] = {
  val (v1, v2) = numbers.splitAt(numbers.length / 2)
  Future(v1.sum).zipWith(Future(v2.sum))(_ + _)
}

As I mentioned in my comment there are several issues with your example.

Viktor Klang
  • 26,479
  • 7
  • 51
  • 68
  • Thanks for your comment. As I explained, I explore approaches on how to execute long-running tasks(you see ```Thread.sleep```) in parallel and combine the result, summing up array is just for demonstration. It also could be remote calls,e.t.c. – Fedor Jan 20 '18 at 18:48
  • That makes sense, but returning Int iso future[Int] makes less sense if you involve long-running tasks, and especially, remote calls. – Viktor Klang Jan 20 '18 at 20:08