漫谈函数式编程在前后端开发中的应用

百家 作者:聊聊架构 2018-06-19 02:32:21

最近在思考一个问题,函数式编程对于我们的软件开发的意义到底有多大?到底值不值得我们花时间去学习。因此,写下这篇文章来记录自己的思考。文章包含了前后端开发中的一些内容,大家各自选择阅读。

首先还是简单说下函数式编程是什么?

它详细的解释可以参考维基百科。缘起数学家 Alonzo Church 提出了 Lambda 演算的概念,可以用函数组合的方式来描述计算过程,换句话来说,如果一个问题能够用一系列函数组合的算法来表达,那么这个问题就认为是可计算的。

它和面向对象编程一样,也是一种编程范式。强调执行的过程而非结果,通过一系列的嵌套的函数调用,完成一个运算过程。它主要有以下几个特点:

  1. 函数是"一等公民":函数优先,和其他数据类型一样。

  2. 只用"表达式",不用"语句":通过表达式(expression)计算过程得到一个返回值,而不是通过一个语句(statement)修改某一个状态。

  3. 无副作用:不污染变量,同一个输入永远得到同一个数据。

  4. 不可变性:前面一提到,不修改变量,返回一个新的值。

函数式编程的概念其实出来也已经好几十年了,我们能在很多编程语言身上看到它的身影。比如比较纯粹的 Haskell,以及一些语言开始逐渐成为多范式编程语言,比如 Swift,还有 Kotlin,Java,Js 等都开始具备函数式编程的特性。这么多语言开始逐渐有了支持,函数式编程对于我们的生活到底能够带来一些什么好处呢。

为了讨论这个问题,还是举一些在不同场景下的应用来看看吧。

函数式编程在前端开发中的应用

提到现代前端开发,那么 React 肯定是逃不开的一个话题。在 React 技术栈中,函数式编程有哪些体现呢?

Stateless components

在 React 0.14 之后推出的,先来看一段代码

function MyComponent(props) {
  return 
My props name {props.name}
}

这是一个简单的无状态组件,我们没有用createClass或者是extends React.Component来创建一个组件,而是通过一个 Pure function 返回了一个组件。

那么这里的好处是什么呢?

  1. 简洁,一眼可以看出这个组件的作用;

  2. 无副作用,只要传入同一个 props 那么 render 出来的组件一定是相同的;

  3. 测试更友好;

  4. 没有this,要知道this还是难倒了好多英雄好汉的;

  5. 更容易实现 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 架构有很多相似的地方,后面再谈。这里简单提一下,最终的结果可以通过一系列的信息组合得来,这是一个很重要的改变。

函数式编程在后端开发中的应用
Lambda 架构

Lambda 是将计算施加于大量数据的一种通用方法。

传统的数据系统擅长业务的处理,但是在面临数据分析、生成报表等任务的时候就显得很乏力了。

当然,Lambda 架构有很多的优点,这里只重点讨论下在函数式编程方面的一些体现。

这儿要提到的一个词就是MapReduce,先贴一个图。

架构中运用函数式编程的方式将问题进行抽象,拆分成一个映射操作(Map) 和一个化简(Reduce)操作,这有什么好处呢?

首先,函数式编程无副作用的特点天生对并行编程提供了良好的支持。在使用多线程的方案的时候,我们常常会遇到共享状态的问题,因此我们可能会采用各种各样的锁机制。一旦引入了锁,那么代码本身的复杂度也就增加了。而当我们采用函数式编程的时候,不用太担心这样的问题,函数操作无副作用,不用担心共享数据带来的问题。

其次,对数据分析更加友好。在 Lambda 架构中,通常会把数据分为两种类型,原始数据和衍生数据。那什么叫原始数据呢,比如咱们看到的维基百科的页面,是经过了很多次编辑的。那么每次编辑这个操作就叫做原始数据,这个动作是不可改的,一旦形成后就记录在那里了。那么最后我们看到的页面就是经过这些原始数据 Reduce 操作后得到的衍生数据。

数据不可变性很大程度上降低了数据库的复杂度,同时提供了对并行计算的良好支持。当然,Lambda 架构也不是说没有缺点的,比如批处理层过慢,同时维护多个视图等等,这我了解不深也不是本文的范围就不聊了。

这里我们在回过头来看前面写到的 Redux 的设计会发现有很多异曲同工之妙,每个 Action 出发的操作都可以看做是不可改的原始数据,通过 reducer 纯函数得到的结果始终是稳定的,最终的结果就是将多个 reducer 的结果组合起来。

函数式编程和函数响应式编程

可能很多做移动端开发的朋友会更多的听到这个概念,比如 iOS 开发上最早有 ReactiveCocoa,后来又有了 RxSwift,安卓上也有常见的 RxJava 等等。这里以 ReactiveX(Rx)来说,最早由微软的架构师 Erik Meijer 领导的团队开发,目前各种版本几乎覆盖率主流的编程语言。

那么 Rx 和函数式编程的关系是什么呢?

我的理解是,Rx 是一种以函数式编程为基础之一的编程模型,引入了流的概念,以一种统一的方式处理异步事件的机制。贴一张官方的图来看看:

在 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 的时候可能会稍微麻烦一点,不过这一点可以通过较为完备的测试来解决。

那么到底会不会让我们生活变得更好

写了很多例子,我们回过头来再讨论一下函数式编程到底对于我们编码来说意义有多大。

就我个人来说的话,它会大大增加我的生产力。但诚然函数式编程存在很多的优点,但是也并不是一招鲜吃遍天的。

我觉得比较麻烦的一个点就是它的学习成本相对来说要高一点,其实最主要的是思维的转变。

函数式编程的抽象程度更高,和大家从学校里就开始接触的编码方式截然不同。所以当团队平均水平不是那么高的时候,这一点确实可能会成为我们做技术选型要考量的一个关键因素。不过我倒是认为软件开发者都应该去学一学函数式编程。就拿抽象这个事情来说,软件开发上有一句所谓的“名言”:“没有什么问题是不能通过抽象来解决的,如果有,那就再加一层抽象”。

在函数式编程中,我们将一个个复杂的问题抽象成一个个过程的表达,然后再将不同的过程结果组合起来,更加容易找到问题的解决办法。对于我们在其他领域的编码也是一样的道理,剥离问题表面,还原问题本质。有了这样的思维的时候,当你和别人在看同一个问题的时候,你会更容易有一种拨云见日的感觉。

除了抽象的能力,分解问题的能力也是很重要的一个启发。将问题化小,分而治之,然后组合结果。当然这个能力不仅仅是函数式编程才具备的,分治法在很多算法里已经体现得淋漓尽致了。不过这里还是想再提一下这个话题,分而治之可以在各个维度的工作上进行运用,小到一个算法的具体实现,然后到一个问题的过程分解,甚至大到一个工作任务的拆解,都可以用分而治之的思维去寻找解题之法。

当然函数式编程还有其他的一些不足之处,比如有人会说在函数式编程中数据复制可能会比较严重,可能会造成性能问题。这个问题我是这样看的,局部来说他可能确实看起来会存在一定的影响。但是从另一个角度来说,在我们使用函数式编程的时候,不用担心全局变量被破坏,没有执行顺序的依赖。我们在并行编程的时候,也不需要依赖于过多的锁的,那么反而最终可以提升最终性能。

最后的话

写到这里,稍微感觉写得有点发散了。但是贯穿全文的其实也都是围绕函数式编程的一些特性:抽象、组合、不可变等等。

作为软件行业的从业者,即使现在不懂它没有关系,但是千万别因为别人的危言耸听而不敢去尝试。记得以前公司里有人要讨论 TDD 到底好不好这个问题,其中一方直接怼回去“Talk is cheap, show me the code",只有当你真正去尝试了,你才有资格去讨论它到底好不好。

这里我要借用下马云大大的曾经说的商机来临要经历的四个阶段了,“看不见“、“看不起“、“看不懂”、“来不及”,对于任何一门新技术来说也是这样(何况,函数式编程并不是什么新技术)。面对一门技术,勇敢地去试一试,然后相信你会有自己的判断。

关于作者:

花名无同,工作于优易数据,从事敏捷转型以及技术研发相关工作。公司长期招聘各种技术达人,前后端开发,如果有兴趣加入我们,欢迎发送简历给我 719754914@qq.com。

参考资料:

  1. https://en.wikipedia.org/wiki/Functional_programming

  2. https://reactjs.org/docs/higher-order-components.html

  3. http://reactivex.io/

  4. https://zh.wikipedia.org/wiki/MapReduce

  5. 图书《七周七并发模型》


当前分布式系统是大势所趋,Google、Facebook 等大型系统架构也是以分布式系统架构为基础。过去二十年,整个分布式系统架构演进史是从 C/S→B/S→分布式系统→网格计算→云计算,包括目标、定位、场景,影响深远。未来,如何从全球多域的角度去规划分布式架构呢?

下个月深圳 ArchSummit 架构师峰会邀请了 Pinterest 武永胜、百度王耀、菜鸟网络黄浩、美团宋斌来分享他们的一手分布式系统设计经验,这些内容一定可以助你一臂之力。

关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接