代码地址
前言
Swift函数式程序的特性:
- 模块化:函数式编程更倾向于将程序反复分解为越来越小的模块单元,而这些块可以通过函数装配起来,以定义一个完整的程序。
- 对可变状态的谨慎处理:面向对象编程专注于类和对象的设计,每个类和对象都有他们自己的分装状态。而函数式编程强调基于值编程的重要性,这能使我们免受可变状态或一些其他副作用的干扰。通过避免可变状态,函数式程序比其对应的命令式或者面向对象的程序更容易组合。
- 类型:适合的数据和函数的类型,将有助于构建代码,这比其他东西都重要。Swift有强大的类型系统,使用得当能让代码更加安全和健壮。
函数式编程的基本思想
避免使用程序状态和可变对象,是降低程序复杂度的有效方式之一,而这也是函数式编程的精髓。函数式编程强调的是执行的结果,而非执行的过程。先构建一系列简单却有一定功能的小函数,然后再将这些函数进行组装以实现完整的逻辑和复杂的运算。
函数式思想
函数在Swift中是一等值(first-class-values),也就是说函数可以作为参数传递给其他函数,也可以作为其他函数的返回值。
案例:Battleship
这个例子引出一等函数:编写战舰类游戏需要实现的一个核心函数。问题归结为判断给定的点是否在射程范围内。
首先定义两种类型,Distance(表示距离)、Position(表示位置)
typealias Distance = Double
struct Position {
var x: Distance
var y: Distance
}
接着引入Ship结构体,用来表示战船
struct Ship {
/// 位置
var position: Position
/// 射程范围
var firingRange: Distance
/// 不安全范围
var unsafeRange: Distance
}
然后扩展Ship类型,添加一个canEnageShip函数用于检查是否另一艘船在射程范围内
extension Ship {
func canEnageShip(target: Ship) -> Bool {
let dX: Distance = target.position.x - position.x
let dY: Distance = target.position.y - position.y
return sqrt(dX*dX + dY*dY) <= firingRange
}
}
再扩展inUnsafeRange函数来判断是否另一艘船在不安全范围内
func inUnsafeRange(target: Ship) -> Bool {
let dX: Distance = target.position.x - position.x
let dY: Distance = target.position.y - position.y
return sqrt(dX*dX + dY*dY) <= unsafeRange
return position.minus(tagert: target.position).length() <= unsafeRange
}
游戏中我们需要安全输出,所以扩展canSafelyEngageShip函数来判断是否另一艘船在射程范围内并且在不安全范围外
func canSafelyEngageShip(target: Ship) -> Bool {
return canEnageShip(target: target) && (!inUnsafeRange(target: target))
}
随着代码的发张,canEnageShip、inUnsafeRange中包含了一段复杂的计算代码,可以在Position中扩张几个辅助函数专门负责几何运算,可以让这些代码变得更清晰易懂些。重新修改后代码如下:
extension Position {
func minus(tagert: Position) -> Position {
return Position(x: x - tagert.x, y: y - tagert.y)
}
func length() -> Distance {
return sqrt(x*x + y*y)
}
}
extension Ship {
func canEnageShip(target: Ship) -> Bool {
return position.minus(tagert: target.position).length() <= firingRange
}
func inUnsafeRange(target: Ship) -> Bool {
return position.minus(tagert: target.position).length() <= unsafeRange
}
func canSafelyEngageShip(target: Ship) -> Bool {
return canEnageShip(target: target) && (!inUnsafeRange(target: target))
}
}
一等函数
在当前的这一系列函数中,主要行为是为构成返回值的布尔条件组合进行编码。虽然在这个例子中函数做了什么并不复杂,但还是有更加模块化的解决方案。
问题归根结底是要定义是要定义一个函数判定一个点是否在特定范围内,这样的函数类型为这样:function pointInRegion(point: Poosition) -> Bool。这个函数的类型非常重要,给它一个独立的名字:
typealias Regin = (Position) -> Bool
从现在开始Region指代把Position转化为Bool的函数,换句话说就是用一个能判定给定点是否在范围内的函数来代表一个区域。
用函数而不是一个结构体或对象来表示一个区域,这就回到了函数式编程的核心理念函数是值,它与结构体、Bool值没什么区别。
既然如此,就可以使用Region来定义区域:
/// 圆心为原点半径为radius的圆
///
/// - Parameter radius: 半径
/// - Returns: 圆形区域
func circle(radius:Distance) -> Region {
return { point in point.length() <= radius }
}
然而并不是所有圆的圆心都在原点,可以通过增加参数来解决:
/// 圆心为center半径为radius的圆
///
/// - Parameters:
/// - center: 圆心
/// - radius: 半径
/// - Returns: 圆形区域
func circle1(center: Position, radius: Distance) -> Region {
return { point in center.minus(tagert: point).length() <= radius }
}
可是我想对更多图形组件做出同样的改变而不仅仅是圆,还有矩形或者其他图形。这个时候就需要一个更加函数式的区域变换函数,这个函数按照一定的偏移量移动一个区域:
/// 移动区域
///
/// - Parameters:
/// - region: 区域
/// - offset: 偏移量
/// - Returns: 移动后的区域
func shift(region: @escaping Region, offset: Position) -> Region {
return { point in region(point.plus(tagert: offset)) }
}
到此接触到了函数式编程的一个核心概念,那就是:避免创建像circle1这样越来越复杂的函数,应该编写一些基础的图形组件(如圆),进而以这些组件为基础来构建一系列函数(像shift这样的变换函数来改变另一个函数),这样就可以通过装配小型函数,广泛的解决各样问题。例如用下面的方式表示一个圆:
let circle2 = shift(region: circle(radius: 5), offset: Position(x: 5, y: 5))
接下来还可以编写更多的函数来控制、变换和合并各个区域:
/// 反转区域
///
/// - Parameter region: 区域
/// - Returns: 反转后的区域
func invert(region: @escaping Region) -> Region {
return { point in !region(point) }
}
/// 区域交集
///
/// - Parameters:
/// - region1: 区域1
/// - region2: 区域2
/// - Returns: 交集区域
func intersection(region1: @escaping Region, region2: @escaping Region) -> Region {
return { point in region1(point) && region2(point) }
}
/// 区域并集
///
/// - Parameters:
/// - region1: 区域1
/// - region2: 区域2
/// - Returns: 并集区域
func union(region1: @escaping Region, region2: @escaping Region) -> Region {
return { point in region1(point) || region2(point) }
}
/// 区域差(在第一个区域中但不在第二个区域中)
///
/// - Parameters:
/// - region: 原区域
/// - mimus: 做差区域
/// - Returns: 差区域
func difference(region: @escaping Region, mimus: @escaping Region) -> Region {
return { point in region(point) && (!mimus(point)) }
}
关于区域的小型函数库已经准备完毕,回到战船的例子做如下的重构:
extension Ship {
func canEnageShip(target: Ship) -> Bool {
return shift(region: circle(radius: firingRange), offset: position)(target.position)
}
func inUnsafeRange(target: Ship) -> Bool {
return shift(region: circle(radius: unsafeRange), offset: position)(target.position)
}
func canSafelyEngageShip(target: Ship) -> Bool {
return difference(region: shift(region: circle(radius: firingRange), offset: position), mimus: shift(region: circle(radius: unsafeRange), offset: position))(target.position)
}
}
更进一步,我觉得可以把这几个判断另一艘船是否在某范围内的函数替换为一系列表示区域的函数,更加方便装配:
/// 射程区域
///
/// - Returns: 区域
func enageRegion() -> Region { return shift(region: circle(radius: firingRange), offset: position) }
/// 不安全区域
///
/// - Returns: 区域
func unsafeRegion() -> Region { return shift(region: circle(radius: unsafeRange), offset: position) }
/// 安全输出区域
///
/// - Returns: 区域
func safelyEngageReion() -> Region { return difference(region: enageRegion(), mimus: unsafeRegion()) }
面对同意问题,使用Region函数重构后的版本是更加申明式的解决方案。后面这个版本是更容易理解的,因为这种方案是装配式的。
然而将Region类型定义为简单类型,并作为Position -> Bool函数的别名这种方法有它自身的缺点。其实可以定义一个包括单一函数的结构体:
struct Region1 {
let lookup: (Position) -> Bool
}
接下来用extensions的方式为结构体定义一些类似函数来替代原来对Region类型进行操作的函数。这可以通过对区域进行反复的函数变换来得到需要的复杂区域,而不像之前那样将区域做为参数传递给其他函数:
struct Region1 {
let lookup: (Position) -> Bool
}
extension Region1 {
func shift(offset: Position) -> Region1 { return Region1(lookup: { point in self.lookup(point.plus(tagert: offset)) }) }
func invert() -> Region1 { return Region1(lookup: { point in !self.lookup(point) }) }
func intersection(other: Region1) -> Region1 { return Region1(lookup: { point in self.lookup(point) && other.lookup(point) }) }
func union(other: Region1) -> Region1 { return Region1(lookup: { point in self.lookup(point) || other.lookup(point) }) }
func difference(other: Region1) -> Region1 { return Region1(lookup: { point in self.lookup(point) && (!other.lookup(point)) }) }
}
func circle3(radius: Distance) -> Region1 { return Region1(lookup: { point in point.length() <= radius }) }
extension Ship {
func enageRegion1() -> Region1 { return circle3(radius: firingRange) }
func unsafeRegion1() -> Region1 { return circle3(radius: unsafeRange) }
func safelyEngageReion1() -> Region1 { return enageRegion1().difference(other: unsafeRegion1()) }
}
这样做法有两个优点:
- 需要的括号更少
- 这种方式下,Xcode的自动补全对装配复杂的区域十分有用
再增加一些问题的复杂性,如果在这个时候如果我拥有的不是一艘战舰,而是一个舰队。那么问题的处理将会如下:
extension Ship {
func allEnageRegion1(friends: Ship ...) -> Region1 { return friends.reduce(enageRegion1(), { (region, friend) in region.union(other: friend.enageRegion1()) }) }
func allUnsafeRegion1(friends: Ship ...) -> Region1 { return friends.reduce(unsafeRegion1(), { (region, friend) in region.union(other: friend.unsafeRegion1()) }) }
func allSafelyEnageRegion1(friends: Ship ...) -> Region1 { return friends.reduce(safelyEnageReion1(), { (region, friend) in region.union(other: friend.safelyEnageReion1()) }) }
}
注意事项
值得说明的是前面是如何被构建的?既不是以更小的区域组成的也不是单纯的图形,唯一能做的就是检验一个点是否在区域内,如果要形象化这些区域,只能对足够多的点进行采样来生成位图。
总结
函数式编程可以用规范的方式将函数作为参数装配为更大的程序。从前面的例子看,每个函数单独都不算强大,然而装配到一起的时候却可以描述非常复杂的区域,解决办法简单而优雅。这与单纯的将函数拆分的方法是完全不同的。在这里如何定义区域是至关重要的,其他所有定义都是自然而然、水到渠成。
启示
应该谨慎的选择类型,这无比的重要,将左右开发的流程。