【函数式 Swift】函数式思想

所谓函数式编程方法,是借助函数式思想对真实问题进行分析和简化,继而构建一系列简单、实用的函数,再“装配”成最终的程序,以解决问题的方法。javascript

本章关键词

请带着如下关键词阅读本文:java

  • 一等值(一等函数)
  • 模块化
  • 类型驱动

案例:Battleship

本章案例是一个关于战舰攻击范围计算的问题,描述以下:git

  • 战舰可以攻击到射程范围内的敌船
  • 攻击时不能距离自身太近
  • 攻击时不能距离友船太近

问题:计算某敌船是否在安全射程范围内。github

对于这个问题,咱们换一种描述:编程

  • 输入:目标(Ship)
  • 处理:计算战舰到敌船的距离、敌船到友船的距离,判断敌船距离是否在射程内,且敌船到友船距离足够大
  • 输出:是否(Bool)

看上去问题并不复杂,咱们能够产出如下代码:swift

typealias Distance = Double

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

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

extension Ship {
    func canSafelyEngageShip(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
    }
}复制代码

能够看出,canSafelyEngageShip 方法分别计算了咱们须要的两个距离:targetDistancefriendlyDistance,随后与战舰的射程 firingRange 和安全距离 unsafeRange 进行比较。api

功能看上去没有什么问题了,若是以为 canSafelyEngageShip 方法过于繁琐,还能够添加一些辅助函数:安全

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)
    }
}

extension Ship {
    func canSafelyEngageShip(target: Ship, friendly: Ship) -> Bool {
        let targetDistance = target.position.minus(p: position).length
        let friendlyDistance = friendly.position.minus(p: target.position).length
        return targetDistance <= firingRange
            && targetDistance > unsafeRange
            && (friendlyDistance > unsafeRange)
    }
}复制代码

到此,咱们编写了一段比较直观且容易理解的代码,但因为咱们使用了很是“过程式”的思惟方式,因此扩展起来就不太容易了。好比,再添加一个友船,咱们就须要再计算一个 friendlyDistance_2,这样下去,代码会变得很复杂、难理解。模块化

为了更好解决这个问题,咱们先介绍一个概念:一等值(First-class Value),或者称为 一等函数(First-class Function)函数式编程

咱们来看看维基上的解释:

In computer science, a programming language is said to have first-class functions if it treats functions as first-class citizens. Specifically, this means the language supports passing functions as arguments to other functions, returning them as the values from other functions, and assigning them to variables or storing them in data structures.

简单来讲,就是函数与普通变量相比没有什么特殊之处,能够做为参数进行传递,也能够做为函数的返回值。在 Swift 中,函数是一等值。带着这个思惟,咱们尝试使用更加声明式的方式来思考这个问题。

归根结底,就是定义一个函数来判断一个点是否在范围内,因此咱们要的就是一个输入 Position,输出 Bool 的函数:

func pointInRange(point: Position) -> Bool {
    ...
}复制代码

而后,咱们就能够用这个可以判断一个点是否在区域内的函数来表示一个区域,为了更容易理解,咱们给这个函数起个名字(由于函数是一等值,因此咱们能够像变量同样为其设置别名):

typealias Region = (Position) -> Bool复制代码

咱们将攻击范围理解为可见区域,超出攻击范围或处于不安全范围均视为不可见区域,那么可知:

有效区域 = 可见区域 - 不可见区域

如此,问题从距离运算演变成了区域运算。明确问题后,咱们能够定义如下区域:

// 圆心为原点,半径为 radius 的圆形区域
func circle(radius: Distance) -> Region {
    return { point in point.length <= radius }
}

// 圆心为 center,半径为 radius 的圆形区域
func circle2(radius: Distance, center: Position) -> Region {
    return { point in point.minus(p: center).length <= radius }
}

// 区域变换函数
func shift(region: @escaping Region, offset: Position) -> Region {
    return { point in region(point.minus(p: offset)) }
}复制代码

前两个函数很容易理解,但第三个区域有些特别,它将一个输入的 region 经过 offset 变化后返回一个新的 region。为何要有这样一个特殊“区域”呢?其实,这是函数式编程的一个核心概念,为了不产生 circle2 这样会不断扩展而后变复杂的函数,经过一个函数来改变另外一个函数的方式更加合理。例如,一个圆心为 (5,5) 半径为 10 的圆就能够用下面的方式来表示了:

shift(region: circle(radius: 10), offset: Position(x: 5, y: 5))复制代码

掌握了 shift 式的区域定义方法,咱们能够继续定义如下“区域”:

// 将原区域取反获得新区域
func invert(region: @escaping Region) -> Region {
    return { point in !region(point) }
}

// 取两个区域的交集做为新区域
func intersection(region1: @escaping Region, _ region2: @escaping Region) -> Region {
    return { point in region1(point) && region2(point) }
}

// 取两个区域的并集做为新区域
func union(region1: @escaping Region, _ region2: @escaping Region) -> Region {
    return { point in region1(point) || region2(point) }
}

// 取在一个区域,且不在另外一个区域,获得新区域
func difference(region: @escaping Region, minus: @escaping Region) -> Region {
    return intersection(region1: region, invert(region: minus))
}复制代码

很轻松有木有!

基于这个小型工具库,咱们来改写案例中的代码,并与以前的代码进行对比:

// After
func canSafelyEngageShip(target: Ship, friendly: Ship) -> Bool {
    let rangeRegion = difference(region: circle(radius: firingRange),
     minus: circle(radius: unsafeRange))
    let firingRegion = shift(region: rangeRegion, offset: position)
    let friendlyRegion = shift(region: circle(radius: unsafeRange),
     offset: friendly.position)
    let resultRegion = difference(region: firingRegion, minus: friendlyRegion)
    return resultRegion(target.position)
}

// Before
func canSafelyEngageShip(target: Ship, friendly: Ship) -> Bool {
    let targetDistance = target.position.minus(p: position).length
    let friendlyDistance = friendly.position.minus(p: target.position).length
    return targetDistance <= firingRange
      && targetDistance > unsafeRange
      && (friendlyDistance > unsafeRange)
}复制代码

借助以上函数式的思惟方式,咱们避开了具体问题中一系列的复杂数值计算,获得了易读、易维护、易迁移的代码。


思考

一等值(一等函数)

一等值这个名词咱们可能较少听到,但其概念倒是渗透在咱们平常开发过程当中的。将函数与普通变量对齐,是很重要的一项语言特性,不只是编码过程,在简化问题上也能为咱们带来巨大的收益。

例如本文案例,咱们使用函数描述区域,而后使用区域运算代替距离运算,在区域运算中,又使用了诸如 shift 的函数式思想,进而将问题进行简化并最终解决。

模块化

在《函数式 Swift》的前言部分,有一段对模块化的描述:

相比于把程序认为是一系列赋值和方法调用,函数式开发者更倾向于强调每一个程序都能被反复分解为愈来愈小的模块单元,而全部这些模块能够经过函数装配起来,以定义一个完整的程序。

模块化是一个听上去很酷,遇到真实问题后有时又会变得难如下手,本文案例中,原始问题看上去目标简单而且明确,只须要必定的数值计算就能够获得最终结果,但当咱们借助函数式思惟,将问题的解决转变为区域运算后,关注点就转变为区域的定义上,而后进一步分解为区域变换、交集、并集、差集等模块,最后,将这些模块“装配”起来,问题的解决也就瓜熟蒂落了。

类型驱动

这里的类型,对应着前文中咱们定义的 Region,由于咱们选用了 Region 这个函数式定义来描述案例中的基本问题单元,即判断一个点是否在区域内,从而使咱们的问题转变为了区域运算。

可见,咱们是一种“类型驱动”的问题解决方式,或者说是编码方式,类型的选择决定了咱们解决问题的方向,假如咱们坚持使用 PositionDistance,那么解决问题的方向必然陷入此类数值运算中,显然,函数式的类型定义帮助咱们简化而且更加优雅的解决了问题。


参考资料

  1. Github: objcio/functional-swift
  2. First-class function

本文属于《函数式 Swift》读书笔记系列,同步更新于 huizhao.win,欢迎关注!