0Pricing
Scala for Backend Engineering & Functional Programming · 课时

累加器模式

在递归过程中传递状态。

累加器模式 是 CoddyKit 上的免费 Scala for Backend Engineering & Functional Programming 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Scala for Backend Engineering & Functional Programming 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Scala for Backend Engineering & Functional Programming 课程共包含 4 节课。

为什么使用累加器

普通递归会在递归调用返回后,沿着调用栈向上返回的过程中构建结果。

累加器则会将当前结果带入每次调用中,因此到达基本情况时,答案已经准备就绪。

这一小步改变可以实现尾递归,并使栈空间保持恒定。

辅助函数

累加器模式使用一个内部辅助函数,并为它增加一个参数:目前为止的结果。

外部函数只需使用一个初始值启动它,通常是 0 或空列表。

def sum(xs: List[Int]): Int = {
  def loop(rest: List[Int], acc: Int): Int = rest match {
    case Nil    => acc
    case h :: t => loop(t, acc + h)
  }
  loop(xs, 0)
}

运行累加器

这里的完整程序使用累加器计算列表总和。

请注意,基本情况直接返回 acc,而不是返回 0。我们在沿着列表向下处理时,已经逐步构建出了总和。

def sum(xs: List[Int]): Int = {
  def loop(rest: List[Int], acc: Int): Int = rest match {
    case Nil    => acc
    case h :: t => loop(t, acc + h)
  }
  loop(xs, 0)
}

@main def run(): Unit =
  println(sum(List(1, 2, 3, 4)))  // 10

比较两种结构

在普通递归中,组合步骤(h + ...)必须等待内部调用完成。

在累加器版本中,组合操作发生在调用之前,而调用是函数执行的最后一件事。

正是这种最后调用的特性,使它成为尾递归。

// Plain: combine after the call
case h :: t => h + sum(t)

// Accumulator: combine before the call
case h :: t => loop(t, acc + h)

尾递归

尾递归调用是指递归调用作为函数的最后一个操作,调用之后没有其他事情需要执行。

Scala 可以将其优化为循环,重复使用同一个栈帧,因此无论递归多深,都不会发生栈溢出。

import scala.annotation.tailrec

@tailrec
def countDown(n: Int): Unit =
  if (n < 0) ()
  else { println(n); countDown(n - 1) }

@tailrec 注解

添加 @tailrec 后,您可以要求编译器验证该函数确实是尾递归。

如果不是,编译就会失败,并显示清晰的错误信息。这样便可将隐蔽的性能陷阱转化为构建时保证。

import scala.annotation.tailrec

def sum(xs: List[Int]): Int = {
  @tailrec
  def loop(rest: List[Int], acc: Int): Int = rest match {
    case Nil    => acc
    case h :: t => loop(t, acc + h)
  }
  loop(xs, 0)
}

@main def run(): Unit = println(sum((1 to 100000).toList))

累加构建列表

累加器不一定只能保存数字,也可以构建集合。

这个 reverse 函数会将每个头部添加到累加器前端,从而自然地颠倒顺序。使用 :: 添加到前端速度很快,因此这种方式效率很高。

def reverse[A](xs: List[A]): List[A] = {
  def loop(rest: List[A], acc: List[A]): List[A] = rest match {
    case Nil    => acc
    case h :: t => loop(t, h :: acc)
  }
  loop(xs, Nil)
}

实际使用 reverse

累加器从空值开始,并随着我们消耗输入而逐渐增长。

由于每个头部都会被放到 acc 的前端,第一个元素最终会位于末尾,从而在恒定栈开销下得到反转后的列表。

def reverse[A](xs: List[A]): List[A] = {
  def loop(rest: List[A], acc: List[A]): List[A] = rest match {
    case Nil    => acc
    case h :: t => loop(t, h :: acc)
  }
  loop(xs, Nil)
}

@main def run(): Unit =
  println(reverse(List(1, 2, 3)))  // List(3, 2, 1)

多个累加器

辅助函数可以同时携带多个累加器。

这里我们在同一个循环中跟踪当前乘积和计数,并将两者作为元组返回。

每个累加器都会将更新后的值传递给下一次调用。

def stats(xs: List[Int]): (Int, Int) = {
  def loop(rest: List[Int], prod: Int, count: Int): (Int, Int) =
    rest match {
      case Nil    => (prod, count)
      case h :: t => loop(t, prod * h, count + 1)
    }
  loop(xs, 1, 0)
}

选择初始值

累加器的初始值必须是所执行运算的单位元。

加法使用 0,乘法使用 1,构建列表使用 Nil,连接字符串使用空字符串。

错误的初始值可能会悄无声息地产生错误答案。

// addition  -> seed 0
// product   -> seed 1
// list      -> seed Nil
// string    -> seed ""

结果的顺序

累加器递归会从左到右处理元素,但基于前置添加的累加器会将元素反转。

如果构建列表时需要保留顺序,可以在最后执行 reverse,或使用追加操作,不过追加速度较慢。先前置添加、最后反转是通常采用的写法。

def mapInc(xs: List[Int]): List[Int] = {
  def loop(rest: List[Int], acc: List[Int]): List[Int] = rest match {
    case Nil    => acc.reverse
    case h :: t => loop(t, (h + 1) :: acc)
  }
  loop(xs, Nil)
}

快速检查

选择关于累加器递归的准确说法。

回顾

累加器会将当前结果沿着递归调用向下传递,因此基本情况可以直接返回它。

这会使递归调用处于尾部位置,从而启用 Scala 的尾调用优化和 @tailrec 安全检查。

使用运算的单位元作为累加器初始值;当顺序很重要时,请在最后反转结果。

常见问题解答

「累加器模式」课时是免费的吗?

是的 — 「累加器模式」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Scala for Backend Engineering & Functional Programming 课程的其余内容,请升级到 CoddyKit PRO。 Scala for Backend Engineering & Functional Programming 课程共包含 4 节课。

「累加器模式」这节课中我会学到什么?

在递归过程中传递状态。 你通过在浏览器中直接运行的动手代码来练习 Scala for Backend Engineering & Functional Programming,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Scala for Backend Engineering & Functional Programming 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Scala for Backend Engineering & Functional Programming 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。

「累加器模式」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Scala for Backend Engineering & Functional Programming 课中编写并运行代码吗?

能。每节 Scala for Backend Engineering & Functional Programming 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 递归思维
  2. 累加器模式
  3. foldLeft 与 foldRight
  4. reduce 与聚合
← 返回 Scala for Backend Engineering & Functional Programming