Как преобразовать карту [A, Future [B]] в будущее [Карта [A, B]]?
Я работал с библиотекой Akka Scala и столкнулся с проблемой. Как говорится в названии, мне нужно преобразовать Map[A, Future[B]]
в Future[Map[A,B]]
. Я знаю, что можно использовать Future.sequence
для Iterables, таких как списки, но в этом случае это не работает.
Мне было интересно: есть ли чистый способ в Scala сделать это преобразование?
Ответы
Ответ 1
Посмотрите, работает ли это для вас:
val map = Map("a" -> future{1}, "b" -> future{2}, "c" -> future{3})
val fut = Future.sequence(map.map(entry => entry._2.map(i => (entry._1, i)))).map(_.toMap)
Идея состоит в том, чтобы сопоставить карту с Iterable
для Tuple
ключа карты и результата будущего, привязанного к этому ключу. Оттуда вы можете sequence
, что Iterable
, а затем, когда у вас есть агрегат Future
, сопоставьте его и преобразуйте это Iterable
из Tuples
в карту через toMap
.
Теперь альтернативой этому подходу является попытка сделать что-то похожее на то, что делает функция sequence
, с несколькими настройками. Вы можете написать функцию sequenceMap
следующим образом:
def sequenceMap[A, B](in: Map[B, Future[A]])(implicit executor: ExecutionContext): Future[Map[B, A]] = {
val mb = new MapBuilder[B,A, Map[B,A]](Map())
in.foldLeft(Promise.successful(mb).future) {
(fr, fa) => for (r <- fr; a <- fa._2.asInstanceOf[Future[A]]) yield (r += ((fa._1, a)))
} map (_.result)
}
И затем используйте его в следующем примере:
val map = Map("a" -> future{1}, "b" -> future{2}, "c" -> future{3})
val fut = sequenceMap(map)
fut onComplete{
case Success(m) => println(m)
case Failure(ex) => ex.printStackTrace()
}
Это может быть несколько более эффективным, чем первый пример, поскольку он создает меньше промежуточных коллекций и меньше обращений к ExecutionContext
.
Ответ 2
Я думаю, что наиболее кратким мы можем быть с ядром Scala является
val map = Map("a" -> future{1}, "b" -> future{2}, "c" -> future{3})
Future.traverse(map) { case (k, fv) => fv.map(k -> _) } map(_.toMap)
Ответ 3
Обновление: вы можете получить хороший .sequence
синтаксис Scalaz 7 без лишней суеты:
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.{ Future, future }
import scalaz._, Scalaz.{ ToTraverseOps => _, _ }
import scalaz.contrib.std._
val m = Map("a" -> future(1), "b" -> future(2), "c" -> future(3))
И затем:
scala> m.sequence.onSuccess { case result => println(result) }
Map(a -> 1, b -> 2, c -> 3)
В принципе, не нужно скрывать ToTraverseOps
как это, но на данный момент это трюк. Дополнительную информацию о классе типа Traverse
, зависимостях и т.д. См. В остальном ответе ниже.
Как отмечает copumpkin в комментарии выше, Scalaz содержит Traverse
type class с экземпляром для Map[A, _]
, который является одним из элементов головоломки здесь. Другая часть - это экземпляр Applicative
для Future
, который не находится в Scalaz 7 (который по-прежнему кросс-построен против pre- Future
2.9), но находится в scalaz-contrib
.
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scalaz._, Scalaz._
import scalaz.contrib.std._
def sequence[A, B](m: Map[A, Future[B]]): Future[Map[A, B]] = {
type M[X] = Map[A, X]
(m: M[Future[B]]).sequence
}
Или:
def sequence[A, B](m: Map[A, Future[B]]): Future[Map[A, B]] =
Traverse[({ type L[X] = Map[A, X] })#L] sequence m
Или:
def sequence[A, B](m: Map[A, Future[B]]): Future[Map[A, B]] =
TraverseOpsUnapply(m).sequence
В идеальном мире вы сможете написать m.sequence
, но механизм TraverseOps
, который должен сделать этот синтаксис возможным, в настоящее время не может сказать, как перейти от конкретного экземпляра Map
к соответствующему Traverse
.
Ответ 4
Это также работает, когда идея заключается в использовании результата последовательности (значений карты), чтобы выполнить обещание, в котором говорится, что вы можете начать получать значения с вашей карты. mapValues
дает нестрогий вид вашей карты, поэтому value.get.get
применяется только при извлечении значения. Правильно, вы можете сохранить свою карту! Бесплатное объявление для головоломок в этой ссылке.
import concurrent._
import concurrent.duration._
import scala.util._
import ExecutionContext.Implicits.global
object Test extends App {
def calc(i: Int) = { Thread sleep i * 1000L ; i }
val m = Map("a" -> future{calc(1)}, "b" -> future{calc(2)}, "c" -> future{calc(3)})
val m2 = m mapValues (_.value.get.get)
val k = Future sequence m.values
val p = Promise[Map[String,Int]]
k onFailure { case t: Throwable => p failure t }
k onSuccess { case _ => p success m2 }
val res = Await.result(p.future, Duration.Inf)
Console println res
}
Здесь REPL, где вы видите, заставляет карту m2 печатать все свои значения:
scala> val m2 = m mapValues (_.value.get.get)
m2: scala.collection.immutable.Map[String,Int] = Map(a -> 1, b -> 2, c -> 3)
Это показывает то же самое с фьючерсами, которые все еще в будущем:
scala> val m2 = m mapValues (_.value.get.get)
java.util.NoSuchElementException: None.get
Ответ 5
Просто создайте новое будущее, которое ждет всех фьючерсов в значениях карты, а затем построит карту для возврата.
Ответ 6
Я попытался бы избежать использования сверхнадежных сверхсовременных решений на основе Scalaz (если только ваш проект уже сильно основан на Scalaz и имеет тонны "вычислительно сложного" кода, без обид на "перегруженном" замечании):
// the map you have
val foo: Map[A, Future[B]] = ???
// get a Seq[Future[...]] so that we can run Future.sequence on it
val bar: Seq[Future[(A, B)]] = foo.map { case (k, v) => v.map(k -> _) }
// here you go; convert back `toMap` once it completes
Future.sequence(bar).onComplete { data =>
// do something with data.toMap
}
Однако, следует с уверенностью предположить, что ваши значения карты каким-то образом генерируются из ключей карты, которые изначально находятся в Seq
, таких как List
, и что часть кода который создает исходный Map
, находится под вашим контролем, а не отправляется из другого места. Поэтому я лично взял бы более простой и понятный подход вместо того, чтобы не начинать с Map[A, Future[B]]
в первую очередь.
def fetchAgeFromDb(name: String): Future[Int] = ???
// no foo needed anymore
// no Map at all before the future completes
val bar = personNames.map { name => fetchAgeFromDb(name).map(name -> _) }
// just as above
Future.sequence(bar).onComplete { data =>
// do something with data.toMap
}
Ответ 7
Является ли это решение приемлемым:
без контекста выполнения, это должно работать...
def removeMapFuture[A, B](in: Future[Map[A, Future[B]]]) = {
in.flatMap { k =>
Future.sequence(k.map(l =>
l._2.map(l._1 -> _)
)).map {
p => p.toMap
}
}
}