原文:Basic building blocks of Functional reactive programming(FRP): IOS
控制你的代码的副作用
每一个庞大的结构都有基础单元 ,它们组合在一起,对整个结构形成意义。例如,砖块、水泥、油漆、混凝土等等构成了建筑物的基础单元。同样,在我们继续讨论函数响应式编程 这个庞大的领域之前,如果我们能理解基础单元 ,使我们创建的庞大的应用程序有意义,那将是非常好的。让我们看一下 FRP 的一些基础单元,从事件流开始。
事件流(Event streams)
事件流可以被定义为随着时间发生的事件序列 ;你可以把它看作是一个异步数组。对事件流的简单描述如下图所示:
如你所见,我们将时间表示在从左到右排列的箭头上,向右移动,事件随时间发生。在时间轴上断断续续画出的彩色气泡(用它们的名字表示)代表事件 。我们可以为整个序列添加一个事件监听器 ,每当事件发生时,我们可以通过做一些事情来响应它;这就是核心思想。
在 Swift 中,我们还有许多其他类型的序列,比如说数组:
假设我们有一个 eventStream
数组:
swift
var eventStream = ["1", "2", "abc", "3", "4", "cdf", "6"]
让我们试着将事件与 eventStream
数组进行比较;数组是空间中的序列,这意味着 eventStream
数组中的所有元素现在都存在于内存中;另一方面,eventStream
不具有这种属性。事件可能会随着时间的推移而发生,你甚至不会知道所有可能发生的元素以及它们将在什么时候发生。
因此,如果我们必须在数组和事件流之间建立联系,那么我们可以断言,如果
["1", "2", "abc", "3", "4", "cdf", "6"]
值是在一段时间内发生的,而不是一开始就存在于内存中,前面的数组将像一个事件流,事件"1"
可能在第一秒发生,事件"2"
可能在第四秒发生,事件"abc"
可能在第十秒发生,以此类推。
在这里,请注意,事件发生的时间和事件的类型都不是事先知道的。事件只是在它们发生时被处理。事件流的好处是,它们有类似于数组的功能。
所以我们说,我们的问题是把给定数组中的所有数字相加。
正如你所看到的,数组中的元素不是数字,它们是字符串,所以我们必须在这里做一些转换,并通过一个循环来过滤掉那些不能转换为数字的字符串,如果它们是一个有效的数字,再把剩下的加上去。我们可能会使用 for
循环在数组上循环,但是当我们在 for
循环里面遍历数组的时候,我们会使用 map
和 filter
操作符来提供问题的解决方案,这就是函数式方法。
步骤1:
swift
let result = eventStream.map {
// inside map we will parse the array
// elements to convert all the integer compatible elements to integers
}
步骤2:从步骤 1 获得的数组中过滤所有不是数字的元素。所以,步骤 1 的语句现在可以这样扩展:
swift
let result = eventStream.map({
// inside map we will ... to integers
}).filter {
// filter all the non integers to form a pure integer array
}
步骤3:将所有的整数值相加,通过对步骤 2 的扩展,得到总和。因此,同样的语句可以写成如下:
swift
let result = eventStream.map({
// inside map we will ... to integers
}).filter {
// filter all the non integers to form a pure integer array
}.reduce(0,+)
所有这些被称为高阶函数 ,在一行中使用点符号扩展函数的过程被称为链式。
我们可以用事件流做同样的事情,唯一的区别是在间歇性的时间间隔内提供事件。因此,对事件流的处理将需要自己的时间,其结果不会像使用数组时那样瞬间填充。
状态(State)
为了准确起见,我们需要更多地了解共享的可变状态。在我们将这一术语与编程概念联系起来之前,让我们试着用通用术语提出一个定义;考虑一个例子。
假设你买了一辆车,在第一天,你启动车,去开车。一切都运行得很好,很顺利。为了启动汽车,你插入钥匙并顺时针旋转;在系统内部,火花塞点燃了受控的燃料流,你的发动机开始工作。你的汽车的这种状态就是启动状态。然后你把档位切换到驾驶模式,用右脚踩下油门。Voila! 汽车开始移动,现在你可以把你的汽车的这种状态称为运行状态,最终你将在目的地停下你的汽车,然后汽车的状态将相应地改变。
所以你注意到,你的行动或输入、发动机点火、活塞运转等等 -- 所有前台或后台的活动和进程的总和 -- 构成了汽车的状态,由于,它可以随着几个后台进程和用户行动而改变,所以它是可变的。
如此多的因素制约着汽车在某一特定时间点的状态,有时很难控制汽车的状态,然后你就会出现故障!那是汽车修理工要担心的事情。
把同样的概念映射到我们建立的应用程序上,每一个应用程序在任何给定的时间实例上都有一个状态。应用程序可能在后台从网络服务中获取数据,在媒体播放器中播放歌曲,响应用户的输入,等等 -- 所有这些动作或进程(同步和异步)在任何给定的时间点都被统称为应用程序的状态,与汽车修理工不同,我们有责任在任何时间点上管理应用程序的状态。
副作用(Side effects)
既然你现在已经意识到了泛型和编程世界中的共享可变状态,你可以把这两件事的大部分问题归结为副作用。
每个函数在其范围内工作,接受一个输入,应用一些逻辑,并产生一个输出;函数也可以没有输入或输出。例如,一个函数打印出一个对象的状态。当系统的状态因为一个函数的执行而发生变化时,就会产生副作用。例如,假设一个名为 addAndStoreValue()
的函数将两个整数相加,并将结果存储在本地数据库中,同时触发一个通知,要求视图刷新以反映结果值,那么视图中的这种变化将是这个函数因其执行而引起的副作用。换句话说,一旦这个函数被执行,应用程序的状态就会发生变化。这种变化被称为副作用。
任何时候你修改存储在磁盘上的数据或更新屏幕上的标签文本,都会引起副作用。
你一定在想,副作用一点也不坏;实际上,这正是我们编写和执行程序的原因。我们希望程序执行后,系统 / 应用程序的状态会发生变化。你不希望你的程序运行后对手机的状态没有带来任何改变吧?谁想要这样的程序呢?想象一下,运行了一段时间的应用程序,却没有给系统的状态带来任何变化,相当无用,啊!?
所以,从目前的讨论中,我们已经明白,引起副作用是我们所希望的,那么问题出在哪里?
副作用的问题是,我们想控制副作用,从而预测一个函数执行完毕后应用程序的状态。我们要控制执行,以可预测的方式引起副作用,并预测我们的应用程序开始运行后设备的状态。我们还需要将我们的应用程序隔离在模块中,以确定哪些代码会改变应用程序的状态,哪些代码会处理和输出数据。
RxSwift 通过利用前面描述的声明式编码 和创建反应式系统来解决前面的问题。我们将在接下来的章节中更深入地研究这些概念。
函数式编程大多避免了副作用;正因为如此,代码变得更加可测试,因此编写健壮的代码变得更加容易。 一个写得很好的应用程序将导致副作用的代码与程序的其他部分区分开来;因此,测试应用程序变得更加容易,扩展功能变得清晰,重构和调试成为一个简单的过程,并且维护这样的代码库也是无忧无虑。RxSwift 在隔离副作用方面发挥了很大的作用,正如预期的那样。
不变性(Immutability)
在我们讨论不变性之前,让我们先来回答一个问题。**哪种数据类型在多线程环境下工作更安全?**可变数据类型,它可以随着时间的推移而改变,或者恒定数据类型,它一旦被填充就不能改变。让我试着用一个例子来解释这个问题 -- 假设你有一个位置坐标的可变字典。
swift
var locationDictionary = ["latitude": 131.93839, "longitude": 32.83838]
locationDictionary
是用户的位置,每五分钟被填充一次;这个 locationDictionary
将在不久的将来的某个时候被转换为一个 location JSON 对象,并同步到后台(一个向服务器推送数据的 API),在那里,位置将被提取并显示在一些 web 视图中,或者它可能被用于任何其他实时位置更新的目的。
想象一下,在多线程环境中使用这个位置字典;由于它是一个变量,locationDictionary
可以在任何时候被任何线程更新,导致错误的位置更新。你可能最终会向你的 API 发送不需要的或损坏的数据。
简单的解决方法:让变量成为常量,你可以放心,一旦值被填充到字典中,它就不会被改变,因此你会得到可靠的结果。
通过前面的讨论,我们现在可以定义什么是不可变的对象和什么是可变的对象:不可变的对象是一个在其存在过程中或范围内不能改变的对象,而可变的对象可以在其范围内改变。
函数式编程提倡不可变的概念;记得这个例子:
swift
**let** numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
**let** numbersLessThanFive = numbers.filter { $0 < 5 }
你是否注意到我们如何将一个新的 numbersLessThanFive
数组作为一个常量 而不是变量来创建?试着用命令式编程创建一个类似的数组;你能获得一个常数吗?让我们试着这样做:
swift
var numbersLessThanFive = [Int]()
for index in 0..< numbers.count {
if numbers[index] < 5 {
numbersLessThanFive.append(numbers[index])
}
}
这个过程不仅费字,而且涉及到程序中的数据流损失,因为在内存中执行的任何其他线程现在可以改变这个数组中的值,因为这个数组现在是一个变量。在任何编程语言中都鼓励使用不可变的数据流,特别是当你试图建立可能产生无数线程的复杂应用程序时。
由于函数反应式编程的一方面包括大量的函数式编程,在用 FRP 方式建立逻辑模型时,与不可变的数据一起工作是很自然的,因此一半的问题从一开始就被解决了。
好吧,这并不像看起来那么简单,但不断的练习会让你变得完美。你可以在我的书中练习并掌握 FRP 的原则和概念。
由于不可变对象在执行过程中不会随着时间的推移而改变,这意味着它们是有代价的,在某些情况下可能无法重用。你必须处理一个不可变的对象,或者在当前对象不适合环境的情况下填充一个新的对象。正如你所看到的,在使用不可变数据类型时有一些缺点,但作为聪明的程序员,我们需要在建立数据类型模型时取得适当的平衡并做出合理的选择。
从前面的图和我们之前的讨论中,我们可以很清楚地看到为什么使用可变状态工作会导致你的程序代码中出现非确定性的行为。如前所述,我们需要在两者之间取得平衡,以维持一个共享的状态来适应手头的问题。更多不可变的数据类型意味着更多的确定性行为。
在下一篇博客中,我将谈论 RxSwift 的基础,在此之前,请继续关注并享受阅读:)
要想扎实掌握反应式概念并用 RxSwift 编写 IOS 应用程序,你可以找到我的《Swift 4 中的反应式编程》一书的链接。
谢谢你的阅读,如果你觉得有用,请分享给大家 :)