《函数式Swift》读书拾遗

前言

设计良好的Swift函数式程序应该具有的一些特质:

  • 模块化
  • 对可变状态的谨慎处理
  • 类型

避免使用程序状态和可变对象,是降低程序复杂度的有效方式之一,而这也正是函数式编程的精髓。

函数式思想

1. 常规的重构思路

抽象实体,扩展方法,逻辑复杂之后抽象出辅助方法

2. 函数式

函数式编程的核心理念就是函数是值。(因此函数的typealias的命名规则应与类、结构体相同)

实例代码链式版本:

这个例子是你在编写战舰类游戏时可能需要实现的一个核心函数。我们把将要看到的问题归结为,判断一个给定的点是否在射程范围内,并且距离友方船舶和我们自身都不太近。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import Foundation

typealias Distance = Double

struct Position {
var x: Double
var y: Double
}

extension Position {
func minus(p: Position) -> Position {
return Position(x: x - p.x, y: y - p.y)
}
var length: Double {
return sqrt(x * x + y * y)
}
}

struct Ship {
var position: Position
var firingRange: Distance
var unsafeRange: Distance
}

struct Region {
let lookup: Position -> Bool
}

extension Region {
static func circle(redius: Distance) -> Region {
return Region(lookup: { point in point.length <= redius })
}

func shift(offset: Position) -> Region {
return Region(lookup: { point in self.lookup(point.minus(offset)) })
}

func invert() -> Region {
return Region(lookup: { point in !self.lookup(point) })
}

func intersection(region: Region) -> Region {
return Region(lookup: { point in self.lookup(point) && region.lookup(point) })
}

func union(region: Region) -> Region {
return Region(lookup: { point in self.lookup(point) || region.lookup(point) })
}

func difference(minus: Region) -> Region {
return self.intersection(minus.invert())
}
}

extension Ship {
func canSafelyEngageShip(target: Ship, friendly: Ship) -> Bool {
let unsafeRegion = Region.circle(self.unsafeRange)
let maxfireRegion = Region.circle(self.firingRange)
let rangeRegion = maxfireRegion.difference(unsafeRegion)

let firingRegion = rangeRegion.shift(position)
let friendlyRegion = unsafeRegion.shift(friendly.position)

let resultRegion = firingRegion.difference(friendlyRegion)

return resultRegion.lookup(target.position)
}
}


let myShip = Ship(position: Position(x: 2, y: 2), firingRange: 10, unsafeRange: 2)
let enemyShip = Ship(position: Position(x: 5, y: -7), firingRange: 10, unsafeRange: 2)
let friendShip = Ship(position: Position(x: 5, y: -5), firingRange: 10, unsafeRange: 2)

myShip.canSafelyEngageShip(enemyShip, friendly: friendShip)

类型驱动开发

函数生成器,生成预设参数的专用函数。

一些优势:

  • 安全
  • 模块化
  • 清晰易懂

柯里化带来的一些好处(重点在于统一类型):

按照柯里化⻛格来定义滤镜,我们可以很容易地使用 >>> 运算符将它们进行组合。假如我们用这些函数未柯里化的版本来构建滤镜的话,虽然依然可以写出相同的滤镜,但是这些滤镜的类型将根据它们所接受的参数不同而略有不同。这样一来,想要为这些不同类型的滤镜定义一个统一的组合运算符就要比现在困难得多了。

自己的感受就是利于统一类型,将额外的参数都包装在生成的函数中,只保留源作为参数,加工之后的结果作为输出,方便对于函数类型的抽象。

Map、Filter和Reduce

接受其它函数作为参数的函数有时被称为高阶函数

Map

1
2
3
4
5
6
7
8
9
extension Array {
func map<T>(transform: Element -> T) -> [T] {
var result: [T] = []
for x in self {
result.append(transform(x))
}
return result
}
}

由顶层函数转为了协议扩展,这样做的优点是自动补全更完善,暧昧的命名更少,以及(通常)代码结构更清晰。

Filter

1
2
3
4
5
6
7
8
9
extension Array {
func filter(includeElement: Element -> Bool) -> [Element] {
var result: [Element] = []
for x in self where includeElement(x) {
result.append(x)
}
return result
}
}

Reduce

1
2
3
4
5
6
7
8
extension Array {
func reduce<T>(initial: T, combine: (T, Element) -> T) -> T {
var result = initial for x in self {
result = combine(result, x)
}
return result
}
}

用reduce实现map跟filter的版本

1
2
3
4
5
6
7
8
9
10
11
12
13
extension Array {
func mapUsingReduce<T>(transform: Element -> T) -> [T] {
return reduce([]) { result, x in
return result + [transform(x)]
}
}

func filterUsingReduce(includeElement: Element -> Bool) -> [Element] {
return reduce([]) { result, x in
return includeElement(x) ? result + [x] : result
}
}
}

泛型和Any类型

Any类型和泛型两者都能用于定义接受两个不同类型参数的函数。然而,理解两者之间的区别至关重要:泛型可以用于定义灵活的函数,类型检查仍然由编译器负责;而Any类型则可以避开Swift的类型系统(所以应该尽可能避免使用)。

使用泛型允许你无需牺牲类型安全就能够在编译器的帮助下写出灵活的函数;如果使用Any类型,那你就真的孤立无援了。

可选值

??操作符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
in x operator??

func ??<T>(optional: T?, defaultValue: T) -> T {
if let x = optional {
return x
} else {
return defaultValue
}
}

//为了避免defaultValue的无效求值
in x operator ?? { associativity right precedence 110 }

func ??<T>(optional: T?, @autoclosure defaultValue: () -> T) -> T {
if let x = optional {
return x
} else {
return defaultValue()
}
}

分支上的可选值

1
2
3
4
5
6
7
8
9
10
11
12
13
//switch中的使用
switch madridPopulation {
case 0?: print("Nobody in Madrid")
case (1..<1000)?: print("Less than a million in Madrid")
case .Some(let x): print("\(x) people in Madrid")
case .None: print("We don't know about Madrid")
}

//guard中的使用
func populationDescriptionForCity(city: String) -> String? {
guard let population = cities[city] else { return nil }
return "The population of Madrid is \(population * 1000)"
}

可选映射

1
2
3
4
5
6
7
8
9
10
11
12
13
extension Optional {
func map<U>(transform: Wrapped -> U) -> U? {
guard let x = self else { return nil }
return transform(x)
}
}

extension Optional {
func flatMap<U>(f: Wrapped -> U?) -> U? {
guard let x = self else { return nil }
return f(x)
}
}

为什么使用可选值?

避免参数不合法

1
NSParameterAssert(param);

可选类型标识了失败的可能性,类型系统将有助于你捕捉难以察觉的错误。其中一些错误很容易在开发过程中被发现,但是其余的可能会一直留存到生产代码中去。坚持使用可选值能够从根本上杜绝这类错误。

QuickCheck