为什么要函数式编程

15 阅读6分钟

函数式编程(FP)绝不是一个新概念。它几乎贯穿了编程的整个历史。不过,我不确定这样说是否公平,但是......直到近几年,它才似乎在整个开发者界成为主流概念。我认为FP更多是学术领域的领域。

不过,这一切都正在改变。围绕FP的兴趣浪潮正在不断增长,不仅在语言层面,甚至在库和框架中也如此。你很可能正在阅读这篇文章,因为你终于意识到FP是你无法再忽视的东西。或者你和我一样,尝试过很多次学习函数式,但很难理解所有术语或数学符号。

本章旨在回答诸如“为什么我应该在代码中使用函数式制风格?”以及“函数式轻量JavaScript与其他人对函数式描述相比如何?”等问题。在打好基础后,接下来的书中我们将逐步揭示用功能-轻量化风格写JS的技巧和模式。

At a Glance

var numbers = [4,10,0,27,42,17,15,-6,58];
var faves = [];
var magicNumber = 0;

pickFavoriteNumbers();
calculateMagicNumber();
outputMsg();                // The magic number is: 42

// ***************

function calculateMagicNumber() {
    for (let fave of faves) {
        magicNumber = magicNumber + fave;
    }
}

function pickFavoriteNumbers() {
    for (let num of numbers) {
        if (num >= 10 && num <= 20) {
            faves.push( num );
        }
    }
}

function outputMsg() {
    var msg = `The magic number is: ${magicNumber}`;
    console.log( msg );
}

现在,我们来考虑一种截然不同的风格,它能达到完全相同的效果:

var sumOnlyFavorites = FP.compose( [
    FP.filterReducer( FP.gte( 10 ) ),
    FP.filterReducer( FP.lte( 20 ) )
] )( sum );

var printMagicNumber = FP.pipe( [
    FP.reduce( sumOnlyFavorites, 0 ),
    constructMsg,
    console.log
] );

var numbers = [4,10,0,27,42,17,15,-6,58];

printMagicNumber( numbers );        // The magic number is: 42

// ***************

function sum(x,y) { return x + y; }
function constructMsg(v) { return `The magic number is: ${v}`; }

一旦你理解了FP和函数式编程,你很可能会这样阅读和在脑海中处理第二段内容:

我们首先创建一个叫 sumOnlyFavorites(...)的函数,它是三个函数的组合。我们结合了两个过滤器,一个检查值是否大于10,另一个检查小于20。然后我们将和sum(...)这个 reducer(迭代器) 加入到 transducer(转换器) 里面。由此产生的 sumOnlyFavorites(...) 函数是一个新的 reducer(迭代器):它会筛查出同时符合这两个过滤条件的值,并将该值加到 accumulator(累加器)的值中。

然后我们创建另一个函数 printMagicNumber(...),它首先用刚定义的 sumOnlyFavorites(...) reducer(迭代器)对数字列表进行 reduce(迭代)操作,得到只包含筛选条件的数字之和。然后 printMagicNumber(...) 将最终求和的值传递给 constructMsg(...),由它生成一个字符串值,最终这个字符串被进入 console.log(...)进行输出。

这些不断运动转换的代码片段所传达的思路,对于一些开发者来说可能显得比较陌生,比较难懂,真的有必要写这样的代码吗?但对于对代码质量有较高要求的人来说,尤其是 github 上那些已经开源的优秀的代码仓库,要写出高质量、优雅、简洁的代码,函数式编程(FP)是非常必要的。我今天所做的,就是提供一种思路,让函数式编程变得清晰、易懂。我今天的分享是为了追求一种务实的平衡,希望在以后的开发中,保证交付质量的同时,代码清晰、简洁、轻量化。

代码的信心

我有一个非常简单的信念,它几乎贯穿了我开发时所做的一切:你无法信任的代码,就是你不能理解的代码。反过来说也成立:你不能理解的代码,就是你无法信任的代码。大家平时经常调侃这样的段子,“这垃圾代码是谁写的?”一看提交日期,哦,原来是一个月前自己写的。我们有时候会看不懂自己过去写的代码,往往是因为我们往往只是在依赖执行代码,但实际上写代码的思路不够清晰,对代码预期能做什么没有一个确定的把握。

一个理解函数式编程的开发,所写的代码往往 bug 更少,即便测出 bug,这种 bug 通常也很容易排查。

为什么函数式编程很重要

我在github上看到这样一篇文章,说全球开发者大约有70%的时间用在了对过去代码的阅读和理解上,这其中的原因是代码的可读性不高造成的。而影响代码可读性最大的一个因素就是代码的熟悉度。什么是代码熟悉度?比如我们在读别人写的代码,一看到map(...),无论map语法出现在哪一个函数里, 我们立刻就能明白它的意图,但是当我看for 循环的时候,我必须从头读到尾,才能搞清楚大到底在干什么。

比如,

function addTax(prices) {
  const result = [];

  for (let i = 0; i < prices.length; i++) {
    const price = prices[i];
    const taxed = price * 1.1;
    result.push(taxed);
  }

  return result;
}

我们没办法一下就get到这个函数到底在讲什么,我们看每一次循环i的变化,看每一次循环的取值,看中间做了哪些计算,以及最后计算的结果push到了哪里。但如果我把它翻译为map(...),几乎就是秒懂。

function addTax(prices) {
  return prices.map(price => price * 1.1);
}

再举一个对比更加明显的例子,

let total = 0;

for (let i = 0; i < orders.length; i++) {
  if (orders[i].paid) {
    total += orders[i].price;
  }
}

我们必须完整读完,知道每一个循环的执行细节,这个函数的功能是对已支付的订单进行求和。

const total = orders
  .filter(o => o.paid)
  .map(o => o.price)
  .reduce((sum, p) => sum + p, 0);

这段代码我们不需要细读,filter、map以及reduce就已经把代码语义化了,我们顺着它的链式调用就能明白它的意图:先筛选支付订单,然后取价格,最后对价格进行求和。其实,我们平时开发常用的这些ES6自带的语法,就是函数式编程最好的体现。

今天这次分享的一个目标就包括,让我们的代码更直观、更容易理解。写出高质量代码本身就是一件心旷神怡、很有成就感的事情,如果我们的代码充满这种一眼就能被识别意图的结构,我们就能把阅读、调试屎山代码的时间节省下来,用来思考更高层次的逻辑。好的代码就像高速公路,路标清晰、路径明确,不需要频繁刹车来确认方向。函数式编程的目的,不是故意要把代码写得很高级,而是让我们远离低级的脑力消耗,把时间用在真正重要的问题上。

可读性

学习函数式编程会经历这样一个过程,我的初衷是让自己的代码更加简洁、可读性更高,但是在我变得更好之前,我好像变得更糟了,这是很多人都会经历的感受。但是如果坚持下去,我们代码可读性的曲线就会逐渐回升。

image.png