函数式编程 action in D2

727 阅读5分钟

前言

大家好,我是秦粤。阿里巴巴自从2019年加入Ecma后,我们就在Ecma中积极参与推进,决定JavaScript语言发展未来的各种议题,我们阿里巴巴的工效议题Error Cause成功的推进到Stage 2。今年在阿里内部由前端委员会牵头也成立了阿里巴巴前端标准化小组,用组织架构保障阿里前端,持续在影响前端生态未来的标准规范上发力。

同时我们也特别关注到TC39成员中,不少成员都在关注和推进JavaScript中函数式编程的体验。作为今年D2语言框架会场的出品人,因此我们邀请了RxJS的团队负责人Ben,引入JavaScript函数式编程的议题。另外,今年Ecma组织的大事件ES2020已经发布,我们也将继续邀请TC39的成员Ujjwal,讲解其中有哪些特性值得我们前端同学关注以及这些议题的诞生过程。

函数式编程 in JavaScript

函数式编程的思想从1930年提出来已经过去很久了,但大多数工程师使用的场景并不多。函数式编程在JavaScript中几乎是天然就支持的,Brendan Eich参考的语言中包括了函数式编程语言Lisp,因此JavaScript有了函数是一等公民和闭包的概念。

我们大学学习的离散数学,线性代数等等课程,都很容易让我们习惯面向指令编程。我们的代码习惯性都是从上到下,从左到右,加上控制指令block的方式。

ES5之前,如果我们学习了面向原型,会在我们的代码中采用prototype。

ES6和TS后,如果我们学习了面向对象,代码中又会加上Class抽象。

但某种程度而言,这些代码方式都是面向机器友好的。函数式编程是否可以带来一些改变呢?

我自己使用函数式编程的感受是,在面对每个要开发的功能,就像在解数学题。首要是思考数据加工过程如何拆解,初期要不停查工具,看看有哪些函数方法可以给我使用。

后期熟练后,会开始用函数式注释,毕竟长时间不看自己原先的代码,同样会忘记。而且不会采用一逗(句号)到底的方式,分段一下。

函数式编程的基础概念,我在此就不啰嗦了。它们是:范畴论,函数一等公民,纯函数,组合,柯里化,等等。感兴趣的同学可以学习一下:mostly-adequate.gitbooks.io/mostly-adeq…

举个栗子

"show me the code",我们来看看具体例子:调用后台API返回了一个数组,其中我们需要对商品的金额进行汇率运算,转换成用户本地的汇率显示。

{
    success: true,
  data: {
    'name': 'ProductsDetail',
    'list': [
      {
        name:'T shirt',
        price:'10.00',
        unit:'$'
      },
      {
        name:'T shirt with logo',
        price:'15.00',
        unit:'$'
      },
      {
        name:'T shirt with brand logo',
        price:'20.00',
        unit:'$'
      },
    ],
  },
}
{
  userCurrency: '¥',
  userCurrencyRate: '6.5',
}

通常,我们会直接写一个循环,或者用UnderscoreJS这样的库将price获取到转化成数字,然后乘以userCurrencyRate。

我们不妨看看函数式的做法,先从简单的函数拆解来看。

例如我们乘法函数,我们通常会这样写:

const multiply = function(x,y) {
    return x * y;
}
multiply(3, 5);// 15

而当我们想复用乘法函数,例如固定乘3的函数。我们可以采用JS高级编程的柯里化Currying。

先复习一下柯里化Currying:一个函数将返回一个新的函数,直到接收所有参数。我们用Vanilla JS可以写成:

const multiply = function(x) {
  return function(y) {
    return x * y;
  }
}
const multiply3 = multiply(3);
multiply3(5);// 15

不过这样还有一些不方便,Curry函数这些的写法,我们想用multiply函数multiply(3,5)又不可以了。JS高级编程常见的面试题之一,就是autoCurry,顾名思义根据参数判断函数如何返回。

有兴趣的同学可以自己挑战一下实现autoCurry函数,这里我们就直接采用RamdaJS的curry方法。

const R = require('ramda');

const multiply = function(x, y) {
  return x * y;
};
const multiplyAutoCurry = R.curry(multiply);

multiplyAutoCurry(3)(5);// 15
multiplyAutoCurry(3, 5);// 15

有了Curry函数,对于数组我们可以使用map函数,将函数应用到每一项。

const R = require('ramda');

const multiply = function (x, y) {
  return x * y;
};
const multiplyAutoCurry = R.curry(multiply);
const multiply3 = multiplyAutoCurry(3);
R.map(multiply3, [1, 2, 3]);// [3, 6, 9]

因此函数式编程的版本实现,我们就可以写成:

const R = require('ramda');

const multiply = function (x, y) {
  return x * y;
};
const multiplyAutoCurry = R.curry(multiply);
const multiplyCurrency = multiplyAutoCurry(6.5);
const parseIntCurry = R.curry(parseInt);
const parseInt10 = parseIntCurry(R.__, 10);
const currencyConvert = R.compose(multiplyCurrency, parseInt10);

const result = {
  success: true,
  data: {
    name: 'ProductsDetail',
    list: [
      {
        name: 'T shirt',
        price: '10.00',
        unit: '$'
      },
      {
        name: 'T shirt with logo',
        price: '15.00',
        unit: '$'
      },
      {
        name: 'T shirt with brand logo',
        price: '20.00',
        unit: '$'
      },
    ],
  },
};
const priceArray = R.pluck('price', R.prop('list', R.prop('data', result)));// ['10.00', '15.00', '20.00']
R.map(currencyConvert, priceArray);// [ 65, 97.5, 130 ]

由于API返回的结果中price是字符串,为了消除不确定性,我们需要先将price转为数字。这里我们可以用parsefloat,为了演示如何将build-in函数转化成函数式,我选择了parseInt,通过第二个参数传入10解决低版本浏览器兼容性问题。

R.compose是将2个方法组合使用,执行顺序是从右到左。然后我们只需要将我们组合好的工厂函数应用到获取到的价格数组上就行了。

正(ruan)文

大家可以发现,我们整个运算过程只使用一次元数据`result`,这也就是“无副作用”。另外我们将整个过程都转化成了数学解题,而不是让自己像计算机一样思考。

前端同学们熟悉的React库也在使用函数式编程的思想,例如单向数据流,State要求immutable。

对于数组和数据流的操作,特别适合函数式操作。ReactiveX就是将数据看成是函数式流处理,因此抽象出Observable, Observer, Subscription等等概念。

import { fromEvent } from 'rxjs';
import { throttleTime, scan } from 'rxjs/operators';

fromEvent(document, 'click')
  .pipe(
    throttleTime(1000),
    scan(count => count + 1, 0)
  )
  .subscribe(count => console.log(`Clicked ${count} times`));

今年D2我们的语言与框架专场邀请了RxJS:Reactive Extensions For JavaScript的团队负责人Ben Lesh,讲解如何利用JS特性重构RxJS,让代码更小更快。

另外函数式在TC39中,也是大厂专家积极讨论的对象,如何优化JS中写函数式的体验,相关议题有:

github.com/tc39/propos…

github.com/tc39/propos…

github.com/tc39/propos…

github.com/tc39/propos…

今年我们语言与框架专场也邀请了Igalia的TC成员Ujjwal,为我们讲解ES2020和ES2021都有哪些可以提升我们JS开发体验的新特性,以及这些特性是如何从TC39中诞生的。我们如何决定影响JS语言的未来。

后语

函数式编程也不是银弹,但在适用的场景,非常好用。大家无妨给自己的工具箱多装备一个函数式编程的工具。

我在今年的D2大会语言与框架专场等你。

参考文献:

mostly-adequate.gitbooks.io/mostly-adeq…

《Hey Underscore, You're Doing It Wrong!》,链接: www.youtube.com/watch?v=m3s…

《JavaScript函数式编程》

github.com/doodlewind/…

mostly-adequate.gitbooks.io/mostly-adeq…