函数式编程漫谈

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

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

它详细的解释可以参考维基百科。缘起数学家Alonzo Church提出了Lambda演算的概念,可以用函数组合的方式来描述计算过程,换句话来说,如果一个问题能够用一系列函数组合的算法来表达,那么这个问题就认为是可计算的。
它和面向对象编程一样,也是一种编程范式。强调执行的过程而非结果,通过一系列的嵌套的函数调用,完成一个运算过程。
它主要有以下几个特点:

  1. 函数是"一等公民":函数优先,和其他数据类型一样。
  2. 只用"表达式",不用"语句":通过表达式(expression)计算过程得到一个返回值,而不是通过一个语句(statement)修改某一个状态。
  3. 无副作用:不污染变量,同一个输入永远得到同一个数据。
  4. 不可变性:前面一提到,不修改变量,返回一个新的值。

函数式编程的概念其实出来也已经好几十年了,我们能在很多编程语言身上看到它的身影。比如比较纯粹的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返回了一个组件。
那么这里的好处是什么呢?

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

FP在后端开发中的应用

  • Lambda架构

Lambda是将计算施加于大量数据的一种通用方法。
传统的数据系统擅长业务的处理,但是在面临数据分析、生成报表等任务的时候就显得很乏力了。
当然,Lambda架构有很多的优点,这里只重点讨论下在函数式编程方面的一些体现。
这儿要提到的一个词就是MapReduce,先贴一个图。

MapReduce.jpg

架构中运用函数式编程的方式将问题进行抽象,拆分成一个映射操作(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并不是什么新技术)。面对一门技术,勇敢地去试一试,然后相信你会有自己的判断。

我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2mhtssrg4f8k0

参考资料:

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

推荐阅读更多精彩内容