函数即对象

Scala中函数值确实被当做对象对待。

函数类型A => B是类scala.Function1[A, B]的缩写,像下面这样定义:

1
2
3
4
package scala
trait Function1[A, B] {
def apply(x: A): B
}

所以说函数是有apply方法的对象。

还有Function2Function3等等这些traits,可以有更多的参数(目前到22)。

函数值的展开

类似于下面的匿名函数

1
(x: Int) => x * x

会被展开为:

1
2
3
4
5
{ class AnonFun extends Function[Int, Int] {
def apply(x: Int) = x * x
}
new AnonFun
}

或者用匿名类语法变得更短:

1
2
3
new Function1[Int, Int] {
def appy(x: Int) = x * x
}

函数调用的展开

一个函数调用,比如f(a, b),其中f是某个类类型值,会被展开成

1
f.apply(a, b)

所以

1
2
val f = (x: Int) => x * x
f(7)

的面向对象版翻译就是:

1
2
3
4
val f = new Function1[Int, Int] {
def apply(x: Int) = x * x
}
f.apply(7)

函数和方法

注意一个方法,比如:

1
def f(x: Int): Boolean = …

自己并不是一个函数值。

不过如果f被用在一个期望函数类型的地方,它就会自动被转换为函数值:

1
(x: Int) => f(x)

或者是展开的形式:

1
2
3
new Function1[Int, Boolean] {
def apply(x: Int) = f(x)
}

这种转换为匿名函数的方法在λ演算中叫做η变换(eta-expansion)

对象无处不在

纯面向对象语言是一种在其中每个值都是对象的语言。

纯Boolean

Boolean类型被映射到JVM基本的boolean值。

但是也可以把它定义为一个类:

1
2
3
4
5
6
7
8
9
10
package idealized.scala
abstract class Boolean {
def ifThenElse[T](t: => T, e: => T): T
def && (x: => Boolean): Boolean = ifThenElse(x, False)
def || (x: => Boolean): Boolean = ifThenElse(True, x)
def unary_! : Boolean = ifThenElse(False, True)
def == (x: Boolean): Boolean = ifThenElse(x, x.unary_!)
def != (x: Boolean): Boolean = ifThenElse(x.unary_!, x)
}

TrueFalse这两个常量呢?这样:

1
2
3
4
5
6
7
package idealized.scala
object True extends Boolean {
def ifThenElse[T](t: => T, e: => T) = t
}
object False extends Boolean {
def ifThenElse[T](t: => T, e: => T) = e
}

练习:定义一个类来表示非负整数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
abstract class Nat {
def isZero: Boolean
def predecessor: Nat
def successor = new Succ(this)
def + (that: Nat): Nat
def - (that: Nat): Nat
}
object Zero extends Nat {
def isZero = true
def predecessor = throw new Error("0.predecessor")
def + (that: Nat) = that
def - (that: Nat) = if (that.isZero) this else throw new Error("negative number")
}
class Succ(n: Nat) extends Nat {
def isZero = false
def predecessor = n
def + (that: Nat) = new Succ(n + that)
def - (that: Nat) = if (that.isZero) this else n - that.predecessor
}

简单解释一下:

假设现在有6个数([0], [1], [2], [3], [4], [5]),按上面的就表示为

1
2
3
4
5
6
Zero
Succ(Zero)
Succ(Succ(Zero))
Succ(Succ(Succ(Zero)))
Succ(Succ(Succ(Succ(Zeror))))
Succ(Succ(Succ(Succ(Succ(Zero)))))

则计算 [2] + [3] 的过程为

1
2
3
4
5
[2] + [3]
Succ([1] + [3])
Succ(Succ([0] + [3]))
Succ(Succ([3]))
[5]

计算 [5] - [2] 的过程为

1
2
3
4
[5] - [2]
[4] - [1]
[3] - [0]
[3]

这样定义的数叫做皮亚诺数(Peano numbers)

类型边界

Scala中这样写泛型的边界:

1
def assertAllPos[S IntSet](r: S): S = …
  • S <: T表示ST的子类
  • S >: T表示ST的父类

也可以混合用:

1
[S >: NonEmpty IntSet]

协变(covariance)

如果

1
NonEmpty IntSet

那是否

1
List[NonEmpty] List[IntSet]

也成立呢?

直觉上说是有意义的。它们的子类型关系根据类型参数变化,称为协变。但这对所有的类型来说都有意义吗?

先看看Java(和C#)中是怎么样的。

Java中的数组是协变的,所以有:

1
NonEmpty[]

但是协变数组类型会引发问题。考虑下面这段Java代码。

1
2
3
4
NonEmpty[] a = new NonEmpty[]{new NonEmpty(1, Empty, Empty)}
IntSet[] b = a
b[0] = Empty
NonEmpty s = a[0]

注意最后一行中一个Empty被赋值给一个NonEmpty

哪儿错了?

其实当执行上面这段代码时,第3行会抛出一个运行时错误ArrayStoreException。Java会在每个数组中保存一个类型标签,标签内容是这个数组创建时的类型,这会阻止把Empty赋值给b[0]

这实际上把一个编译时错误变成了运行时错误,为什么Java要这样做呢?因为Java 1.5之前并不支持泛型,所以就有类似sort(Object[] array)这样的代码,Object[]使得String[]等类型可以被传递给sort

那么什么时候应该用子类型,什么时候不应该呢?

里氏替换原则(Liskov Substitution principle)

If A <: B, then everything one can to do with a value of type B one should also be able to do with a value of type A.

(原文是

Let q(x) be a property provable about objects x of type B. Then q(y) should be provable for objects y of type A where A

1
2
3
4
val a: Array[NonEmpty] = Array(new NonEmpty(1, Empty, Empty))
val b: Array[IntSet] = a
b(0) = Empty
val s: NonEmpty = a(0)

哪一行会报错?是第2行。

Scala 中的 Array 不支持协变。