1.3 函数即岩点:定义、传参与高阶函数


1.3 函数即岩点:定义、传参与高阶函数

def 定义方法,函数本身也是值,可以作为参数传递与返回。本节把 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

函数类型的构成视角

函数类型的构成视角

composeandThen 这些组合子在第三章会升为主角。

尾递归:不给编译器添麻烦

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 编译能过但大输入会栈溢出——这个注解的价值是让编译器替你验证"确实可优化",比人眼可靠。

图 1-3 高阶函数的数据管道

图 1-3 高阶函数的数据管道

高阶函数实战:把"策略"从代码里抽出来

假设要给一批订单按不同口径排序。面向对象的写法是定义 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 节)正是踩在柯里化的肩膀上——现在建立的直觉到时候直接复用。

常见编译错误:value 里的类型丢失

传函数时写下 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 组合子,到处都是这两种形状的函数在流动。

本节要点回顾

  • def 结构:参数列表 + 返回类型 + 表达式体,= 是关键。
  • 参数三件套:默认、命名、可变参数,API 设计的基本素材。
  • 函数即值Int => Int 就是 Function1 的实例,函数式建在对象上。
  • tailrec:递归写法加注解,让编译器背书。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U