1. 概述
在本教程中,我们将深入探讨 Scala 中的几种重要函数类型:部分应用函数(Partially-Applied Functions)、函数柯里化(Function Currying)、偏函数(Partial Functions)以及函数的基础概念。
我们将从 Scala 中函数的基本特性开始,了解编译器如何处理不同类型的函数。
2. 函数
在 Scala 中,函数是一等公民(first-class values),这意味着它们可以:
✅ 被当作参数传递或作为返回值
✅ 存入容器、赋值给变量
✅ 类型定义方式与普通值类似
✅ 在局部作用域中定义,甚至在表达式中构造
函数通常通过 def
或 val
来定义。这两者的区别在于:
- 使用
def
定义的是“方法”,每次调用都会重新计算; - 使用
val
定义的是“函数值”,只计算一次。
来看一个例子:
val dummyFunctionVal : Int => Int = {
println(new Date())
num => num
}
def dummyFunctionDef(number : Int) : Int = {
println(new Date())
number
}
println(dummyFunctionVal(10)) // 打印时间戳 + 10
println(dummyFunctionVal(10)) // 只返回 10
println(dummyFunctionDef(10)) // 打印时间戳 + 10
println(dummyFunctionDef(10)) // 再次打印时间戳 + 10
2.1 方法 vs 函数值
使用 def
定义的方法必须属于某个类或对象,并且隐式地绑定到该类实例上,它本质上不是值,也没有类型。
而使用 val
定义的函数值是 Scala 内建的 FunctionN
类的特化形式(N 可为 0 到 22),并附带了一些额外的方法,比如 andThen
和 compose
。
尝试对方法进行组合:
val getNameLengthVal : String => Int = name => name.length
val multiplyByTwoVal : Int => Int = num => num * 2
getNameLengthVal.andThen(multiplyByTwoVal) // ✅ 编译通过
def getNameLengthDef(name : String) : Int = name.length
def multiplyByTwoDef(number : Int) : Int = number * 2
getNameLengthDef.andThen(multiplyByTwoDef) // ❌ 编译失败
不过可以通过“eta 扩展”将方法转换为函数值:
val getNameLengthDefFnValue = getNameLengthDef _
val multiplyByTwoDefFnValue = multiplyByTwoDef _
getNameLengthDefFnValue.andThen(multiplyByTwoDefFnValue) // ✅ 编译通过
这个过程也叫 eta expansion,是 Scala 将方法转为函数值的关键技术。
3. 部分应用函数(Partially-Applied Functions)
顾名思义,部分应用函数指的是将函数的部分参数提前提供,剩下的参数留空,从而生成一个新的函数。
⚠️ 重点理解:部分应用函数总是返回一个新函数;原始函数只有在所有参数都传入后才会执行。
3.1 避免重复代码
假设我们要构造 URL,协议是固定的:
def createUrl(protocol: String, domain : String) : String = {
s"$protocol$domain"
}
val baeldung = createUrl("https://","www.baeldung.com")
val facebook = createUrl("https://","www.facebook.com")
val twitter = createUrl("https://","www.twitter.com")
val google = createUrl("https://","www.google.com")
这段代码违反了 DRY 原则。我们可以通过部分应用函数来简化:
val withHttpsProtocol: String => String = createUrl("https://", _: String)
val withHttpProtocol: String => String = createUrl("http://", _: String)
val withFtpProtocol: String => String = createUrl("ftp://", _: String)
这样就可以避免每次都写协议:
val baeldung = withHttpsProtocol("www.baeldung.com")
val facebook = withHttpsProtocol("www.facebook.com")
val twitter = withHttpsProtocol("www.twitter.com")
val google = withHttpsProtocol("www.google.com")
✅ 明显减少了重复代码。
3.2 多参数部分应用示例
再来看一个更实际的例子:HTML 标签构建函数:
def htmlPrinter(tag: String, value: String, attributes: Map[String, String]): String = {
s"<$tag${attributes.map { case (k, v) => s"""$k="$v"""" }.mkString(" ", " ", "")}>$value</$tag>"
}
htmlPrinter("div","Big Animal",Map("size" -> "34","show" -> "true"))
// 输出 <div size="34" show="true">Big Animal</div>
如果多个 div 使用相同属性,我们可以这样优化:
val withDivAndAttr: String => String = htmlPrinter("div", _: String, Map("size" -> "34"))
或者分步部分应用:
val withDiv: (String, Map[String, String]) => String = htmlPrinter("div", _:String , _: Map[String,String])
val withDivAndAttr2: String => String = withDiv(_:String, Map("size" -> "34"))
这种方式灵活又简洁。
4. 函数柯里化(Currying)
柯里化是指将一个多参数函数拆分为多个单参数函数的链式调用。
例如,一个三参数组的函数:
def dummyFunction(a: Int, b: Int)(c: Int, d: Int)(e: Int, f: Int): Int = {
a + b + c + d + e + f
}
val first: (Int, Int) => (Int, Int) => Int = dummyFunction(1,2)
val second: (Int, Int) => Int = first(3,4)
val third : Int = second(5,6)
每传入一组参数,就返回一个接受下一组参数的新函数。
4.1 柯里化的实际用途
Scala 的 Future.apply
就是一个典型例子:
val future2 = Future {
Thread.sleep(1000)
2
}(executionContext)
如果不使用柯里化,语法会变得丑陋:
// 不推荐写法
val future1 = Future({
Thread.sleep(1000)
2 }, executionContext)
4.2 类型推导优势
柯里化还能帮助编译器更好地进行类型推导:
// 错误写法:无法推导泛型 T
def withListItems[T](list : List[T],f : T => Unit) : Unit = ???
withListItems(List(1,2,3,4), number => println(number + 2)) // ❌ 编译失败
// 正确写法:柯里化后可以推导
def withListItems[T](list : List[T])(f : T => Unit) : Unit = ???
withListItems(List(1,2,3,4))(number => println(number + 2)) // ✅ 编译成功
💡 总结一句话:类型推导按参数组从左到右进行,同一组内的参数同时推导。
5. 偏函数(Partial Functions)
偏函数是指只对某些输入有效的一类函数。它允许我们根据输入做出条件判断。
在 Scala 中,偏函数是 PartialFunction[A, B]
的实例:
val isWorkingAge : PartialFunction[Int,String] = new PartialFunction[Int,String] {
override def isDefinedAt(x: Int): Boolean = if(x >= 18 && x <= 60) true else false
override def apply(v1: Int): String = {
if (isDefinedAt(v1)) {
s"You are $v1 years old within working age"
}else{
s"You are $v1 years old and not within working age"
}
}
}
也可以使用模式匹配简化定义:
val isWorkingAge : PartialFunction[Int,String] = {
case age if age >= 18 && age <= 60 => s"You are $age years old and within working age"
case other => s"You are $other years old and not within working age"
}
5.1 偏函数组合
偏函数支持链式调用,比如 orElse
:
val isWorkingAge : PartialFunction[Int,String] = {
case age if age >= 18 && age <= 60 => s"You are $age years old within working age"
}
val isYoung : PartialFunction[Int,String] = {
case age if age < 18 => s"You are less than 18, and not within the working age"
}
val isOld : PartialFunction[Int,String] = {
case age if age > 60 => s"You are greater than 60 and not within the working age"
}
val verdict = isWorkingAge orElse isYoung orElse isOld
verdict(12) // You are less than 18, and not within the working age
verdict(22) // You are 22 years old within working age
verdict(70) // You are greater than 60 and not within the working age
⚠️ 注意:偏函数在 Scala 中广泛应用于集合操作(如 collect
)和 Akka Actor 的消息处理中。
6. 结语
本文介绍了 Scala 中几种重要的函数类型及其应用场景:
✅ 部分应用函数(Partially-Applied Functions)
✅ 函数柯里化(Currying)
✅ 偏函数(Partial Functions)
这些特性不仅提升了代码的可读性和复用性,也让函数式编程思想更加自然地融入 Scala 开发中。
相关代码可在 GitHub 项目 查看。