Think Functionally

函数在Swift里是一等公民,也就是说,函数可以被当做参数传递给其他的函数,并且函数也可以返回新的函数。如果你曾经和简单的类型打交道,比如整型、布尔型和结构体,那么这些说法就会显得很奇怪。这里我们讲尝试去解释为什么函数作为一等公民是有益的,并且实际提供函数式编程的一些例子。

例子:超级战舰

我们从一个小例子开始介绍一等公民——函数:当我们在写一个类似于超级战舰的游戏时,我们很可能就需要实现这样一个函数。这个函数归结为决定对一个给定的点,判断它是不是在射程当中,并且不能离友军和我们自己太近。
一开始,你可能就写出一个非常简单的函数来判断给定点是不是在射程范围内。简单起见,我们假设自己的战舰位于原点上。我们想要描述的区域就如图所示:


The Points in range of a ship located at the origin

我们写出第一个函数inRange1, 检查一个点是不是在上图的灰色区域中。运用了一些简单的几何知识,我们写出如下的函数:

typealias Position = CGPoint
typealias Distance = CGFloat

func inRange1(target: Position, range: Distance) -> Bool {
return sqrt(target.x * target.x + target.y * target.y) <= range
}

我们用到了Swift中的typealias,它允许我们为一个已经存在的类型起一个新的名字。之后我们再写Postion就代表着是CGPoint,也就是坐标系中的xy

如果一直假设我们自己的战舰位于原点,那这个函数就可以胜任了。但是假设我们的战舰也可能会存在于另外一个位置,ownposition,而不是在原点。我们将更新一下图示:


Allowing the ship to have its ownposition

现在我们将增加参数来表示自己战舰的位置:

func inRange2(target: Position, ownPosition: Position, range: Distance) ->  Bool {
let dx = ownPosition.x - target.x
let dy = ownPosition.y - target.y
let targetDistance = sqrt(dx * dx + dy * dy)
return targetDistance <= range
}

然后我们又意识到了,还有不能太靠近自己这个条件。我们继续更新我们的图示,我们将只想把目标瞄准至少离我们自己minimumDistance距离之外的敌人:


Avoiding engaging enemies too close to the ship

同时我们也更新代码:


let minimunDistance: Distance = 2.0

func inRang3(target: Position, ownPosition: Position, range: Distance) -> Bool {
let dx = ownPosition.x - target.x
let dy = ownPosition.y - target.y
let targetDistance = sqrt(dx * dx + dy * dy)
return targetDistance <= range && targetDistance >= minimunDistance
}

最后,你还需要避免太靠近友军舰船的目标,再一次更新视图为:


Avoiding engaging enemies too close to fridendly ship

相应的,我们进一步增加一个参数来表示友军战舰的位置:


func inRange4(target: Position, ownPosition: Position, friendly: Position, range: Distance) -> Bool {
let dx = ownPosition.x - target.x
let dy = ownPosition.y - target.y
let targetDistance = sqrt(dx * dx + dy * dy)
let friendlyDx = friendly.x - target.x
let friendlyDy = friendly.y - target.y
let friendlyDistance = sqrt(friendlyDx * friendlyDx + friendlyDy * friendlyDy)
return targetDistance <= range
&& targetDistance >= minimunDistance
&& (friendlyDistance >= minimunDistance)
}

随着不断演化,代码变得越来越难掌握。这个方法变成了很复杂的一大堆东西。让我们尝试重构它,让它成为更细的组件。

一等公民——函数

针对这里的代码,有很多种重构的方法。一个最明显的模式就是引入一个函数来计算两点之间的距离,或者检查两点是靠近还是远离的函数(或者一些关于靠近和远离的定义)。然而,这里我们采用一个略有不同的方法。

我们回到问题的开始,归根结底,我们需要定义一个函数来决定一个点是不是在射程之内。这个类型的函数就是如下类型:

func pointInRange(point: Position) -> Bool {
//Implement method here
}

在我们的问题中,这个函数非常重要,所以我们单独给它一个名字:

typealias Region = Position -> Bool

从这开始,Region类型就代表了把Position转换成Bool的一类函数。严格来说,命名不是必须的,但是这将有助于我们对之后的内容理解。

我们用一个函数——这个函数可以判断对于给定点是不是在射程当中——来表示一个区域,而不是采用一个类或者结构来表示。如果你之前没有过函数变成的经验,这里可能会显得有些奇怪,但是请务必记住的是函数就是Swift当中的一等公民。我们很自觉的选择了Region这个名字,而不是CheckInregion或者RegionBlock。这些名字暗示着它还是一个函数类型,但是函数式变成的内在哲学就是函数就是值,和结构体、整型或者布尔没有任何区别——采用不同的命名方式将违反这个哲学。

现在开始我们讲写一些列的函数来创建,操作和结合区域。我们呢首先定义的区域就是circle,一个圆心在原点的圆形:

func circle(radius: Distance) -> Region {
return { point in
sqrt(point.x * point.x + point.y * point.y) <= radius
}
}

需要注意的是,对于给定的半径r,调用cirecle(r)将返回一个函数。这里我们采用了Swift的闭包来构建我们需要返回的函数。返回的函数就是对于给定的点point,来检查它是不是在给定半径圆心在原点的圆里。

当然不是所有的圆的圆心都在原点。我们可以增加更多的参数来表示circle,这里我们增加一个center参数:

func circle2(radius: Distance, center: Position) -> Region {
return { point in
let shiftedPoint = Position(x: point.x - center.x, y: point.y - center.y)
return sqrt(shiftedPoint.x * shiftedPoint.x + shiftedPoint.y * shiftedPoint.y) <= radius
}
}

然而如果我们想要对其他的形状做相同的改变时,比如我们还有一些其他的形状,例如矩形,按照这个套路,我们很可能又要重复写这样的代码。一个更加具有函数性质的做法是写一个区域转换函数,这个函数把一个区域按照一个给定的位置来转换:

func shift(offset: Position, region : Region) -> Region {
return {point in
let shiftedPoint = Position(x: point.x - offset.x, y: point.y - offset.y)
return region(shiftedPoint)
}
}

这个函数shift(offset, region)把一个区域region往右上方移动了offset.x和offset.y的距离。shift需要返回一个Region类型的值,而Region类型是一个把点转换成布尔的函数。一开始我们就写了另外一个闭包,引入了我们需要检查的点,通过这个点,我们通过坐标计算出另外一个新的点(point.x - offset.x, point.y - offset.y)。最后我们通过把新的点当做参数传进region函数来判断这个点是不是在原始的region当中。

这也是函数式变成的一个核心概念:不要去创建不断增加的复杂的函数,比如cicle2,而是去另外写一个函数shift来修改另一个函数。举个例子,一个以(5, 5)为圆心半径为10的圆就可以表示为:

shift(Position(5, 5), circle(10))

像这样转换已经存在的区域的方式还有很多种,如果我们想通过取反一个区域,也就是获取原始区域以外的所有点组成的区域:

func invert(region: Region) -> Region {
return { point in !region(point)}
}

也可以把已经存在的区域组合起来,组成一个更大或者更加复杂的区域,比如去交集和并集:

func intersection(region1: Region, region2: Region) -> Region {
return { point in region1(point) && region2(point)}
}


func union(region1: Region, region2: Region) -> Region {
return { point in region1(point) || region2(point)}
}

当然,我们还可以通过这些函数来定义更加丰富的区域。difference函数就接受两个区域作为参数,返回另一个区域,这个区域包含了所有在第一个区域而不在第二个区域的点的集合:

func difference(region: Region, minusRegion: Region) -> Region {
return intersection(region, region2: invert(minusRegion))
}

这个例子展示了Swift可以计算和传递函数,就像整型或者布尔一样。这就让我们可以写出很小的初始函数(比如circle)然后在这基础之上建立一系列的函数,这些函数修改或者结合给定区域以得到新的区域。不用写出非常复杂的函数,而是写出一系列的小的函数,通过这些函数的组装来解决更多的问题。

让我们回到最开始的问题上,通过上面一些列的函数,我们可以重构之前的那个复杂的inRange函数了:

func inRange(ownPosition: Position, target: Position, friendly: Position, range: Distance) -> Bool {
let rangeRegion = difference(circle(range), minusRegion: circle(minimunDistance))
let targetRegion = shift(ownPosition, region: rangeRegion)
let friendlyRegion = shift(friendly, region: circle(minimunDistance))
let resultRegion = difference(targetRegion, minusRegion: friendlyRegion)
return resultRegion(target)
}

代码中定义了两个区域:targetRegionfriendlyRegion,而目标区域就是两者的差。最后直接传递target给我们的目标区域,就能计算出想要的布尔值。

和之前的inRange4函数相比,inRange针对相同的问题提出了一个更具声明性的解决方案。我们可以说inRange函数更好理解,因为这个方案是可组装的。为了理解inRange函数,我们首先得理解每一个组成部分的区域,比如targetRegionfriendlyRegion,然后弄清楚它们是如何结合起来解决问题的。然而对于inRange4,把子区域的计算和描述混杂在一起。利用一些帮助函数把这些概念分开,就正如我们之前做的,就可以增加解决复杂区域的程序的可组合性和易读性。

把函数当成一等公民是这个办法奏效的必须条件,Objective-C同样支持一等公民函数或者block。但是很不幸的是block工作起来很笨重。部分原因是句法问题,声明block和block的类型都不像Swift那样直接。而且之后我们也会看到泛型将会让函数功能大增,将远远超过Objective-C中block能完成的功能。

当然也并不是说我们定义的Region类型就是完美的。比如说,我们不能了解到一个区域是如何构成的:它是由小的区域组成的?还是就是一个简单的圆心在原点的圆形?我们知道的仅仅是对于给定的点,我们就知道它在或者是不在区域当中。如果我们想要可视化这个区域,我们讲采样足够的点来形成一个位图。

类型驱动的开发

在这个例子中,我们用一个具体的例子说明了函数式变成的设计方法。定义一些列的函数来描述区域,单独看起来它们都不是很强大。但是它们在一起,就可以描述你肯定不愿意去从头写的复杂区域。

这个解决方案很简单并且优雅。它和你可能写出来的代码很不一样。它的核心设计思想是如何定义一个区域。一旦我们定义了Region类型,其他的定义就顺其自然的发生了。这个例子就是说明了慎重选择类型。是这个类型决定开发的进程,而不是其他的。