Подтвердить что ты не робот

Как преобразовать карту [A, Future [B]] в будущее [Карта [A, B]]?

Я работал с библиотекой Akka Scala и столкнулся с проблемой. Как говорится в названии, мне нужно преобразовать Map[A, Future[B]] в Future[Map[A,B]]. Я знаю, что можно использовать Future.sequence для Iterables, таких как списки, но в этом случае это не работает.

Мне было интересно: есть ли чистый способ в Scala сделать это преобразование?

4b9b3361

Ответ 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
    }
  }
}