用于增长列表的scalaz流结构
我有一种预感,我可以(应该?)使用scalaz streams来解决我的问题,就像这样 我有一个起始项a。我有一个函数,它接受a并返回a的列表用于增长列表的scalaz流结构,scala,scalaz,akka-stream,scalaz-stream,Scala,Scalaz,Akka Stream,Scalaz Stream,我有一种预感,我可以(应该?)使用scalaz streams来解决我的问题,就像这样 我有一个起始项a。我有一个函数,它接受a并返回a的列表 def doSomething(a : A) : List[A] 我有一个以1项(起始项)开始的工作队列。当我们处理(doSomething)每个项目时,它可能会将许多项目添加到同一工作队列的末尾。然而,在某个时刻(在数百万个项目之后),我们doSomething处理的每个后续项目将开始向工作队列添加越来越少的项目,并且最终不会添加任何新项目(这些项目
def doSomething(a : A) : List[A]
我有一个以1项(起始项)开始的工作队列。当我们处理(doSomething
)每个项目时,它可能会将许多项目添加到同一工作队列的末尾。然而,在某个时刻(在数百万个项目之后),我们doSomething
处理的每个后续项目将开始向工作队列添加越来越少的项目,并且最终不会添加任何新项目(这些项目的doSomething将返回Nil)。这就是我们知道计算最终将终止的方式
假设scalaz streams适用于此,请告诉我实现此功能需要考虑哪些总体结构或类型
一旦用一个“工人”完成了一个简单的实现,我还希望使用多个工人并行处理队列项目,例如,拥有一个5个工人的池(每个工人将把其任务分配给代理来计算doSomething
),因此我需要处理影响(例如工人失败)在这个算法中也是如此。所以“如何”的答案是:
结果:
result: List[Int] = List(2, 4, 6, 1, 3, 5, 0, 2, 4, 1, 3, 0, 2, 1, 0)
但是,您可能会注意到:
1) 我必须解决终止问题。akka stream也存在同样的问题,解决这个问题要困难得多,因为您无法访问队列,也没有自然的背压来保证队列不会因为读卡器速度快而变空
2) 我不得不为输出引入另一个队列(并将其转换为列表
),因为工作队列在计算结束时变为空
所以,这两个库都不能很好地适应这样的需求(有限流),但是scalaz流(在删除scalaz依赖项后将成为“fs2”)足够灵活,可以实现您的想法。最大的问题是,默认情况下,它将按顺序运行。有(至少)两种方法可以加快速度:
1) 将doSomething分为多个阶段,如.flatMap(doSomething1).flatMap(doSomething2).map(doSomething3)
,然后在它们之间放置另一个队列(如果阶段花费的时间相等,则大约快3倍)
2) 并行化队列处理。Akka拥有mapAsync
,它可以自动并行执行map
s。Scalaz流有块——您可以将q分组为5个块,然后并行处理块中的每个元素。无论如何,这两种解决方案(akka和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进行分布式计算,那么看看ApacheSpark,它的scala dsl使用相同的map/flatMap/fold样式。它允许您处理不适合JVM内存的大型集合(通过跨JVM扩展它们),因此您可以通过使用RDD而不是列表来改进@tailrec def calculate
。它还将为您提供在doSomething
中处理故障的工具
另外,这就是为什么我不喜欢使用流媒体库来完成这些任务。流更像是来自某些外部系统(如HttpRequests)的无限流,而不是预定义(甚至大)数据的计算
p.S.2如果您需要反应式(无阻塞),您可以使用Future
(或scalaz.concurrent.Task
)+Future.sequence
工作队列(WQ)是一个列表[a]。当我用doSomething(每个项目返回一个列表[a])处理WQ中的每个项目时,这将以一种纯粹的功能性方式添加到WQ的末尾?谢谢@dk14。我也读了一些关于akka streams和graph dsl的文章,也许这是一个竞争者。综上所述,我们从一个类型为a的项目开始,该项目通过一个函数“doSomething”,该函数返回一个项目列表(零个或多个a项目),然后这些项目逐一反馈到同一个“doSomething”函数中。也许有一种方法可以在akka溪流中很好地表达这一点,即使用剂量测量作为流量函数?感谢@dk14花费时间来解释您所想到的各种解决方案的优点。
@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)