特别处理Akka流的第一个元素

时间:2016-11-22 13:24:42

标签: scala akka akka-stream

是否有一种惯用的方式以特殊的方式处理Akka流的Source第一个元素?我现在拥有的是:

    var firstHandled = false
    source.map { elem =>
      if(!firstHandled) {
        //handle specially
        firstHandled = true
      } else {
        //handle normally
      }
    }

由于

4 个答案:

答案 0 :(得分:8)

虽然我一般会使用Ramon的答案,但您也可以使用前缀为1的prefixAndTailflatMapConcat来实现类似的目标:

val src = Source(List(1, 2, 3, 4, 5))
val fst = Flow[Int].map(i => s"First: $i")
val rst = Flow[Int].map(i => s"Rest:  $i")

val together = src.prefixAndTail(1).flatMapConcat { case (head, tail) =>
  // `head` is a Seq of the prefix elements, which in our case is
  // just the first one. We can convert it to a source of just
  // the first element, processed via our fst flow, and then
  // concatenate `tail`, which is the remainder...
  Source(head).via(fst).concat(tail.via(rst))
}

Await.result(together.runForeach(println), 10.seconds)
// First: 1
// Rest:  2
// Rest:  3
// Rest:  4
// Rest:  5

这当然不仅适用于第一个项目,而且适用于第一个 N 项目,条件是这些项目将被视为严格的集合。

答案 1 :(得分:3)

使用zipWith

您可以使用仅在第一次返回Source的布尔值来压缩原始true。然后可以处理此压缩源。

首先,我们需要一个发布布尔人的来源:

//true, false, false, false, ...
def firstTrueIterator() : Iterator[Boolean] = 
  (Iterator single true) ++ (Iterator continually false)

def firstTrueSource : Source[Boolean, _] = 
  Source fromIterator firstTrueIterator

然后我们可以定义一个处理两种不同情况的函数:

type Data = ???
type OutputData = ???

def processData(data : Data, firstRun : Boolean) : OutputData = 
  if(firstRun) { ... }
  else { ... }

然后可以在原始来源的zipWith中使用此功能:

val originalSource : Source[Data,_] = ???    

val contingentSource : Source[OutputData,_] =
  originalSource.zipWith(firstTrueSource)(processData)

使用有状态流程

您可以创建一个Flow,其中包含与问题中的示例类似但状态更为实用的状态:

def firstRunner(firstCall : (Data) => OutputData,
                otherCalls : (Data) => OutputData) : (Data) => OutputData = {
  var firstRun = true
  (data : Data) => {
    if(firstRun) {
      firstRun = false
      firstCall(data)
    }
    else
      otherCalls(data)
  }
}//end def firstRunner

def firstRunFlow(firstCall :  (Data) => OutputData, 
                 otherCalls : (Data) => OutputData) : Flow[Data, OutputData, _] = 
  Flow[Data] map firstRunner(firstCall, otherCalls)

此流程可以应用于您的原始资源:

def firstElementFunc(data : Data) : OutputData = ???
def remainingElsFunc(data : Data) : OutputData = ???

val firstSource : Source[OutputData, _] = 
  originalSource via firstRunFlow(firstElementFunc,remainingElseFunc)

"惯用语"

直接回答你的问题需要规定"惯用的方法"。我最后回答那个部分,因为它是编译器最不可验证的,因此更接近意见。我永远不会声称自己是惯用代码的有效分类器。

我对akka-streams的个人经验是,最好将我的视角转换为想象一个Data元素的实际流(我想到的是带有厢式车的火车)。我是否需要将其分解为多个固定尺寸的列车?只有某些厢式车可以通过吗?我可以并排安装另外一辆包含Boolean汽车的列车,这些汽车可以向前方发出信号吗?由于我对流(火车)的关注,我更喜欢zipWith方法。我最初的方法是始终使用连接在一起的其他流部分。

此外,我发现最好尽可能在akka Stream组件中嵌入尽可能少的代码。 firstTrueIteratorprocessData根本不依赖于akka。同时,firstTrueSourcecontingentSource定义几乎没有逻辑。这允许您独立于笨重的ActorSystem测试逻辑,并且可以在Futures或Actors中使用guts。

答案 2 :(得分:0)

虽然我更喜欢使用zip的方法,但是也可以使用statefulMapConcat

source
  .statefulMapConcat { _ =>
        var firstRun = true
        elem => {
          if (firstRun) {
            //first
            firstRun = false
          } else {
            //not first            
          }
        }
      }

答案 3 :(得分:0)

您可以使用prepend在流之前添加源。只需在流中添加单个物料源,将其排干后,其余原始源将继续。

https://doc.akka.io/docs/akka/current/stream/operators/Source-or-Flow/prepend.html

 Source(List(1, 2, 3))
  .prepend(Source.single(0))
  .runWith(Sink.foreach(println))

0 1 2 3