具有多个累加器阶段的尾递归

时间:2015-05-07 00:09:57

标签: scala recursion tail-recursion case-class

我正在尝试确定具有多个累加器阶段的尾递归的最佳模式,通过平衡效率和可读性来判断最佳模式。

特定场景是按轨道和时间到达的帧流。我需要累积曲目信息,然后积累这些曲目:

case class Frame(trackId: Int, time: Double)
case class Track(id: Int, count: Int, start: Double, end: Double)

我发现更具可读性的模式使用实际的Track案例类作为轨道累加器:

def scanTracks(reader: Stream[Frame]): List[Track] = {  
  def scanTracks2(reader2: Stream[Frame], track: Option[Track], acc: List[Track]): List[Track] =
    if (reader2.isEmpty)
      track.map(_ :: acc).getOrElse(acc)
    else {
      val frame = reader2.head
      track match {
        case None => scanTracks2(reader2.tail, Some(Track(frame.trackId,1,frame.time,0)), acc)
        case Some(t) => if(frame.trackId == t.id)
          scanTracks2(
            reader2.tail,
            Some(t.copy(count = t.count+1,end=frame.time)),
            acc
          )
          else
          scanTracks2(
            reader2.tail,
            Some(Track(frame.trackId,1,frame.time,frame.time)),
            t :: acc
          )
      }
    }
  scanTracks2(reader, None, Nil)
}

我对这种模式的关注是每次递归执行当前轨道的copy以产生新的轨道累加器。虽然head::tail非常有效,但由于它只是现有列表的一个缺点,因此创建Track的新副本似乎效率较低。

另一种方法是显式传递构成轨道的所有值,以便递归只是改变函数参数:

def scanTracks(reader: Stream[Frame]): List[Track] = {  
  def scanTracks2(reader2: Stream[Frame], id: Int, count: Int, start: Double, end: Double, acc: List[Track]): List[Track] =
    if (reader2.isEmpty)
      Track(id, count + 1, start, end) :: acc
    else {
      val frame = reader2.head
      frame.trackId match {
        case -1 => scanTracks2(reader2.tail, frame.trackId, 1, frame.time, 0, acc)
        case x if x == id => scanTracks2(reader2.tail, id, count + 1, start, frame.time, acc)
        case _ =>
          scanTracks2(
            reader2.tail,
            frame.trackId,
            1,
            frame.time,
            0,
            Track(id, count + 1, start, end) :: acc
          )
      }
    }
  scanTracks2(reader, -1, 0, 0, 0, Nil)
}

这会使功能签名变得混乱,并且 kludge 使用-1作为 no track yet 的标记。总的来说,我觉得可读性已经降低了很多。

我的问题是,我对copy效率的关注在整体方案中是否不合理,或者是否还有另一种模式来进行这种多阶段积累,这种模式优于两者。

1 个答案:

答案 0 :(得分:0)

我到目前为止找到的最佳解决方案是为其他累加器阶段内联尾递归函数。鉴于我上面的例子,我改写如下:

def scanTracks(reader: Stream[Frame]): List[Track] = {

  def scanTracks(scanReader: Stream[Frame], acc: List[Track]): List[Track] =
    if (scanReader.isEmpty)
      acc
    else {
      val frame = scanReader.head

      def scanTrack(trackReader: Stream[Frame], count: Int, end: Double): (Stream[Frame], Track) =
        if (trackReader.isEmpty)
          (trackReader, Track(frame.trackId, count, frame.time, end))
        else {
          val frame2 = trackReader.head
          if (frame2.trackId == frame.trackId)
            scanTrack(trackReader.tail, count + 1, frame2.time)
          else
            (trackReader.tail, Track(frame.trackId, count, frame.time, end))
        }

      val (scanReader2, track) = scanTrack(scanReader.tail, 1, frame.time)
      scanTracks(scanReader2, track :: acc)
    }

  scanTracks(reader, Nil)
}

好处是外部scanTracks已大幅减少其签名中传递的状态,而内部scanTrack不必拾取以前在外部的所有状态因为在内部递归期间没有任何改变的内容在范围内是可访问的。 由于内部scanTrack总是在需要另一个scanTracks递归之前完成其递归,scanTracks仍然尾递归只是将自己称为退出条件。