最近在思考一个问题,函数式编程对于我们的软件开发的意义到底有多大?到底值不值得我们花时间去学习。因此,写下这篇文章来记录自己的思考。文章包含了前后端开发中的一些内容,大家各自选择阅读。
首先还是简单说下函数式编程是什么?
它详细的解释可以参考维基百科。缘起数学家Alonzo Church提出了Lambda演算的概念,可以用函数组合的方式来描述计算过程,换句话来说,如果一个问题能够用一系列函数组合的算法来表达,那么这个问题就认为是可计算的。
它和面向对象编程一样,也是一种编程范式。强调执行的过程而非结果,通过一系列的嵌套的函数调用,完成一个运算过程。
它主要有以下几个特点:
- 函数是"一等公民":函数优先,和其他数据类型一样。
- 只用"表达式",不用"语句":通过表达式(expression)计算过程得到一个返回值,而不是通过一个语句(statement)修改某一个状态。
- 无副作用:不污染变量,同一个输入永远得到同一个数据。
- 不可变性:前面一提到,不修改变量,返回一个新的值。
函数式编程的概念其实出来也已经好几十年了,我们能在很多编程语言身上看到它的身影。比如比较纯粹的Haskell,以及一些语言开始逐渐成为多范式编程语言,比如Swift,还有Kotlin,Java,Js等都开始具备函数式编程的特性。这么多语言开始逐渐有了支持,FP对于我们的生活到底能够带来一些什么好处呢。
为了讨论这个问题,还是举一些在不同场景下的应用来看看吧。
FP在前端开发中的应用场景
提到现代前端开发,那么React肯定是逃不开的一个话题。在React技术栈中,FP有哪些体现呢?
-
Stateless components
在React 0.14之后推出的,先来看一段代码
function MyComponent(props) {
return <div>My props name {props.name}</div>
}
这是一个简单的无状态组件,我们没有用createClass
或者是extends React.Component
来创建一个组件,而是通过一个Pure function返回了一个组件。
那么这里的好处是什么呢?
- 简洁,一眼可以看出这个组件的作用;
- 无副作用,只要传入同一个props那么render出来的组件一定是相同的;
- 测试更友好;
- 没有
this
,要知道this
还是难倒了好多英雄好汉的; - 更容易实现SSR(这一点我并未考证,有知道的朋友可以补充)。
当然,使用Stateless component
并不是万能的,可以很明显的看到没有了React的生命周期,这个问题通常我们会结合HOC来解决。你看这一点就印证了前面说的,通过函数的组合完成对结果的表达,是不是很有意思。
-
Redux
在前端应用越来越复杂的今天,数据流管理是一件很重要的事,Redux就是来解决这个问题的。它是Flux架构的演化实现,官方Github解释为Predictable state container(可预测状态机)。
在Redux中我们存在一个单的树形结构的state,单一数据源降低了多数据源的信任问题。State是通过每个reducer的结果组合而来的,每个reducer都是一个Pure function,如下:
export const isLoading = (state = false, action) => {
switch (action.type) {
case MarketActionsTypes.FETCH_MARKET_DATA_START: {
return false;
}
default:
return false;
}
};
在reducer中不会直接修改每个state中的状态,而是返回一个新的状态,然后整个state的结果通过一个个reducer的结果归纳出来。我们来看reducer源码是怎么工作的:
....
// from https://github.com/reduxjs/redux/blob/b02310b359a0832f65873d024570d411b465ced9/src/combineReducers.js#L162
let hasChanged = false
const nextState = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
....
我们可以很直观的看到,最后的state结果是通过将每个reducer生成的局部结果组合起来得到一个新的nextState,而不是直接在原有的state上进行修改。
所以,我们再回过头来看看它的定义——可预测状态机。
每个reducer都是一个纯函数,只要输入恒定,那么输出肯定是恒定的。同时,无副作用的特性可以保证state不会被意外修改,那么整个应用的state都是可以准确的知道的。
当你明白redux是怎么工作之后,你可能会发现自己都可以写一个dev-tool出来了。
说到这里,咱们会发现这和后面要提到的Lambda架构有很多相似的地方,后面再谈。这里简单提一下,最终的结果可以通过一系列的信息组合得来,这是一个很重要的改变。
FP在后端开发中的应用
-
Lambda架构
Lambda是将计算施加于大量数据的一种通用方法。
传统的数据系统擅长业务的处理,但是在面临数据分析、生成报表等任务的时候就显得很乏力了。
当然,Lambda架构有很多的优点,这里只重点讨论下在函数式编程方面的一些体现。
这儿要提到的一个词就是MapReduce
,先贴一个图。
架构中运用函数式编程的方式将问题进行抽象,拆分成一个映射操作(Map)和一个化简(Reduce)操作,这有什么好处呢?
首先,函数式编程无副作用的特点天生对并行编程提供了良好的支持。在使用多线程的方案的时候,我们常常会遇到共享状态的问题,因此我们可能会采用各种各样的锁机制。一旦引入了锁,那么代码本身的复杂度也就增加了。而当我们采用FP的时候,不用太担心这样的问题,函数操作无副作用,不用担心共享数据带来的问题。
其次,对数据分析更加友好。在Lambda架构中,通常会把数据分为两种类型,原始数据 和衍生数据。那什么叫原始数据呢,比如咱们看到的维基百科的页面,是经过了很多次编辑的。那么每次编辑这个操作就叫做原始数据,这个动作是不可改的,一旦形成后就记录在那里了。那么最后我们看到的页面就是经过这些原始数据Reduce操作后得到的衍生数据。
数据不可变性很大程度上降低了数据库的复杂度,同时提供了对并行计算的良好支持。当然,Lambda架构也不是说没有缺点的,比如批处理层过慢,同时维护多个视图等等,这我了解不深也不是本文的范围就不聊了。
这里我们在回过头来看前面写到的Redux的设计会发现有很多异曲同工之妙,每个Action出发的操作都可以看做是不可改的原始数据,通过reducer纯函数得到的结果始终是稳定的,最终的结果就是将多个reducer的结果组合起来。
函数式编程和函数响应式编程
可能很多做移动端开发的朋友会更多的听到这个概念,比如iOS开发上最早有ReactiveCocoa,后来又有了RxSwift,安卓上也有常见的RxJava等等。这里以ReactiveX(Rx)来说,最早由微软的架构师Erik Meijer领导的团队开发,目前各种版本几乎覆盖率主流的编程语言。
那么Rx和函数式编程的关系是什么呢?
我的理解是,Rx是一种以函数式编程为基础之一的编程模型,引入了流的概念,以一种统一的方式处理异步事件的机制。贴一张官方的图来看看:
rxjsintro.gif
在Rx的世界中,所有的异步事件都可以用一个Observable数据流来表示,数据流里的型号可能是一次网络请求,可能是一次点击操作。当数据到来的时候,我们可以通过一个个函数的组合对事件进行处理,最终产生一个结果。如下的一个例子,将一个搜索框searchBar的输入事件变为一个数据流,然后可以对这个数据流上组合任何的操作。
let searchResults = searchBar.rx
.text
.orEmpty
.throttle(0.3, scheduler: MainScheduler.instance)
.distinctUntilChanged()
.flatMapLatest { query -> Observable<[Repository]> in
if query.isEmpty {
return .just([])
}
return searchGitHub(query)
.catchErrorJustReturn([])
}
.observeOn(MainScheduler.instance)
有了函数式编程的加持,我们可以很清晰的理解这段代码的意思:将输入框的文本变化转为数据流->过滤空字符串->控制数据流产生的最小时间时间价格->内容没有变化时信号不会继续传播->然后对最近的信号映射为一次API请求,然后在主队列上进行观测信号。整个代码的行为都比用命令式编程来的清晰直观。如果是用传统的命令式编程的方式来写这段代码的话,光代码量可能就得膨胀好几倍,更不用提带来的复杂度的问题了。当然,这里有一个问题就是在debug的时候可能会稍微麻烦一点,不过这一点可以通过较为完备的测试来解决。
那么到底会不会让我们生活变得更好
写了很多例子,我们回过头来再讨论一下函数式编程到底对于我们编码来说意义有多大。
就我个人来说的话,它会大大增加我的生产力。但诚然函数式编程存在很多的优点,但是也并不是一招鲜吃遍天的。
我觉得比较麻烦的一个点就是它的学习成本相对来说要高一点,其实最主要的是思维的转变。
FP的抽象程度更高,和大家从学校里就开始接触的编码方式截然不同。所以当团队平均水平不是那么高的时候,这一点确实可能会成为我们做技术选型要考量的一个关键因素。不过我倒是认为软件开发者都应该去学一学函数式编程。就拿抽象这个事情来说,软件开发上有一句所谓的“名言”:“没有什么问题是不能通过抽象来解决的,如果有,那就再加一层抽象”。在函数式编程中,我们将一个个复杂的问题抽象成一个个过程的表达,然后再将不同的过程结果组合起来,更加容易找到问题的解决办法。对于我们在其他领域的编码也是一样的道理,剥离问题表面,还原问题本质。有了这样的思维的时候,当你和别人在看同一个问题的时候,你会更容易有一种拨云见日的感觉。
除了抽象的能力,分解问题的能力也是很重要的一个启发。将问题化小,分而治之,然后组合结果。当然这个能力不仅仅是FP才具备的,分治法在很多算法里已经体现得淋漓尽致了。不过这里还是想再提一下这个话题,分而治之可以在各个维度的工作上进行运用,小到一个算法的具体实现,然后到一个问题的过程分解,甚至大到一个工作任务的拆解,都可以用分而治之的思维去寻找解题之法。
当然FP还有其他的一些不足之处,比如有人会说在FP中数据复制可能会比较严重,可能会造成性能问题。这个问题我是这样看的,局部来说他可能确实看起来会存在一定的影响。但是从另一个角度来说,在我们使用FP的时候,不用担心全局变量被破坏,没有执行顺序的依赖。我们在并行编程的时候,也不需要依赖于过多的锁的,那么反而最终可以提升最终性能。
最后的话
写到这里,稍微感觉写得有点发散了。但是贯穿全文的其实也都是围绕函数式编程的一些特性:抽象、组合、不可变等等。
作为软件行业的从业者,即使现在不懂它没有关系,但是千万别因为别人的危言耸听而不敢去尝试。记得以前公司里有人要讨论TDD到底好不好这个问题,其中一方直接怼回去“Talk is cheap, show me the code",只有当你真正去尝试了,你才有资格去讨论它到底好不好。这里我要借用下马云大大的曾经说的商机来临要经历的四个阶段了,“看不见“、“看不起“、“看不懂”、“来不及”,对于任何一门新技术来说也是这样(何况,FP并不是什么新技术)。面对一门技术,勇敢地去试一试,然后相信你会有自己的判断。
参考资料:
网友评论