Scala - 基于Future结果谓词排序

时间:2015-05-29 19:45:56

标签: algorithm scala sorting

我有一个我想要排序的对象数组,其中排序的谓词是异步的。 Scala是否具有基于类型签名为(T, T) -> Future[Bool]而不仅仅是(T, T) -> Bool的谓词进行排序的标准或第三方库函数?

或者,是否有其他方法可以构建此代码?我已经考虑过查找列表元素的所有2对排列,在每对上运行谓词并将结果存储在Map((T, T), Bool)或某种结构中,然后对其进行排序 - 但我怀疑它会比起一个天真的排序算法还要执行更多的比较。

2 个答案:

答案 0 :(得分:1)

如果您的谓词是异步的,您可能也希望获得异步结果并避免使用Await阻止线程

如果要根据未来的布尔谓词对List[(T,T)]进行排序,最简单的方法是对List[(T,T,Boolean)]进行排序

因此,如果您有List[(T,T)]和谓词(T, T) -> Future[Bool],那么如何获得List[(T,T,Boolean)]?或者更确切地说是Future[List[(T,T,Boolean)]],因为您希望保持异步行为。

val list: List[(T,T)] = ...
val predicate = ...
val listOfFutures: List[Future[(T,T,Boolean]] = list.map { tuple2 => 
  predicate(tuple2).map( bool => (tuple2._1, tuple2._2, bool) 
}
val futureList: Future[List[(T,T,Boolean)]] = Future.sequence(listOfFutures)
val futureSortedResult: Future[List[(T,T)]] = futureList.map { list =>
    list.sort(_._3).map(tuple3 => (tuple3._1,tuple3._2))
}

这是伪代码,我没有编译它可能没有,但你明白了。

密钥是Future.sequence,非常有用,它以某种方式允许将Monad1[Monad2[X]]转换为Monad2[Monad1[X]],但请注意,如果任何谓词未来失败,则全局排序操作也将是故障。

如果您希望获得更好的性能,那么“批量”调用返回Future[Boolean]的服务可能是更好的解决方案。 例如,您可以设计一个服务(如果您显然拥有它),而不是(T, T) -> Future[Bool],例如List[(T, T)] -> Future[List[(T,T,Bool)],这样您就可以在异步单个调用中获得所需的一切。

答案 1 :(得分:0)

不太令人满意的替代方案是阻止每次比较,直到评估未来。如果评估排序谓词很昂贵,排序将花费很长时间。实际上,这只是将可能并发的程序转换为顺序程序;使用期货的所有好处都将丢失。

import scala.concurrent.duration._
implicit val executionContext = ExecutionContext.Implicits.global

val sortingPredicate: (Int, Int) => Future[Boolean] = (a, b) => Future{
  Thread.sleep(20) // Assume this is a costly comparison
  a < b
}

val unsorted = List(4, 2, 1, 5, 7, 3, 6, 8, 3, 12, 1, 3, 2, 1)
val sorted = unsorted.sortWith((a, b) =>
  Await.result(sortingPredicate(a, b), 5000.millis) // careful: May throw an exception
)
println(sorted) // List(1, 1, 1, 2, 2, 3, 3, 3, 4, 5, 6, 7, 8, 12)

我不知道是否有利用异步比较的开箱即用解决方案。但是,您可以尝试实现自己的排序算法。如果我们考虑平均在O(n log(n))中运行的Quicksort,那么我们实际上可以非常容易地利用异步比较。

如果您不熟悉Quicksort,该算法基本上会执行以下操作

  1. 从集合中选择一个元素(称为 Pivot
  2. 将数据透视与所有剩余元素进行比较。使用小于pivot的元素创建一个集合,使用大于pivot的元素创建一个集合。
  3. 对两个新集合进行排序并将它们连接起来,将枢轴放在中间。
  4. 由于步骤2执行了大量独立比较,我们可以同时评估比较。

    这是一个未经优化的实施:

    object ParallelSort {
      val timeout = Duration.Inf
    
      implicit class QuickSort[U](elements: Seq[U]) {
        private def choosePivot: (U, Seq[U]) = elements.head -> elements.tail
    
        def sortParallelWith(predicate: (U, U) => Future[Boolean]): Seq[U] =
          if (elements.isEmpty || elements.size == 1) elements
          else if (elements.size == 2) {
            if (Await.result(predicate(elements.head, elements.tail.head), timeout)) elements else elements.reverse
          }
          else {
            val (pivot, other) = choosePivot
            val ordering: Seq[(Future[Boolean], U)] = other map { element => predicate(element, pivot) -> element }
    
            // This is where we utilize asynchronous evaluation of the sorting predicate
            val (left, right) = ordering.partition { case (lessThanPivot, _) => Await.result(lessThanPivot, timeout) }
    
            val leftSorted = left.map(_._2).sortParallelWith(predicate)
            val rightSorted = right.map(_._2).sortParallelWith(predicate)
            leftSorted ++ (pivot +: rightSorted)
          }
      }
    
    }
    

    可以使用(与上面相同的例子)如下:

    import ParallelSort.QuickSort
    val sorted2 = unsorted.sortParallelWith(sortingPredicate)
    println(sorted2) // List(1, 1, 1, 2, 2, 3, 3, 3, 4, 5, 6, 7, 8, 12)
    

    请注意,Quicksort的这种实现是否比完全顺序的内置排序算法更快或更慢,这在很大程度上取决于比较的成本:比较阻止的时间越长,上面提到的替代解决方案就越糟糕。在我的机器上,给出了昂贵的比较(20毫秒)和上面的列表,内置排序算法运行在~1200毫秒,而这个自定义Quicksort运行在~200毫秒。如果您担心性能问题,那么您可能想要提出更聪明的方法。 编辑:我刚刚检查了内置排序算法和自定义Quicksort算法执行的比较:显然,对于给定列表(以及我随机输入的其他一些列表)内置算法使用了更多的比较,因此并行执行可能不会带来性能提升。我不了解更大的列表,但您无论如何都必须根据您的特定数据对其进行分析。