def 定义方法,函数本身也是值,可以作为参数传递与返回。本节把 def 的参数机制(默认参数、命名参数、可变参数)和高阶函数一次讲透,为第三章的组合攀爬打好岩点。
def add(x: Int, y: Int): Int = x + y // def 名字(参数列表): 返回类型 = 函数体
注意那个 =:它强调"函数体是一个表达式,方法是对它求值"。单表达式可省花括号;多行用缩进或花括号,块的值就是最后一行的值。
三种常用参数机制:
def greet(name: String, prefix: String = "Mr."): String = s"Hello, $prefix $name" greet("Wang") // 默认参数 greet("Li", prefix = "Dr.") // 命名参数,跳过顺序 def sum(nums: Int*): Int = nums.sum sum(1, 2, 3) // 可变参数,收成 Seq sum(List(1,2,3)*) // 序列展开传入
返回 Unit 的方法有副作用(打印、写库),有值的方法是纯计算。团队协作里这个区分越清晰,代码越可测——测试一个返回值的函数不需要任何 mock。
把一个函数赋给 val,就得到"函数字面量":
val double: Int => Int = n => n * 2 val double2: Int => Int = _ * 2 // 占位符简写 double(21) // 42
类型 Int => Int 读作"从 Int 到 Int 的函数"。它本质上是一个 trait 的实例——Function1[Int, Int],带一个 apply 方法。看到这里你可以回头印证第一章的话:Scala 里函数式能力是建在对象系统上的。
函数能当参数传,就有了高阶函数:
def applyTwice(f: Int => Int, x: Int): Int = f(f(x)) applyTwice(double, 5) // 20
给一个税率算税后价,再按策略打折——两步各自是一个函数,串起来:
val withTax: Double => Double = p => p * 1.13 val discount: Double => Double = p => p * 0.9 val finalPrice = discount compose withTax finalPrice(100.0) // 101.7

compose、andThen 这些组合子在第三章会升为主角。
Scala 不会自动把递归优化成循环,但 tailrec 注解可以强制编译器验证并优化:
import scala.annotation.tailrec @tailrec def loop(n: Int, acc: Int): Int = if (n == 0) acc else loop(n - 1, acc + n) loop(100000, 0) // 不爆栈
去掉 @tailrec 编译能过但大输入会栈溢出——这个注解的价值是让编译器替你验证"确实可优化",比人眼可靠。

假设要给一批订单按不同口径排序。面向对象的写法是定义 Comparator 子类,函数式的写法是把"比较"本身当参数传:
case class Order(id: Int, amount: Double, days: Int) val orders = List(Order(1, 320.0, 3), Order(2, 99.5, 1), Order(3, 780.0, 7)) val byAmount = orders.sortBy(_.amount) // 按金额 val byUrgency = orders.sortBy(o => (o.days, -o.amount)) // 按 (天数升序, 金额降序) 的元组排序 —— 元组自带字典序 val bigOrOld = orders.filter(o => o.amount > 200 || o.days > 5)
sortBy 接收的就是一个函数值。换成 Java 你要写匿名内部类,换成本节学到的写法,"策略"退化成一行 lambda,新增口径时不再触碰排序逻辑本身。
def fold[A, B](xs: List[A])(z: B)(f: (B, A) => B): B = ??? val sum: List[Int] => Int = fold(_)(0)(_ + _) // 只填一部分,剩下的仍是函数
参数列表拆成多组(柯里化)有两个实际收益:一是类型推断器可以从前一组参数推断后组里省略的类型;二是可以部分应用,把"折叠规则"做成可传递的值。Scala 3 的 using 参数(5.3 节)正是踩在柯里化的肩膀上——现在建立的直觉到时候直接复用。
传函数时写下 orders.map(o => o.amount > 200) 得到 List[Boolean] 而不是订单列表,随后调用 .id 报错——这不是 bug,是把"判断"当成了"变换"。排错口诀:map 变形、filter 筛选、flatMap 变形再压平,先问自己要的是哪一种。
函数式的招牌写法是递归,但朴素递归会爆栈。加一个 tailrec 注解,编译器验证后自动改写成循环:
import scala.annotation.tailrec @tailrec def sum(xs: List[Int], acc: Int = 0): Int = xs match case Nil => acc case h :: t => sum(t, acc + h) // 递归调用是最后一步,可改写为跳转 sum(List(1, 2, 3, 4)) // 10
如果递归调用不是尾位置,@tailrec 直接编译失败——它是一道免费的正确性闸门。累加器 acc 的模式(把中间结果当参数传下去)是写所有遍历的通用套路,第四章集合操作的实现原理就是这么写的。
把下面几个签名读出声,是检验本节是否过关的口试题:
val f1: Int => String = i => i.toString val f2: (Int, Int) => Int = (a, b) => a + b val f3: Int => Int => Int = a => b => a + b // 柯里化:逐个吃参数 f3(2)(3) // 5
f3 的类型从右往左结合:先吃一个 Int 得到新函数,再吃一个。它与 f2 表达同一个加法,但形状不同——支持部分应用。第五章的隐式参数、第六章的 Future 组合子,到处都是这两种形状的函数在流动。
= 是关键。Int => Int 就是 Function1 的实例,函数式建在对象上。