1. 概述

在本教程中,我们将深入探讨 Scala 中的几种重要函数类型:部分应用函数(Partially-Applied Functions)、函数柯里化(Function Currying)、偏函数(Partial Functions)以及函数的基础概念。

我们将从 Scala 中函数的基本特性开始,了解编译器如何处理不同类型的函数。

2. 函数

在 Scala 中,函数是一等公民(first-class values),这意味着它们可以:

✅ 被当作参数传递或作为返回值
✅ 存入容器、赋值给变量
✅ 类型定义方式与普通值类似
✅ 在局部作用域中定义,甚至在表达式中构造

函数通常通过 defval 来定义。这两者的区别在于:

  • 使用 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),并附带了一些额外的方法,比如 andThencompose

尝试对方法进行组合:

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 项目 查看。


原始标题:Functions in Scala

» 下一篇: Scala 中的范围