桃花落,闲池阁,山盟虽在,锦书难托,莫,莫,莫!
「函数(Function)」是函数式编程的基本单元。本文将重点讨论函数式理论中几个容易混淆的概念,最后通过ScalaHamcrest
的实战,加深对这些概念的理解和运用。
- 函数类型
- 函数形态
- 柯里化
- 高阶函数
- 部分应用函数
- 偏函数
- 实战ScalaHamcrest
- 总结
函数类型
在Scala
中,函数可以使用「函数字面值(Function Literal)」直接定义。有时候,函数字面值也常常称为「函数」,或「函数值」。
(s: String) => s.toLowerCase
匿名函数
事实上,「函数字面值」本质上是一个「匿名函数(Anonymous Function)」。在Scala
里,函数被转换为FunctionN
的实例。上例等价于:
new Function1[String, String] {
def apply(s: String): String = s.toLowerCase
}
其中,Function1[String, String]
可以简写为String => String
,因此它又等价于:
new (String => String) {
def apply(s: String): String = s.toLowerCase
}
也就是说,「函数字面值」可以看做「匿名函数对象」的一个「语法糖」。
函数值
综上述,函数实际上是FunctionN[A1, A2, ..., An, R]
类型的一个实例而已。例如,(s: String) => s.toLowerCase
是Function1[String, String]
类型的一个实例。
在函数式编程中,函数做为一等公民,函数值可以被自由地传递和存储。例如,它可以赋予一个变量lower
。
val lower: String => String = _.toLowerCase
假如存在一个map
的柯里化的函数。
def map[A, B](a: A)(f: A => B): B = f(a)
函数值可以作为参数传递给map
函数。
map("HORANCE") { _.toLowerCase }
有名函数
相对于「匿名函数」,如果将「函数值」赋予def
,或者val
,此时函数常称为「有名函数」(Named Function)。
val lower: String => String = _.toLowerCase
def lower: String => String = _.toLowerCase
两者之间存在微妙的差异。前者使用val
定义的变量直接持有函数值,多次使用lower
将返回同一个函数值;后者使用def
定义函数,每次调用lower
将得到不同的函数值。
函数调用
在Scala
里,「函数调用」实际上等价于在FunctionN
实例上调用apply
方法。例如,存在一个有名函数lower
。
val lower: String => String = _.toLowerCase
当发生如下函数调用时:
lower("HORANCE")
它等价于在Function1[String, String]
类型的实例上调用apply
方法。
lower.apply("HORANCE")
形式化
一般地,函数字面值具有如下形式:
(a1: A1, a2: A2, ..., an: An) => E: R
其中,E
是一个具有类型为R
的表达式。它的类型为:
(A1, A2, ..., An) => R
或者表示为:
FunctionN[A1, A2, ..., An, R]
其中,函数字面值等价于匿名函数对象的定义:
new FunctionN[A1, A2, ..., An, R] {
def apply(a1: A1, a2: A2, ..., an: An): R = E
}
其中,FunctionN
类型定义为
trait FunctionN[-A1, -A2, ..., -An, +R] {
def apply(a1: A1, a2: A2, ..., an: An): R
}
高阶函数
接受函数作为参数,或返回函数的函数常常称为「高阶函数(Hign Order Function)」。高阶函数是组合式设计的基础,它极大地提高了代码的可复用性。
例如,starts, ends, contains
是三个高阶函数,它们都返回String => Boolean
类型的谓词,用于判断某个字符串的特征。
type StringMatcher = String => String => Boolean
def starts: StringMatcher = prefix =>
_ startsWith prefix
def ends: StringMatcher = suffix =>
_ endsWith suffix
def contains: StringMatcher = substr =>
_ contains substr
同样地,高阶函数也可以接受函数类型作为参数。例如,如果要忽略大小写,并复用上面三个函数,可以定义ignoringCase
的高阶函数,用于修饰既有的字符串匹配器。
def ignoringCase(matcher: StringMatcher): StringMatcher = substr =>
str => matcher(substr.toLowerCase)(str.toLowerCase)
可以如下方式使用ignoringCase
。
assertThat("Horance, Liu", ignoringCase(starts)("horance"))
其中,assertThat
也是一个高阶函数。
def assertThat[A](actual: A, matcher: A => Boolean) =
assert(matcher(actual))
部分应用函数
所谓「部分应用函数」(Partially Applied Function),即至少还有一个参数还未确定的函数。例如,times
用于判断m
是不是n
的整数倍。
def times: Int => Int => Boolean = n => m => m % n == 0
对于常见的倍数关系,可以先确定参数n
的值。例如:
val twice = times(2)
val triple = times(3)
此时,twice, triple
是一个Int => Boolean
的函数,可以被再次利用。
twice(4) // true
triple(9) // true
η扩展
首先,将原来的times
进行一下变换。
def times(n: Int)(m: Int): Boolean = m % n == 0
此时,如果采用如下的方式确定twice, triple
,将发生错误。
val twice = times(2) // Error
val triple = times(3) // Error
因为,times(2)
的真实类型为方法(Method),且拥有(Int)Boolean
的类型。它不是函数,且没有函数值。
可以通过在「方法」的后面手动地添加「一个空格和一个_
」,将方法转换为一个「部分应用函数」,这种转换常称为「η
扩展」。例如:
val twice = times(2) _
val triple = times(3) _
事实上,times(2) _
将生成一个匿名的函数对象。它等价于:
val twice = new Function1[Int, Boolean] {
def apply(m: Int): Boolwan = this.times(2)(m) // 在this对象调用`times`方法
}
自动扩展
如果上下文已经标明函数的类型,编译器会自动实施η
扩展。例如:
def atom(matcher: Int => Boolean, action: Int => String): Int => String =
m => if (matcher(m)) action(m) else ""
因为,atom
的matcher
参数已经标明了函数原型。因此,当传递times(3), to("Fizz")
给atom
时,编译器会自动实施η
扩展,将times(3), to("Fizz")
转变为一个部分应用函数。
val r_n1 = atom(times(3), to("Fizz"))
它等价于
val r_n1 = atom(times(3) _, to("Fizz") _)
其中,to
的实现为:
def to(s: String): Int => String = _ => s
偏函数
JSONObject
是一个JSON
的一个子类,它持有bindings: Map[String, JSON]
。toString
迭代地处理Map
中元组,并通过调用_1, _2
方法分别获取当前迭代元组的关键字和值。
case class JSONObject(bindings: Map[String, JSON]) extends JSON {
override def toString: String = {
val contents = bindings map { t =>
"\\\\"" + t._1 + ":" + t._2.toString + "\\\\""
}
"{" + (contents mkString ",") + "}"
}
}
可以使用「模式匹配」直接获取Map
中当前迭代的元组。
bindings map { {
case (key, value) => "\\\\"" + key + ":" + value + "\\\\""
} }
其中,map
的大括号(或小括号)常常被省略。
bindings map {
case (key, value) => "\\\\"" + key + ":" + value + "\\\\""
}
其中,{ case (key, value) => ??? }
表达式是一个整体,包括外面的大括号。此外,该表达式是有类型的,它的类型为PartialFunction[(String, JSON), String]
。
val f: PartialFunction[(String, JSON), String] = {
case (key, value) => "\\\\"" + key + ":" + value + "\\\\""
}
也就是说,{ case (key, value) => ??? }
是「偏函数」(Partial Function)的一个字面值。
偏函数的定义
一般地,对于函数A => B
,它对A
类型的所有值都有意义;而PartialFunction[A, B]
仅对A
类型的部分值有意义。
也就是说,偏函数是一个残缺的函数,它只处理模式匹配成功的值,而对模式匹配失败的值抛出MatchError
异常。例如,positive
就是一个偏函数,它仅对大于0
的整数有意义。
val positive: PartialFunction[Int, Int] = { case x if x > 0 => x }
当传递负数或0
值时,将在运行时抛出MatchError
异常。
positive(1) // OK
positive(-1) // MatchError
剖析偏函数
偏函数是一个一元函数。
trait PartialFunction[-A, +B] extends (A => B) {
def isDefinedAt(x: A): Boolean
def applyOrElse[A1 <: A, B1 >: B](x: A1)(default: A1 => B1): B1 =
if (isDefinedAt(x)) apply(x) else default(x)
}
对于偏函数positive
val positive: PartialFunction[Int, Int] = { case x if x > 0 => x }
为了方便理解,上述偏函数实现类似于如下的匿名函数定义:
val positive = new PartialFunction[Int, Int] {
def isDefinedAt(x: A): Boolean = x > 0
def apply(x: Int): Int = x
}
追溯MatchError
当调用positive(-1)
时,将抛出MatchError
异常。追本溯源,它实际调用的是AbstractPartialFunction
的apply
方法。
AbstractPartialFunction
的存在,避免了偏函数的字面值产生大量的匿名类定义。
abstract class AbstractPartialFunction[-T, +R] extends PartialFunction[T, R] {
def apply(x: T): R = applyOrElse(x, PartialFunction.empty)
}
其中,empty
定义在PartialFunction
的伴生对象中。
object PartialFunction {
val empty = new PartialFunction[Any, Nothing] {
def isDefinedAt(x: Any) = false
def apply(x: Any) = throw new MatchError(x)
...
}
}
当调用positive(-1)
时,AbstractPartialFunction
的apply
方法被调用,它委托给了PartialFunction
的applyOrElse
方法。
因为此时传入的是负数,isDefinedAt
返回false
,因此最终偏函数的返回值为PartialFunction.empty
,而调用empty.apply
将抛出MatchError
异常。
提升
如果对偏函数进行提升(lift),会将偏函数升级为一个普通的一元函数。例如,对于偏函数positive
:
val positive: PartialFunction[Int, Int] = { case x if x > 0 => x }
通过lift
,可以将positive
提升为Int => Option[Int]
的一元函数。
val positiveOption = positive.lift
当positiveOption
传入的是正数x
时,将返回Some(x)
,否则返回None
。
降级
如果对一元函数positiveOption
调用Function.unlift
,将使得该一元函数降级为一个偏函数。例如:
val positive = Function.unlift(positiveOption) // positive: PartialFunction[Int, Int]
其中,unlift
实现在单键对象Function
中,它的函数原型为:
def unlift[T, R](f: T => Option[R]): PartialFunction[T, R] = ???
函数形态
在Scala
中,可能存在多种风格类似的函数定义(广义的函数定义)。首先,可以以方法(Method)的形式存在。
def lower(s: String): String = s.toLowerCase
特殊地,虽然以方法的形式存在,但它返回了函数类型。因此,也常常成为方法。
def lower: String => String = _.toLowerCase
def lower: Function1[String, String] = _.toLowerCase
另外,也可以使用val
持有函数值。
val lower: String => String = _.toLowerCase
val lower: Function1[String, String] = _.toLowerCase
val lower = (s: String) => s.toLowerCase
假如存在一个map
的柯里化的函数。
def map[A, B](a: A)(f: A => B): B = f(a)
它接受上述6
种风格的lower
定义。
map("HORANCE")(lower)
当然,map
也可以直接接受匿名函数(函数字面值)。
map("HORANCE") { _.toUpperCase }
虽然map
接受多种不同的lower
表示,但三种表示形式存在微妙的差异。
方法与函数
def lower(s: String): String = s.toLowerCase
def lower: String => String = _.toLowerCase
从严格意义上讲,两者都是使用了def
定义的「方法」(Method)。但是,后者返回了一个「函数类型」,并具有函数调用的语义。
因此按照习惯,前者常称为「方法」,而后者则常常称为「函数」;前者代表了面向对象的风格,后者代表了函数式的风格。
在Scala
中,方法与函数是两个不同的概念,并存在微妙的差异,并代表了两种不同的编程范式。但幸运的是,它们的差异性对于99%
的用户是无感知的。
方法不是函数
在Scala
中,方法并不是函数。例如
def lower(s: String): String = s.toLowerCase
lower
是一个方法(Method),而方法是没有值的,因此它不能赋予给变量。
val f = lower // 错误,lower是一个方法,它没有值
可以对方法lower
实施η
扩展,将其转换为部分应用函数。例如:
val f = lower _ // f: String => String =
事实上,lower _
将生成一个匿名的函数对象。它等价于:
val f = new Function1[String, String] {
def apply(s: String): String = this.lower(s) // 在this对象调用`lower`方法
}
特殊地,当上下文需要一个「函数类型」时,此时编译器可以自动完成「方法」的η
扩展。例如,map
的第二个参数刚好是一个函数类型。此时,可以直接将lower
方法进行传递,编译器自动完成η
扩展,将其转换为一个函数对象。
map("HORANCE")(lower) // map期待一个函数类型,编译器自动完成`η`扩展
它等价于:
map("HORANCE")(lower _)
两阶段调用
def lower: String => String = _.toLowerCase
事实上,此处的lower
是一个使用def
定义的方法。但是特殊地,它返回了函数类型。当发生如下调用时。
lower("HORANCE")
实际上,它做了两件事情:
- 在
this
对象上调用lower
方法,并返回String => String
类型的函数对象; - 在该函数对象上调用
apply
方法;
也就是说,上例函数调用等价于:
val f = this.lower // f: String => String =
f.apply("HORANCE")
事实上,lower("HORANCE")
的调用具有特殊意义。其一,lower
在this
对象上调用刚好返回函数类型;其二,FunctionN
上的apply
是一个特殊的方法,其具有更贴切的函数调用语义。
按照习惯,即使lower
是一个方法定义,但也被常常称为函数定义。
def与val
如下两种方式,定义了类似的有名函数,但两者之间存在微妙的差异。
val lower: String => String = _.toLowerCase
def lower: String => String = _.toLowerCase
使用val定义函数
前者使用val
的变量直接持有函数值,多次使用lower
将返回同一个函数值。也就是说,如下表达式求值为真。
lower eq lower // true
使用def定义函数
后者使用def
定义函数,每次调用lower
将得到不同的函数值。也就是说,如下表达式求值为假。
lower eq lower // false
柯里化
「柯里化(Currying)」是函数式理论的一个重要概念。例如,之前的map
就是一个柯里化的方法定义。
def map[A, B](a: A)(f: A => B): B = f(a)
柯里化类型的自动推演,及其控制结构的抽象等应用场景扮演重要角色。
类型推演
假如,map
是一个普通的方法定义,如果没有进行柯里化。
def map[A, B](a: A, f: A => B): B = f(a)
当直接传递匿名函数时,如果没有显式地标注参数类型,类型推演将发生错误。
map("HORANCE", s => s.toLowerCase) // Error
它必须显式地声明匿名函数的入参类型。
map("HORANCE", (s: String) => s.toLowerCase)
如果对map
进行柯里化,让其拥有两个柯里化的参数列表。
def map[A, B](a: A)(f: A => B): B = f(a)
当传递匿名函数时,它便可以自动完成匿名函数参数的类型推演。
map("HORANCE", s => s.toLowerCase)
控制抽象
借助于柯里化,可以实现控制结构的抽象。例如,可以设计一个指令式的until
的函数,它将循环调用blok
,直至条件cond
为真为止(与while
相反)。
@annotation.tailrec
def until(cond: => Boolean)(block: => Unit) {
if (!cond) {
block
until(cond)(block)
}
}
当调用until
时,最后一个柯里化参数列表的调用可以使用大括号代替小括号。例如,
var n = 10
var r = 0
until (n == 0) {
r += n
n -= 1
}
事实上,它等价于
(0 to 10).sum
前者为指令式的代码风格,后者为函数式的指令风格。
形式化
一般地,具有多个参数列表,并且每个参数列表中有且仅有一个参数的「柯里化」常称为「完全柯里化」。假设存在一个完全柯里化的方法定义,它具有如下的形式:
def f(a1: A1)(a2: A2)...(an: An):R = E
其中,n > 1
。它等价于:
def f(a1: A1)(a2: A2)...(an_1: An_1) = an => E
递归这个过程,可以推出最终的等价形式:
def f = (a1 => (a2 => ...(an => E)...))
虽然,后者也是一个使用def
定义的方法,但它返回了一个「完全柯里化的函数类型」。按照惯例,后者也常称为「完全柯里化的函数」。
柯里化方法与函数
def concat(s1: String)(s2: String): String = s1 + s2
def concat: String => String => String = s1 => s2 => s1 + s2
按照上述形式化的描述,前者进行2
次变换可将其转变为后者,两者功能等价。但是,它们两者之间是存在着微妙的差异,前者是一个方法,它没有值;而后者是一个函数,且拥有函数值。
柯里化方法
首先,对于柯里化的方法,它首先是一个方法(Method)。
def concat(s1: String)(s2: String): String = s1 + s2
因此,它没有值。如下语句将发生编译错误。
val f = concat // Error
但是,可以应用η
扩展将该柯里化的方法转变为柯里化的函数值。
val f = concat _ // f: String => (String => String) =
如果固定第一个参数的值,必须再实施η
扩展,才能得到部分应用函数。
val f = concat("horance") _ // f: String => String =
柯里化函数
而对于柯里化的函数,它是一个函数(Function)。
def concat: String => String => String = s1 => s2 => s1 + s2
无需η
扩展,它便可以直接获取到函数值。
val f = concat // f: String => (String => String) =
如果固定第一个参数的值,也不用实施η
扩展,便直接可得到的部分应用函数。
val g = concat("horance") // g: String => String =
自动扩展
当然,如果存在一个高阶函数twice
,它接受String => String => String
的函数类型。
def twice[A](x: A)(f: A => A => A): A = f(x)(x)
对于柯里化的方法concat
。
def concat(s1: String)(s2: String): String = s1 + s2
可以直接传递concat
给twice
,编译器自动完成η
扩展。
twice("horance")(concat)
事实上,它等价于:
twice("horance")(concat _)
FunctionN.curried(N >= 2)
一般地,完全柯里化的函数具有如下的类型。
(A1 => (A2 => ...(An => E)...))
可以通过调用FunctionN.curried
方法,将一个「非完全柯里化的函数」转化为等价的「完全柯里化的函数」。例如,如果concat
是一个普通的方法定义。
def concat(s1: String, s2: String): String = s1 + s2
首先,应用η
扩展,将其转变为部分应用函数;此时等到的「部分应用函数」还不是一个完全被柯里化的函数。
val f = concat _ // f: (String, String) => String =
可以在这个实例对象上通过调用curried
将其转变为「完全柯里化」的等价形式。
val g = f.curried // g: String => (String => String) =
其中,curried
是一个高阶函数,它返回一个完全柯里化的函数。
trait Function2[-T1, -T2, +R] {
def apply(v1: T1, v2: T2): R
def curried: T1 => T2 => R =
x1 => x2 => apply(x1, x2)
}
Function.uncurried
可以对g
应用Function.uncurried
的逆向操作,将其转变为「非柯里化」的函数。
val f = Function.uncurried(g) // f: (String, String) => String =
其中,Function.uncurried
是这样定义的。
object Function {
def uncurried[A1, A2, B](f: A1 => A2 => B): (A1, A2) => B =
(x1, x2) => f(x1)(x2)
}
FunctionN.tupled(N >= 2)
假设存在一个二元组。
val names = ("horance", "liu")
如果要调用如下的函数,完成字符串的连接。
def concat(s1: String, s2: String): String = s1 + s2
它需要依次取出元组中的元素并进行传递。
val fullName = concat(names._1, names._2) // fullName: String = horanceliu
可以应用Function2.tupled
改善设计。首先,应用η
扩展将该方法变换为部分应用函数,然后再在该函数对象上调用tupled
,并直接传递二元组。
val fullName = (concat _).tupled(names) // fullName: String = horanceliu
其中,tupled
是一个高阶函数,它返回(String, String) => String
类型的函数。
trait Function2[-T1, -T2, +R] {
def apply(v1: T1, v2: T2): R
def tupled: Tuple2[T1, T2] => R = {
case Tuple2(x1, x2) => apply(x1, x2)
}
}
Function.untupled
可以对g
应用Function.untupled
的逆向操作,将接受元组类型参数的函数转变为接受参数列表的函数。
val g = (concat _).tupled // g: ((String, String)) => String =
val f = Function.untupled(g) // f: (String, String) => String =
其中,Function.untupled
是这样定义的。
object Function {
def untupled[A1, A2, B](f: (A1, A2) => B): (A1, A2) => B =
(x1, x2) => f((x1, x2))
}
实战ScalaHamcrest
对于任意的类型A
,A => Boolean
常常称为「谓词」;如果该谓词用于匹配类型A
的某个值,也常常称该谓词为「匹配器」。
ScalaHamcrest
是一个使用函数式设计思维实现的Matcher
集合库。通过实战ScalaHamcrest
,以便加深理解Scala
函数的理论基础。
原子匹配器
可以定义特定的原子匹配器。例如:
def always: Any => Boolean = _ => true
def never: Any => Boolean = _ => false
也可以定义equalTo
的原子匹配器,用于比较对象间的相等性。
def equalTo[T](expected: T): T => Boolean =
expected == _
与equalTo
类似,可以定义原子匹配器same
,用于比较对象间的一致性。
def same[T <: AnyRef](t: T): T => Boolean = t eq _
其中,T <: AnyRef类型
对T
进行界定,排除AnyVal
的子类误操作same
。类似于类型上界,也可以使用其他的类型界定形式;例如,可以定义instanceOf
,对类型T
进行上下文界定,用于匹配某个实例的类型。
def instanceOf[T : ClassTag]: Any => Boolean = x =>
x match {
case _: T => true
case _ => false
}
有时候,基于既有的原子可以很方便地构造出新的原子。
def nil = equalTo[AnyRef](null)
def empty = equalTo("")
组合匹配器
也可以将各个原子或者组合器进行组装,形成威力更为强大的组合器。
def allOf[T](matchers: (T => Boolean)*): T => Boolean =
actual => matchers.forall(_(actual))
def anyOf[T](matchers: (T => Boolean)*): T => Boolean =
actual => matchers.exists(_(actual))
特殊地,基于anyof
,可以构造很多特定的匹配器。
def blank: String => Boolean = """\\\\s*""".r.pattern.matcher(_).matches
def emptyOrNil = anyOf(nil, equalTo(""))
def blankOrNil = anyOf(nil, blank)
修饰匹配器
修饰也是一种特殊的组合行为,用于完成既有功能的增强和补充。
def not[T](matcher: T => Boolean): T => Boolean =
!matcher(_)
def is[T](matcher: T => Boolean): T => Boolean =
matcher
其中,not, is
是两个普遍的修饰器,可以修饰任意的匹配器。此外,可以通过定义语法糖,提升用户感受。例如,可以使用not
替换not(equalTo)
,is
替代is(equalTo)
,不仅减轻用户的负担,而且还能提高表达力。
def not[T](expected: T): T => Boolean = not(equalTo(expected))
def is[T](expected: T): T => Boolean = is(equalTo(expected))
测试用例
至此,还不知道ScalaHamcrest
如何使用呢?可以定义一个实用方法assertThat
。
def assertThat[A](actual: A, matcher: A => Boolean) =
assert(matcher(actual))
其中,assert
定义于Predef
之中。例如存在如下一个测试用例。
assertThat(2, allOf(always, instanceOf[Int], is(2), equalTo(2)))
增强特性
遗留一个很有意思的问题,能否使用&&
直接连接多个匹配器形成调用链,替代allOf
匹配器呢?
assertThat("horance", equalTo("horance") && ignoringCase(equalTo)("HORANCE"))
可以定义个一个implicit Matcher
,并添加了&&, ||, !
的基本操作,用于模拟谓词的基本功能。
implicit class Matcher[-A](pred: A => Boolean) extends (A => Boolean) {
self =>
def &&[A1 <: A](that: A1 => Boolean): A1 => Boolean =
x => self(x) && that(x)
def ||[A1 <: A](that: A1 => Boolean): A1 => Boolean =
x => self(x) || that(x)
def unary_![A1 <: A]: A1 => Boolean =
!self(_)
def apply(x: A): Boolean = pred(x)
}
总结
本文重点叙述了Scala
函数式编程的基础,重点讨论了几个容易混淆的概念。包括「方法」与「函数」,「偏函数」与「部分应用函数」,「柯里化」,「高阶函数」等,并通过实战ScalaHamcrest
,深入理解Scala
函数的基本概念。
接下来,将重点讨论函数式编程的应用。