我正在尝试确定具有多个累加器阶段的尾递归的最佳模式,通过平衡效率和可读性来判断最佳模式。
特定场景是按轨道和时间到达的帧流。我需要累积曲目信息,然后积累这些曲目:
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
效率的关注在整体方案中是否不合理,或者是否还有另一种模式来进行这种多阶段积累,这种模式优于两者。
答案 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
仍然尾递归只是将自己称为退出条件。