对 iOS 开发中的函数式编程的一点探索
iOS 的开发中,运用最广的 OC 语言是典型的面向对象语言,所以很多 iOS 的同学可能更习惯于 OOP 编程模式。
但是随着 Swift 的推出和应用,更加丰富的编程模式开始被讨论。虽然 Swift 被定义为面向协议语言,但是其中仍然有很多函数式思想,在这里我谈一下对 iOS 开发中,函数式编程的一点理解。
如何理解 FP
函数式编程,顾名思义,其思想更接近于数学计算。
函数式编程是一种抽象度很高的编程方式,其中一个很重要的概念就是纯函数,即:对于一个函数来说,只要输入是确定的,那么输入就是确定的,这种函数是没有副作用的。
Swift 中的 FP 思想
函数式编程语言并不多,但是函数式编程思想却运用的很广泛。这里列出几个 Swift 中比较常用的数组变换操作函数。
比如在 Swift 中操作数组,这里有一个整型数组:
1 | var array = [1, 2, 3, 4, 5] |
首先实现数据各元素 +1,可能写起来是这个样子的:
1 | func increment(array: [Int]) -> [Int] { |
那如果又想实现数据各元素 *2 呢,可能又会有这样一份代码:
1 | func double(array: [Int]) -> [Int] { |
上述两份代码相似度已经达到不能容忍的状态,那么我们可以考虑将共同的提取出来,将差异从外界接收,也就是将对数组的转换操作,作为一个参数传入:
1 | func compute(array: [Int], transform:(Int) -> (Int)) -> [Int] { |
这样我们就可以接收所有对整型数组的操作,并且返回对应的 result 了。
但是这样的局限性仍然很大,只能处理整型数组,为了更好的兼容其他类型,在这个基础上,可以引入泛型:
1 | func genericCompute<Element, T>(array: [Element], transform:(Element) -> T) -> [T] { |
至此基本上可以处理各种类型的数组,并且可以得到不同类型数组的返回值。事实上,Swift 中也确实是这样做的,Array 有对应的 map 函数,可以实现上述功能。而这种功能就是纯数组变换的,不参杂对外界对任何影响,只要输入确定,输出也一定确定。
同样的,Swift 中也提供了其他 Array 的变换函数。
filter 函数可以用来做过滤操作:
1 | // filter 的大概实现: |
而针对上述操作,可以把它们再归纳为:给定一个初始值以及变换函数,遍历更新结果的一个过程。因此,将 赋给 result 变量的初始值,和用于在每一次循环中更新 result 的函数 进行抽象,便可得到 reduce 函数:
1 | // reduce 的大概实现: |
Swift 中对于以上几种函数已经封装好,我们可以随便调用对 Array 做想要的变换操作了。比起专门抽出一段来处理 Array,这种直接传入函数的调用方式更佳简洁,也更容易理解。
经典 OC 的 FRP 开源库:RAC 中的 FP 思想
iOS 中更多接触的是面向对象编程,但经典开源库 ReactiveCocoa 的 OC 版本中展示了一套在 OC 中运用的响应式函数式编程模式。我们通常对 RAC 的响应式讨论的比较多, 而 RAC 中的核心函数 bind,是函数式编程中 Monad 概念的一种运用。
关于函数式编程中的 Functor、Applicative 和 Monad 概念这里不再展开,想了解的可以看一下这篇博客。
写一段简单的测试代码:
1 | void bindForTest() { |
调用这个函数,运行可以看到控制台输出:
1 | 2018-07-30 13:52:52.686901+0800 FPTest[9654:689258] subscribe value = 2 |
RAC 的调用方式这里不展开,可以看到 bind 函数实现的结果同样是对一系列的值进行了变换,只不过这里的上下文可以理解为 RACSignal,我们可以才想到 RAC 的内部的 bind 函数实现方式,是将一个信号的每个值依次进行变换之后再输出,事实上也确实如此。
OC 中的日常应用
同样的,上述 Swift 的 Extention 中实现的功能,在 OC 中同样有场景需要,我们可以通过 block 的形式将变换函数传递进来,以实现类似的功能,比如 map 可以如下实现:
1 | + (NSArray *)map:(NSArray *)array block:(id(^)(id obj, NSInteger idx))block { |
当然也可以放入 NSArray 的分类中,这样调用起来更加方便。
小结
如上所述,函数式编程思想实际上随处可见,在实际的开发中,我们应当在合适的场景下分别应用,争取可以以更轻巧简洁的方式解决问题。