浅谈 Swift 的函数式编程

2,051 阅读6分钟
原文链接: www.jianshu.com

Swift 在设计上非常注重函数式思想的渗透,这使得我们在日常开发中又有了一个新的方向可以选择。很多人可能不太了解函数式,其实我之前也并没有怎么接触过函数式编程,所以本文也就是漫谈一下函数式给我们带来的便利,有错误的地方也欢迎大家指出。

现在有非常多使用函数式思想设计的库,比如大名鼎鼎的 ReactiveX,它将一系列事物抽象成信号源,你可以观察这个信号源,也可以给它发出的信号裹上一层“衣服”(也就是我们说的变换或者操作符)来得到一个新的信号源,并且这种调用是可以链式进行的,然后我们订阅这个最终的信号源,当初始的信号源发出一个信号,这个信号将经过一层层的变换,变成你想要得到的数据传递给观察者。

这么说可能会有些抽象,举个简单的例子吧,有一个 UITextField,当用户输入的时候我们拿到用户输入的值,交给一个网络请求,将网络请求的结果再交给一个解析函数,得到的结果显示到一个 UILabel 上。整个逻辑如果用传统的方式做将会出现各种状态,各种事件地响应,并且这些状态也将会被修改,如果逻辑再复杂一些,代码将变得十分复杂,这种模式我们称之为命令式编程。那我们来看看用函数式结合 ReactiveX 将会是怎样的情景:

textField.rx_value
    .map(someTransformer)
    .flatMap(startNetworkRequest)
    .map(anotherTransformer)
    .bindTo(label.rx_value)
    .dispose(...)

上面也只是个伪代码了,但是逻辑还是十分清晰的,所有的函数都没有产生副作用,算是比较纯粹的函数式编程了。这样我们就可以将一个复杂的逻辑流变成几行代码就能描述清楚的事件流了。这种模式就是声明式编程。

Getting Started

上面扯了这么多,没有什么干货。下面我们通过一个例子,实战一下函数式编程的实际应用。

在这之前,我们先小试牛刀一下。
假设有下面两个数组:

let numbers = [8, 2, 1, 0, 3]
let indexes = [2, 0, 3, 2, 4, 0, 1, 3, 2, 3, 3]

现在我们要根据 indexes 作为下标依次从 numbers 中取出数字,然后拼接成一个字符串。如果用命令式编程,我们会很容易想到下面这样的代码:

var temp = [Int]()
for i in indexes {
    temp.append(numbers[i])
}

var result = ""
for n in temp {
    result += String(n)
}

print(result)

OK,代码是能 work 的,但我认为这很糟糕,当然也很不 functional,整个逻辑中充满着命令和状态变化。

下面我们就利用函数式把它重写一下:

print(indexes.map({ "\(numbers[$0])" }).reduce("") { $0 + $1 })

这真的很 functional,没有任何新的中间量出现,没有任何状态变化,我们在一行里完成了上面 9 行才能完成的工作。它很好地揭示了函数式编程的核心 mapreduce 是如何工作的。

map —— 将原有元素进行一定地变换,这个变换可以是数值上的变换,也可以是类型上的变换,但唯一不能变的就是输出的维度。也就是说,如果输入是一个整型,那么输出一定也是一个什么类型,而不能是一个数组,因为 map 函数并不能帮你把这个数组展开,虽然语法上没问题,但结果一定不是你想要的。下面的 flatMap 也许能帮到你。

flatMap —— 非常类似 map,只不过这次,你可以变换维度了,经过 flatMap 函数,输入值将会变成另外一个可以被 map 操作的类型(比如数组),然后这个类型中的每个元素将会全部被展开添加到结果中去,也就是说输入可能有三个值,而输出却有更多或者更少的值了,很 magical。下面是个例子:

print([1, 3, 2].flatMap { [Int](1...$0) })    // [1, 1, 2, 3, 1, 2]

输入数组中的每个元素将会产生大小为它自身的一个数列,输出结果就是将这些数列拼接起来了。

reduce —— 数学上的归一化简,就是将一组数经过一定的运算变成一个数,通常我们可以用它来计算一组数的和,例如:

print([1, 2, 3, 4].reduce(0) { $0 + $1 })    // 10

reduce 接受一个初始值和一个函数,在这个函数中你可以拿到当前元素和当前的累加数值,并据此返回一个新的累加数值,以此类推,最终会返回最后一个累加数值。

filter —— 这个就更好理解了,根据一个函数去过滤一个数组的元素,没什么可说的。

What's Next?

Swift 是一个多范式的编程语言。下面我们结合协议、泛型,来看看如何在 Swift 中实现链式运算。

现在假设我们有三种变换:

  • 将一个数进行幂运算
  • 将一个数偶数化,如果它不是偶数则加一
  • 将一个数变成字符串

如果用常规方法,我们将这么做:

string(even(power(e, n)))

这很简单,但现在如果我想增加一个变换呢?我就需要修改函数调用了,显然这会比较麻烦。

下面我们利用函数式思想重构一下这个例子。

首先我们用协议将变换抽象化:

protocol Transformer {
    associatedtype InputType
    associatedtype OutputType
    func transform(elem: InputType) -> OutputType
}

然后实现这些变换:

struct PowerTransformer : Transformer {
    let n: Int

    init(n: Int) {
        self.n = n
    }

    func transform(elem: T) -> T {
        return Int(pow(Double(elem as! Int), Double(n))) as! T
    }
}

struct EvenTransformer : Transformer {
    func transform(elem: T) -> T {
        return elem % 2 == 0 ? elem : elem + 1
    }
}

struct StringTransformer : Transformer {
    func transform(elem: T) -> String {
        return "\(elem)"
    }
}

这里我用到了泛型,如果你对泛型还不了解的话还是建议先去看看官方的 Guide。
现在做链式计算其实和之前还是一样的,但是由于我们有了变换的抽象,我们就能很容易地实现一个组合变换类型:

struct ComposedTransformer : Transformer {
    let transformer1: T
    let transformer2: U

    init(transformer1: T, transformer2: U) {
        self.transformer1 = transformer1
        self.transformer2 = transformer2
    }

    func transform(elem: T.InputType) -> U.OutputType {
        return transformer2.transform(transformer1.transform(elem))
    }
}

它接受两个变换,然后依次调用这两个变换,输出最后的结果。通过递归归纳,我们可以不断地组合,生成组合变换,然后那它再与其他变换组合...得到最终的组合变换我们就可以用于计算各种数值了。

Higher

但是现在创建组合变换貌似有点麻烦,因为要写很长的构造器参数,并且整个语句还是和嵌套函数调用一样。别忘了,Swift 支持自定义运算符!

infix operator ~> { associativity left }
func ~>(t1: T, t2: U) -> ComposedTransformer {
    return ComposedTransformer(transformer1: t1, transformer2: t2)
}

我相信不用说你也知道怎么用了。这个运算符能帮我们生成组合变换,所以我们只需要这样:

let powerTransformer = PowerTransformer(n: 3)
let evenTransformer = EvenTransformer()
let stringTransformer = StringTransformer()

let composedTransformer = powerTransformer ~> evenTransformer ~> stringTransformer

就已经得到了这个链式的运算组合了。

composedTransformer.transform(7)

直接拿来计算就可以了,并且由于运算符没有嵌套,我们可以很轻松地改变变换链的顺序,可以随意增加删除变换。是不是很方便呢?

Wrap Up

当然,函数式编程的优点远不于此,我这里也只是抛砖引玉。今年 WWDC 有个 Session 很不错,Session 419 - Protocol and Value Oriented Programming in UIKit Apps,讲了如何在 UI 编程中用好值类型和协议,当然,只要牵扯到值类型的东西,都是可以和函数式扯上关系的。

总结一下,函数式编程很好,很强大,虽然是个古老的编程思想,但现代化的编程思想也无不在向其靠近,结合新的技术,恰当地使用函数式一定能为你的开发提升很多效率!