【问题标题】:How to convert Map[A,Future[B]] to Future[Map[A,B]]?如何将 Map[A,Future[B]] 转换为 Future[Map[A,B]]?
【发布时间】:2013-07-02 23:35:24
【问题描述】:

我一直在使用 Scala Akka 库,但遇到了一些问题。正如标题所说,我需要将Map[A, Future[B]] 转换为Future[Map[A,B]]。我知道可以将Future.sequence 用于像列表这样的可迭代对象,但在这种情况下不起作用。

我想知道:Scala 中是否有一种干净的方式来进行这种转换?

【问题讨论】:

  • 我想 Scalaz 会为 Map 提供一个 Traversable(Haskell 的含义)实例,它可以满足您的需求。
  • @copumpkin 现在我将检查 SO 以查看是否有人发帖,Traversable 这个词的 Haskell 意义是什么?
  • 请参阅 these papers 了解原始配方。 Scalaz 版本被命名为Traverse——请参阅下面我的回答,了解如何在此处使用它。

标签: scala akka future


【解决方案1】:

我认为核心 Scala 2.12.x 最简洁的是

val futureMap = Map("a" -> future{1}, "b" -> future{2}, "c" -> future{3}) 

Future.traverse(futureMap.toList) { case (k, fv) => fv.map(k -> _) } map(_.toMap)

【讨论】:

  • traverse是否超时变化?我无法让这段代码工作。我在 scala 2.12
  • @Amiko 我更新了它。我不再写 Scala 了,所以我不能确切地说出它为什么坏了。包括这个以防万一:scalafiddle.io/sf/LBTLWYn/0.
【解决方案2】:

这个解决方案是否可以接受: 如果没有执行上下文,这应该可以工作...

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
    }
  }
}

【讨论】:

    【解决方案3】:

    我会尽量避免使用过度设计的基于 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
    }
    

    【讨论】:

      【解决方案4】:

      这也有效,其想法是使用(地图值的)序列结果来触发一个承诺,表明您可以开始从地图中检索值。 mapValues 为您提供a non-strict view of your map,因此value.get.get 仅在您检索值时应用。没错,你可以保留你的地图! the puzzlers in that link 的免费广告。

      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
      

      【讨论】:

      • 谢谢,@som-snytt!在这种情况下,具体的谜题是#37,碰巧;-)
      【解决方案5】:

      更新:您实际上可以在 Scalaz 7 中获得漂亮的 .sequence 语法而无需大惊小怪:

      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, _] 的实例,这是这里的拼图之一。另一部分是FutureApplicative 实例,它不在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 实例。

      【讨论】:

      • 顺便说一句,SIP-14 包含在 Scala 2.9.3 中
      【解决方案6】:

      看看这是否适合你:

      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,以及与该键相关联的未来结果。从那里你可以sequenceIterable,然后一旦你有聚合Future,映射它并通过TuplesIterable转换为一个映射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 的命中更少。

      【讨论】:

      • 本着同样的精神:Future.sequence(map map {case (a, b) =&gt; b map (a -&gt; _ )}) map (_.toMap)
      • @MarkusMikkolainen,您当然可以通过将代码扩展为多行而不是我提交的一行来使其更具可读性。就效率而言,您将在ExecutionContext 中产生 n+1 次额外点击(如上文所述以及超出sequence 发生的情况)(+1 来自最后的 _.toMap)。应该没什么大不了的,但我会尝试发布一个解决方案,该解决方案更多地反映了sequence 函数,而无需直接使用它,以提高点击ExecutionContext 的效率。
      • @MarkusMikkolainen,好的,更新了另一种解决方案,它反映了现有的 sequence 功能。
      • @ChrisGrimm,据我所知,Future 上的组合子,如 mapflatMaprecover 等不会导致阻塞行为。我相信每个人都只是创建一个新的Promise,它在onComplete 中完成,前面的FutureFuturePromise 是作为组合器的结果返回的。所以本质上你只是将onCompletes链接在一起。
      • @cmbaxter:非常感谢您的回答。它确实帮助我理解了 scala 中 map/flatMap/futures 的性质。 :)
      【解决方案7】:

      只需创建一个新的未来,它等待地图值中的所有未来,然后构建一个地图以返回。

      【讨论】:

        猜你喜欢
        • 2020-07-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-12-20
        • 2021-01-30
        • 1970-01-01
        • 1970-01-01
        • 2018-06-29
        相关资源
        最近更新 更多