本文是一个系列,是函数式Swift的读书笔记(其实是为了备忘)
函数在 Swift 中是一等值 (first-class-values),换句话说,函数可以作为参数被传递到其它函数,也可以作为其它函数的返回值
案例:BattleShip
这个例子是 判断一个给定的点是否在射程范围内,并且距离友方船舶和我们自身都不太近
1,非函数式方式:(我之前一直用这种方式编程的)
typealias Distance = Double
struct Position{
var x: Double
var y: Double
}
//1.假设船舶在原点
extension Position{
func withIn(range:Distance) -> Bool {
return sqrt(x*x + y*y) <= range
}
}
// 考虑到船舶有位置, 需要一个ship
struct Ship {
var position : Position
var firingRange :Distance
var unsafeRange : Distance
}
//2.只考虑是否在敌船范围内。
extension Ship{
func canEngage(ship target:Ship) -> Bool {
let dx = target.position.x - position.x
let dy = target.position.y - position.y
let targetDistance = sqrt(dx*dx + dy*dy)
return targetDistance <= firingRange
}
}
//3.避免与过近的敌船交战
extension Ship{
func canSagelyEngage(ship target:Ship) -> Bool {
let dx = target.position.x - position.x
let dy = target.position.y - position.y
let targetDistance = sqrt(dx * dx + dy * dy)
return targetDistance <= firingRange && targetDistance > unsafeRange
}
}
//4.避免目标船舶与友方船舶靠的过近
extension Ship {
func canSafelyEngage(ship target: Ship, friendly: Ship) -> Bool {
let dx = target.position.x - position.x
let dy = target.position.y - position.y
let targetDistance = sqrt(dx * dx + dy * dy)
let friendlyDx = friendly.position.x - target.position.x
let friendlyDy = friendly.position.y - target.position.y
let friendlyDistance = sqrt(friendlyDx * friendlyDx +
friendlyDy * friendlyDy)
return targetDistance <= firingRange
&& targetDistance > unsafeRange
&& (friendlyDistance > unsafeRange)
}
}
// 写一个辅助方法和一个计算属性负责几何运算,从而让代码清晰一些
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)
}
}
//5.添加了辅助方法以后,最终代码变为
extension Ship {
func canSafelyEngage2(ship target: Ship, friendly: Ship) -> Bool {
let targetDistance = target.position.minus(position).length
let friendlyDistance = friendly.position.minus(target.position).length
return targetDistance <= firingRange
&& targetDistance > unsafeRange
&& (friendlyDistance > unsafeRange)
}
}
2.函数式思维的方式
在当前 canSafelyEngage(ship:friendly) 的方法中,主要的行为是 为构成返回值的布尔条件 组合 进行编码
原来的问题归根结底是要 定义一个函数 来判断一个点是否在范围内。
func positionInRange(position:Position)-Bool{
// 判断点是否在范围内
}
可以用一个独立的名词来命名该函数类型
typealias Rangin = (Position)->Bool
Region 类型将指代把 Position 转化为 Bool 的函数。它可以让我们更容易理解在接下来即将看到的一些类型
我们有意识地选择了 Region 作为这个类型的名字,而非 CheckInRegion 或 RegionBlock 这种字里行间暗示着它们代表一种函数类型的名字。函数式编程的核心理念就是函数是值,它和结构体、整型或是布尔型没有什么区别
定义一个以原点为圆心的圆。返回一个函数,以半径 r 为参数,调用 circle(radius: r) 返回的是一个函数。这里我们使用了 Swift 的闭包来构造我们期待的返回函数。
func circle(radius:Distance) -> Region {
return {
point in
point.length <= radius
}
}
要得到一个圆心是任意定点的圆,我们只需要添加另一个代表圆心的参数,并确保在计算新区域时将这个参数考虑进去
func circle2(radius:Distance,center:Position) -> Region {
return {
point in
point.minus(center).length <= radius
}
}
// 如果我么你想要更多的图形组件,我们于鏊重复这些代码,更加函数式的方式是写一个区域变换函数,这个函数按一定的偏移量移动一个区域:
func shift(_ region: @escaping Region,by offSet:Position)->Region{
return {
point in
region(point.minus(offSet))
}
}
// 调用 shift(region, by: offset) 函数会将区域向右上方移动,偏移量分别是 offset.x 和 offset.y。我们需要的是一个传入 Position 并返回 Bool 的函数 Region。为此,我们需要另写一个闭包,它接受我们要检验的点,这个点减去偏移量之后我们得到一个新的点。最后,为了检验新点是否在原来的区域内,我们将它作为参数传递给 region 函数。
//这是函数式编程的核心概念之一:为了避免创建像 circle2 这样越来越复杂的函数,我们编写了一个 shift(_:by:) 函数来改变另一个函数。例如,写一个圆心为(5,5),半径为10的圆, 可以用下边的方式
let shifted = shift(circle(radius:10),by:Position(x:5,y:5))
//还有很多类似的方法,比如反转一个区域以定义另外一个区域
func invert(_ region:@escaping Region)->Region{
return{!region($0)}
}
// 一个区域的交集和并集
func intersect(_ region:@escaping Region,with other:@escaping Region) -> Region {
return{
region($0)&&other($0)
}
}
func union(_ region:@escaping Region,with other:@escaping Region)->Region{
return {
region($0)||other($0)
}
}
// 然后,可以利用上边的函数,生成新的region函数
func subtract(_ region:@escaping Region,from original:@escaping Region)->Region{
return intersect(original, with: invert(region))
}
//解析:这里其实就是,传入某个region,按照某种规则,生成另外一个region。
3. 函数式的实现方式
函数库已经写完了,可以重写那个复杂的方法了:
extension Ship{
func canSafelyEngage(ship target: Ship, friendlyShip:Ship) -> Bool {
// 我们与敌人的范围交集
let rangeRegion = subtract(circle(radius: unsafeRange), from:circle(radius: firingRange))
//根据我们的位置,偏移一下。
let firingRegion = shift(rangeRegion, by: position)
// 友船的范围
let friendlyRengion = shift(circle(radius: unsafeRange), by: friendlyShip.position)
//最终的交集范围
let resultRegion = subtract(friendlyRengion, from: firingRegion)
// 判断
return resultRegion(target.position)
}
}
//我想吐槽,有点麻烦。
//与原来的 canSafelyEngage(ship:friendly:) 方法相比,使用 Region 方法重构后的版本是更加声明式的解决方案。
4. 优化
Region 类型的方法有它自身的缺点。我们选择了将 Region 类型定义为简单类型,并作为 (Position) -> Bool 函数的别名。其实,我们也可以选择将其定义为一个包含单一函数的结构体”
struct Region{
let lookup: Position->Bool
}
//接下来我们可以用 extensions 的方式为结构体定义一些类似的方法,来代替对原来的 Region 类型进行操作的自由函数,“这可以让我们能够通过对区域进行反复的函数调用来变换这个区域,直至得到需要的复杂区域,而不用像以前那样将区域作为参数传递给其他函数
//这种方法有一个优点,它需要的括号更少。再者,这种方式下 Xcode 的自动补全在装配复杂的区域时会十分有用
rangeRegion.shift(ownPosition).difference(friendlyRegion)
4.类型驱动开发。
我们定义了一系列函数来描述区域。每一个函数单打独斗的话都并不强大。然而装配到一起时,却可以描述你绝不会想要从零开始编写的复杂区域。
这与单纯地将 canSafelyEngage(ship:friendly:) 方法拆分为几个独立的方法那种重构方式是完全不同的。 我们确定了 如何来定义区域 ,这是至关重要的设计决策。当我们选择了 Region 类型之后,其它所有的定义就都自然而然,水到渠成了。
这个例子给我们的启示是,我们应该 谨慎地选择类型 。这比其他任何事都重要,因为类型将左右开发流程。
//概念性的东西比较多,所以文字比较多,感觉不是总结,而是复制粘贴书。但是为了易懂,还是觉得这样。