【摘录】函数式 Swift

307 阅读12分钟

本文章仅作为本人读此书时的一些记录和摘要,阅读全书请跳转至 Objc 中国 进行购买。侵删。

引言

Swift 函数式程序的一些特质:

  • 模块化:相较于把程序认为是一系列赋值和方法调用,函数式开发者更倾向于强调每个程序都能够被反复分解为越来越小的模块单元,而所有这些块可以通过函数装配起来,以定义一个完整的程序。当然,只有当我们能够避免在两个独立组件之间共享状态时,才能将一个大型程序分解为更小的单元。这引出我们的下一个关注特质。
  • 对可变状态的谨慎处理: 函数式编程有时候 (被半开玩笑地) 称为“面向值编程”。面向对象编程专注于类和对象的设计,每个类和对象都有它们自己的封装状态。然而,函数式编程强调基于值编程的重要性,这能使我们免受可变状态或其他一些副作用的困扰。通过避免可变状态,函数式程序比其对应的命令式或者面向对象的程序更容易组合。
  • 类型: 最后,一个设计良好的函数式程序在使用类型时应该相当谨慎。精心选择你的数据和函数的类型,将会有助于构建你的代码,这比其他东西都重要。Swift 有一个强大的类型系统,使用得当的话,它能够让你的代码更加安全和健壮。

译序

避免使用程序状态和可变对象,是降低程序复杂度的有效方式之一,而这也正是函数式编程的精髓。函数式编程强调执行的结果,而非执行的过程。我们先构建一系列简单却具有一定功能的小函数,然后再将这些函数进行组装以实现完整的逻辑和复杂的运算,这是函数式编程的基本思想。

函数式思想

函数在 Swift 中是一等值 (first-class-values),换句话说,函数可以作为参数被传递到其它函数,也可以作为其它函数的返回值。

案例:Battle ship

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)
    }
}
extension Position {
    func within(range: Distance) -> Bool {
        return sqrt(x * x + y * y) <= range
    }
}

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

extension Ship {
    func canSafelyEngage(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)
    }
}

一等函数

我们使用一个能判断给定点是否在区域内的函数来代表一个区域,而不是定义一个对象或结构体来表示它。如果你不习惯函数式编程,这可能看起来会很奇怪,但是记住:在 Swift 中函数是一等值!我们有意识地选择了 Region 作为这个类型的名字,而非 CheckInRegion 或 RegionBlock 这种字里行间暗示着它们代表一种函数类型的名字。函数式编程的核心理念就是函数是值,它和结构体、整型或是布尔型没有什么区别 —— 对函数使用另外一套命名规则会违背这一理念。

// 一等函数
typealias Region = (Position) -> Bool

// 定义一个以原点为圆心的圆
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)) }
}

// 表示一个圆心为 (5, 5) 半径为 10 的圆
let shifted = shift(circle(radius: 10), by: Position(x: 5, y: 5))

// 表示一个区域外的所有区域
func invert(_ region: @escaping Region) -> Region {
    return { point in !region(point) }
}

// 两个区域的交集
func intersect(_ region: @escaping Region, with other: @escaping Region)
-> Region {
    return { point in region(point) && other(point) }
}

// 两个区域的并集
func union(_ region: @escaping Region, with other: @escaping Region)
-> Region {
    return { point in region(point) || other(point) }
}

// 在第一个区域而不第二个区域
func subtract(_ region: @escaping Region, from original: @escaping Region)
-> Region {
    return intersect(original, with: invert(region))
}

// 新的方法
extension Ship {
    func canSafelyEngageV2(ship target: Ship, friendly: Ship) -> Bool {
        let rangeRegion = subtract(circle(radius: unsafeRange),
        from: circle(radius: firingRange))
        let firingRegion = shift(rangeRegion, by: position)
        let friendlyRegion = shift(circle(radius: unsafeRange),
        by: friendly.position)
        let resultRegion = subtract(friendlyRegion, from: firingRegion)
        return resultRegion(target.position)
    }
}

在 Swift 中计算和传递函数的方式与整型或布尔型没有任何不同。这让我们能够写出一些基础的图形组件 (比如圆),进而能以这些组件为基础,来构建一系列函数。每个函数都能修改或是合并区域,并以此创建新的区域。比起写复杂的函数来解决某个具体的问题,现在我们完全可以通过将一些小型函数装配起来,广泛地解决各种各样的问题。

案例研究:封装 Core Image

滤镜类型

import CoreImage

typealias Filter = (CIImage) -> CIImage

// 模糊
func blur(radius: Double) -> Filter {
    return { image in
        let parameters: [String: Any] = [
            kCIInputRadiusKey: radius,
            kCIInputImageKey: image
        ]
        guard let filter = CIFilter(name: "CIGaussianBlur", parameters: parameters) else { fatalError() }
        guard let outputImage = filter.outputImage else { fatalError() }
        return outputImage
    }
}

// 颜色叠层
func generate(color: UIColor) -> Filter {
    return { _ in
        let parameters = [kCIInputColorKey: CIColor(cgColor: color.cgColor)]
        guard let filter = CIFilter(name: "CIConstantColorGenerator",  parameters: parameters) else { fatalError() }
        guard let outputImage = filter.outputImage else { fatalError() }
        return outputImage
    }
}

// 合成滤镜
func compositeSourceOver(overlay: CIImage) -> Filter {
    return { image in
        let parameters = [
            kCIInputBackgroundImageKey: image,
            kCIInputImageKey: overlay
        ]
        guard let filter = CIFilter(name: "CISourceOverCompositing", parameters: parameters) else { fatalError() }
        guard let outputImage = filter.outputImage else { fatalError() }
        return outputImage.cropped(to: image.extent)
    }
}

// 颜色叠层滤镜
func overlay(color: UIColor) -> Filter {
    return { image in
        let overlay = generate(color: color)(image).cropped(to: image.extent)
        return compositeSourceOver(overlay: overlay)(image)
    }
}

// 先将图像模糊,再覆盖上一层红色叠层
let url = URL(string: "http://via.placeholder.com/500x500")!
let image = CIImage(contentsOf: url)!

let radius = 5.0
let color = UIColor.red.withAlphaComponent(0.2)
let blurredImage = blur(radius: radius)(image)
let overlaidImage = overlay(color: color)(blurredImage)

// 将上面两个表达式合为一体
let result = overlay(color: color)(blur(radius: radius)(image))

使代码更具可读性:

// 失去可读性,更好地办法是构建一个可以将滤镜合二为一的函数
func compose(filter filter1: @escaping Filter, with filter2: @escaping Filter) -> Filter {
    return { image in filter2(filter1(image)) }
}

let blurAndOverlay = compose(filter: blur(radius: radius), with: overlay(color: color))
let result1 = blurAndOverlay(image)

// 更进一步
infix operator >>>

func >>>(filter1: @escaping Filter, filter2: @escaping Filter) -> Filter {
    return { image in filter2(filter1(image)) }
}

let blurAndOverlay2 = blur(radius: radius) >>> overlay(color: color)
let result2 = blurAndOverlay2(image)

由于运算符 >>> 默认是左结合的 (left-associative),就像 Unix 的管道一样,因此滤镜将以从左到右的顺序被应用到图像上。

我们定义的组合滤镜运算符是一个复合函数的例子。在数学中,f 和 g 两个函数构成的复合函数有时候被写作 f · g,表示定义的新函数将输入的 x 映射到 f(g(x))。除了顺序,这恰恰也是我们的 >>> 运算符所做的:将一个图像参数传递给运算符操作的两个滤镜。

理论背景:柯里化

有两种等效的方式能够定义一个可以接受两个 (或更多) 参数的函数。对于大多数程序员来说,应该会觉得第一种风格更熟悉:

func add1(_ x: Int, _ y: Int) -> Int {
    return x + y
}

add1 函数接受两个整型参数并返回它们的和。然而在 Swift 中,我们对该函数的定义还可以有另一个版本:

func add3(_ x: Int) -> (Int) -> Int {
    return { y in x + y }
}

add1 与 add2 的区别在于调用方式:

add1(1, 2) // 3
add2(1)(2) // 3

在第一种方法中,我们将两个参数同时传递给 add1;而第二种方法则首先向函数传递第一个参数 1,然后将返回的函数应用到第二个参数 2。两个版本是完全等价的:我们可以根据 add2 来定义 add1,反之亦然。

add1 和 add2 的例子向我们展示了如何将一个接受多参数的函数变换为一系列只接受单个参数的函数,这个过程被称为柯里化 (Currying),它得名于逻辑学家 Haskell Curry;我们将 add2 称为 add1 的柯里化版本。

那么,为什么说柯里化很有趣呢?正如迄今为止我们在本书中所看到的,在一些情况下,你可能想要将函数作为参数传递给其它函数。如果我们有像 add1 一样未柯里化的函数,那我们就必须同时用到它的全部两个参数来调用这个函数。然而,对于一个像 add2 一样被柯里化了的函数来说,我们有两个选择:可以使用一个或两个参数来调用。

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

讨论

我们相信本章所设计的 API 也有一些优点:

  • 安全 — 使用我们构筑的 API 几乎不可能发生由未定义键或强制类型转换失败导致的运行时错误。
  • 模块化 — 使用 >>> 运算符很容易将滤镜进行组合。这样你可以将复杂的滤镜拆解为更小,更简单,且可复用的组件。此外,组合滤镜与组成它的组件是完全相同的类型,所以你可以交替使用它们。
  • 清晰易懂 — 即使你从未使用过 Core Image,也应该能够通过我们定义的函数来装配简单的滤镜。你完全不需要关心 kCIInputImageKey 或 kCIInputRadiusKey 这样的特定键如何进行初始化。单看类型,你几乎就能够知道如何使用 API,甚至不需要更多文档。

Map、Filter 和 Reduce

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

泛型介绍

关于这段代码,最有意思的是它的类型签名。理解这个类型签名有助于你将 genericCompute 理解为一个函数族。类型参数 T 的每个选择都会确定一个新函数。该函数接受一个整型数组和一个 (Int) -> T 类型的函数作为参数,并返回一个 [T] 类型的数组。

extension Array {
    func map<T>(_ transform: (Element) -> T) -> [T] {
        var result: [T] = []
        for x in self {
            result.append(transform(x))
        }
        return result
    }
}

我们在函数的 transform 参数中所使用的 Element 类型源自于 Swift 的 Array 中对 Element 所进行的泛型定义。

Filter

extension Array {
    func filter(_ includeElement: (Element) -> Bool) -> [Element] {
        var result: [Element] = []
        for x in self where includeElement(x) {
            result.append(x)
        }
        return result
    }
}

Reduce

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:

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

请务必注意:尽管通过 reduce 来定义一切是个很有趣的练习,但是在实践中这往往不是一个什么好主意。原因在于,不出意外的话你的代码最终会在运行期间大量复制生成的数组,换句话说,它会反复分配内存,释放内存,以及复制大量内存中的内容。比如说,用一个可变结果数组来编写 map 的效率显然会更高。理论上,编译器可以优化上述代码,使其速度与可变结果数组的版本一样快,但是 Swift (目前) 并没有那么做。

泛型和 Any 类型

除了泛型,Swift 还支持 Any 类型,它能代表任何类型的值。从表面上看,这好像和泛型极其相似。Any 类型和泛型两者都能用于定义接受两个不同类型参数的函数。然而,理解两者之间的区别至关重要:泛型可以用于定义灵活的函数,类型检查仍然由编译器负责;而 Any 类型则可以避开 Swift 的类型系统 (所以应该尽可能避免使用)。

让我们考虑一个最简单的例子,构想一个函数,除了返回它的参数,其它什么也不做。我们可能写为下面这样:

func noOp<T>(_ x: T) -> T {
    return x
}

func noOpAny(_ x: Any) -> Any {
    return x
}

noOp 和 noOpAny 两者都将接受任意参数。关键的区别在于我们所知道的返回值。在 noOp 的定义中,我们可以清楚地看到返回值和输入值完全一样。而 noOpAny 的例子则不太一样,返回值是任意类型 — 甚至可以是和原来的输入值不同的类型。我们可以给出一个 noOpAny 的错误定义,如下所示:

func noOpAnyWrong(_ x: Any) -> Any {
    return 0
}

使用 Any 类型可以避开 Swift 的类型系统。然而,尝试将使用泛型定义的 noOp 函数返回值设为 0 将会导致类型错误。此外,任何调用 noOpAny 的函数都不知道返回值会被转换为何种类型。而结果就是可能导致各种各样的运行时错误。

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