Введение
Scala Future
(новое в 2.10 и теперь 2.9.3) является аппликативным функтором, что означает, что если у нас есть проходимый тип F
, мы можем взять F[A]
и функцию A => Future[B]
и превратить их в Future[F[B]]
.
Эта операция доступна в стандартной библиотеке как Future.traverse
. Scalaz 7 также предоставляет более общий traverse
, который мы можем использовать здесь, если импортируем экземпляр аппликативного функтора для Future
из библиотеки scalaz-contrib
.
Эти два метода traverse
ведут себя по-разному в случае потоков. Обход стандартной библиотеки потребляет поток перед возвратом, а Scalaz немедленно возвращает будущее:
import scala.concurrent._
import ExecutionContext.Implicits.global
// Hangs.
val standardRes = Future.traverse(Stream.from(1))(future(_))
// Returns immediately.
val scalazRes = Stream.from(1).traverse(future(_))
Есть и другое отличие, поскольку Лейф Уорнер наблюдает здесь. Стандартная библиотека traverse
немедленно запускает все асинхронные операции, в то время как Scalaz запускает первое, ждет его завершения, запускает второе, ждет его и т.д.
Разное поведение для потоков
Это довольно легко показать это второе различие, написав функцию, которая будет спать несколько секунд для первого значения в потоке:
def howLong(i: Int) = if (i == 1) 10000 else 0
import scalaz._, Scalaz._
import scalaz.contrib.std._
def toFuture(i: Int)(implicit ec: ExecutionContext) = future {
printf("Starting %d!\n", i)
Thread.sleep(howLong(i))
printf("Done %d!\n", i)
i
}
Теперь Future.traverse(Stream(1, 2))(toFuture)
напечатает следующее:
Starting 1!
Starting 2!
Done 2!
Done 1!
И версия Scalaz (Stream(1, 2).traverse(toFuture)
):
Starting 1!
Done 1!
Starting 2!
Done 2!
Что, вероятно, не то, что мы хотим здесь.
А для списков?
Как ни странно, оба обхода ведут себя одинаково в списках - Scalaз не ждет, пока одно будущее завершится, прежде чем начинать следующее.
Другое будущее
Scalaz также включает свой собственный пакет concurrent
с собственной реализацией фьючерсов. Мы можем использовать тот же тип настройки, что и выше:
import scalaz.concurrent.{ Future => FutureZ, _ }
def toFutureZ(i: Int) = FutureZ {
printf("Starting %d!\n", i)
Thread.sleep(howLong(i))
printf("Done %d!\n", i)
i
}
И затем мы получаем поведение Scalaz для потоков как для списков, так и для потоков:
Starting 1!
Done 1!
Starting 2!
Done 2!
Возможно, не удивительно, что пересечение бесконечного потока все равно возвращается немедленно.
Вопрос
На данный момент нам действительно нужна таблица для подведения итогов, но список должен сделать:
- Потоки со стандартным обходом библиотеки: потреблять перед возвратом; не жди каждого будущего.
- Потоки с прохождением Scalaза: немедленно возвращайтесь; дождитесь завершения каждого будущего.
- Scalaz фьючерсы с потоками: немедленно вернуться; дождитесь завершения каждого будущего.
И:
- Списки со стандартным обходом библиотеки: не ждите.
- Списки с прохождением Scalaза: не ждите.
- Scalaz со списками: ждите завершения каждого будущего.
Есть ли в этом смысл? Есть ли "правильное" поведение для этой операции в списках и потоках? Есть ли какая-то причина, по которой "наиболее асинхронное" поведение, т.е. не использовать коллекцию перед возвратом и не ждать завершения каждого будущего, прежде чем перейти к следующему, здесь не представлено?