用于增长列表的scalaz流结构

时间:2015-12-07 00:14:51

标签: scala scalaz akka-stream scalaz-stream

我有预感,我可以(应该?)使用scalaz-streams来解决我的问题。

我有一个起始项目A.我有一个带A的函数并返回A的列表。

def doSomething(a : A) : List[A]

我有一个以1项(起始项)开头的工作队列。当我们处理(doSomething)每个项目时,它可能会将许多项目添加到同一工作队列的末尾。然而,在某些时候(在数百万项之后)我们doSomething开始的每个后续项目将开始向工作队列添加越来越少的项目,并且最终不会添加新项目(doSomething将为这些项目返回Nil)。这就是我们知道计算最终会终止的方式。

假设scalaz-stream适用于此可以请给我一些关于我应该考虑实现这个的整体结构或类型的提示?

使用单个" worker"进行简单的实施完成后,我还想使用多个工作程序并行处理队列项,例如拥有一个由5名工人组成的工作人员(并且每个工人都会将其任务交给代理人来计算doSomething),因此我需要在此算法中处理效果(例如工人失败)。

1 个答案:

答案 0 :(得分:2)

所以答案是"怎么样?"是:

import scalaz.stream._
import scalaz.stream.async._
import Process._

def doSomething(i: Int) = if (i == 0) Nil else List(i - 1)

val q = unboundedQueue[Int]
val out = unboundedQueue[Int]

q.dequeue
 .flatMap(e => emitAll(doSomething(e)))
 .observe(out.enqueue)
 .to(q.enqueue).run.runAsync(_ => ()) //runAsync can process failures, there is `.onFailure` as well

q.enqueueAll(List(3,5,7)).run
q.size.continuous
 .filter(0==)
 .map(_ => -1)
 .to(out.enqueue).once.run.runAsync(_ => ()) //call it only after enqueueAll

import scalaz._, Scalaz._
val result = out
  .dequeue
  .takeWhile(_ != -1)
  .map(_.point[List])
  .foldMonoid.runLast.run.get //run synchronously

结果:

result: List[Int] = List(2, 4, 6, 1, 3, 5, 0, 2, 4, 1, 3, 0, 2, 1, 0)

但是,您可能会注意到:

1)我不得不解决终止问题。同样的问题对于akka-stream而言要难以解决,因为你无法访问队列而没有自然的背压来保证队列因为快速阅读而不会变空。

2)我必须为输出引入另一个队列(并将其转换为List),因为在计算结束时工作的队列变为空。

因此,两个库都不太适应这样的要求(有限流),但是scalaz-stream(在删除scalaz依赖之后将成为" fs2")足够灵活,可以实现您的想法。大"但"关于它,它将默认按顺序运行。有(至少)两种方法可以加快速度:

1)将你的doSomething分成几个阶段,比如.flatMap(doSomething1).flatMap(doSomething2).map(doSomething3),然后在它们之间放置另一个队列(如果阶段花费相同的时间,大约快3倍)。

2)并行化队列处理。 Akka有mapAsync - 它可以自动并行map个。 Scalaz-stream有块 - 你可以将你的q分组为let~5,然后并行处理chunk中的每个元素。无论如何,两种解决方案(akka vs scalaz)都不适合使用一个队列作为输入和输出。

但是,再次,它太复杂了而且毫无意义,因为有一种经典的简单方式:

@tailrec def calculate(l: List[Int], acc: List[Int]): List[Int] = 
  if (l.isEmpty) acc else { 
    val processed = l.flatMap(doSomething) 
    calculate(processed, acc ++ processed) 
  }

scala> calculate(List(3,5,7), Nil)
res5: List[Int] = List(2, 4, 6, 1, 3, 5, 0, 2, 4, 1, 3, 0, 2, 1, 0)

这是并行化的:

@tailrec def calculate(l: List[Int], acc: List[Int]): List[Int] = 
  if (l.isEmpty) acc else { 
    val processed = l.par.flatMap(doSomething).toList
    calculate(processed, acc ++ processed) 
  }

scala> calculate(List(3,5,7), Nil)
res6: List[Int] = List(2, 4, 6, 1, 3, 5, 0, 2, 4, 1, 3, 0, 2, 1, 0)

所以,是的,我会说scalaz-stream和akka-streams都不符合你的要求;然而经典的scala平行系列完美契合。

如果需要跨多个JVM进行分布式计算 - 看一下Apache Spark,它的scala-dsl使用相同的map / flatMap / fold样式。它允许您处理大型集合(通过在JVM上扩展它们),这些集合不适合JVM的内存,因此您可以通过使用RDD而不是{来改进@tailrec def calculate {1}}。它还将为您提供List内处理失败的信息。

P.S。所以这就是为什么我不喜欢使用流式库来完成这些任务的原因。流更像是来自某些外部系统(如HttpRequests)的无限流,而不是关于预定义(甚至大)数据的计算。

P.S.2如果你需要类似反应(没有阻止),你可以使用doSomething(或Future)+ scalaz.concurrent.Task