浅谈 Swift 的函数式编程

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<T : Strideable> : 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<T : IntegerType> : Transformer {
    func transform(elem: T) -> T {
        return elem % 2 == 0 ? elem : elem + 1
    }
}

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

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

struct ComposedTransformer<T : Transformer, U : Transformer where T.OutputType == U.InputType> : 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 ~><T : Transformer, U : Transformer where T.OutputType == U.InputType>(t1: T, t2: U) -> ComposedTransformer<T, U> {
    return ComposedTransformer(transformer1: t1, transformer2: t2)
}

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

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

let composedTransformer = powerTransformer ~> evenTransformer ~> stringTransformer

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

composedTransformer.transform(7)

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

Wrap Up

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容