【译】成为一名函数式码农系列之六

原文链接:So You Want to be a Functional Programmer (Part 6)

作者:Charles Scalfani

翻译:野草

本文发表于前端早读课【第888期】

往期回顾:

成为一名函数式码农系列之一

成为一名函数式码农系列之二

成为一名函数式码农系列之三

成为一名函数式码农系列之四

成为一名函数式码农系列之五

下一步

你已经学了函数式编程相关的所有新知识,你可能开始困惑:“然后呢?我如何才能在我的工作中实践起来?”

这得具体问题具体分析。如果你工作中用的是像Elm或者Haskell这类纯函数语言,那这些知识很容易就能运用起来。

如果你只能用某种命令式语言(这种情况很常见),比如JavaScript,你还是能运用大部分我们之前提到的知识点,不过会有很多的限制。

函数式JavaScript

JavaScript拥有很多类函数式的特性。JavaScript没有纯性,但是我们可以设法得到一些不变量和纯函数,甚至可以借助一些库。

但这并不是理想的解决方法。如果你不得不使用纯特性,为何不直接考虑函数式语言?

不变性(Immutability)

首先要考虑的点就是不变性。ES6新增了一个关键词const,它意味着一旦被赋值,就不能重新设置:

const a = 1;
a = 2; // 抛出类型错误的异常 

此处a定义为常量,也就是说一旦被设定就不能再修改了,这就是为什么a = 2会抛出异常错误(除了Safari外)。

const的缺陷在于它不够严格,我们来看个例子:

const a = {
    x: 1,
    y: 2
};
a.x = 2; // 没有异常!
a = {}; // 抛出异常:类型错误

注意到a.x = 2没有抛出异常。用const关键词限制的只有变量a本身,a所指向的对象是可变的。

这很槽糕。本来以为有了const,JavaScript会更加完善。

那么问题来了,如何才能在JavaScript中得到不变性。

不幸的是,只有Immutable.js库能做到。虽然它能保证更好的不可变形,但悲剧的是,它以更像Java的方式写我们的JavaScript。

柯里化与整合(curring and composition)

之前我们已经学会如何将函数柯里化,举一个复杂的例子再回顾一下:

const f = a => b => c => d => a + b + c + d

我们得手写上述柯里化的过程。然后用如下的方式调用:

console.log(f(1)(2)(3)(4)); // 打印出 10

括号之多,连写Lisp的程序员都要hold不住了。

简化上述过程的库很多,我最喜欢用的库是Ramda

我们用Ramda去改写上面的例子:

const f = R.curry((a, b, c, d) => a + b + c + d);
console.log(f(1, 2, 3, 4)); // 打印出 10
console.log(f(1, 2)(3, 4)); // 打印出 10
console.log(f(1)(2)(3, 4)); // 打印出 10

函数定义的方法并没有改进多少,但在调用的时候可以避免写那么多括号了。调用f的时候,你想任意指定参数的个数。

我们重写一下之前的mult5AfterAdd10函数:

const add = R.curry((x, y) => x + y);
const mult5 = value => value * 5;
const mult5AfterAdd10 = R.compose(mult5, add(10));

事实上Ramda提供了很多辅助函数来做些简单常见的运算,比如R.add以及R.multiply。以上代码我们还可以简化:

const mult5AfterAdd10 = R.compose(R.multiply(5), R.add(10));

Map,Filter和Reduce

Ramda也有对应的maoFilter以及reduce函数。在原生JavaScript中,这几个函数是在Array.prototype对象中的,而在Ramda中它们是柯里化的:

const isOdd = R.flip(R.modulo)(2);
const onlyOdd = R.filter(isOdd);
const isEven = R.complement(isOdd);
const onlyEven = R.filter(isEven);
const numbers = [1, 2, 3, 4, 5, 6, 7, 8];
console.log(onlyEven(numbers)); // 打印出 [2, 4, 6, 8]
console.log(onlyOdd(numbers)); //  打印出 [1, 3, 5, 7]

R.modulo接受2个参数,被除数和除数。

isOdd函数表示一个数除2的余数。若余数为0,则返回false,即不是奇数;若余数为1,则返回true,是奇数。用R.filp置换一下R.modulo函数两个参数顺序,使得2作为除数。

isEven函数是isOdd函数的补集。

onlyOdd函数是由isOdd函数进行断言的过滤函数。当它传入最后一个参数,一个数组,它就会被执行。

同理,onlyEven函数是由isEven函数进行断言的过滤函数。

当我们给函数onlyEvenonlyOdd传入numbersisEvenisOdd获得了最后的参数,然后执行最终返回我们期望的数字。

JavaScript缺陷

借用了库和语言增强工具,JavaScript已经能做那么多事情了,但它仍然有个致命的缺陷——它是命令式语言,却想什么都做。

绝大多数的前端开发不得不使用JavaScript,因为它是浏览器唯一接受的语言。不过,也有很多开发者绕过了这个坑——他们用另一种语言编写,然后编译成JavaScript。

CoffeeScript是这类语言中最早的一批。目前,TypeScript已经被Angular2采用。Babel可以将这类语言编译成JavaScript。

越来越多的开发者在项目中采用这种方式。

但这些语言本质上还是JavaScript,并未有明显改善。为何我们大胆尝试直接使用一门纯函数语言,然后转译成JavaScript?

Elm

这系列文章,我们已经借用Elm来帮助我们理解函数式编程。

那Elm是什么?怎么用?

Elm是一种能编译成JavaScript的纯函数语言,你可以用Elm脚手架搭建一个Web应用。Elm脚手架,全称为The Elm Architecture,简称TEA,它是Redux的启蒙者。

Elm程序在运行时不会报错。

Elm在一些公司中已经投入了应用,比如NoRedLink。Evan Czapliki大神,Elm的作者,目前在这家公司工作(准确地说,他在Prezi工作)。

更多信息参见6 Months of Elm in Production,NoRedInk的Richard Feldman关于Elm的分享。

我要用Elm取代所有JavaScript吗?

不用,你可以逐步地取代。具体可参考教程How to use Elm at Work

为什么要学Elm?

  • 纯函数式编程具有约束和释放双重特性,它约束你的行为(通常是避免了给自己挖坑)同时让你远离bug和槽糕的设计。因为所有的Elm程序都要遵循Elm脚手架的规范。
  • 学习函数式编程能让你成为更好的程序员。本文涉及到的只是函数式编程的冰山一角。你应该去实践一下,感受它的魅力,感受它是如何减少你的代码量以及增加稳定性。
  • JavaScript最初是在10天内仓促地完成的,然后在过去的20多年里一直在打补丁,直到现在变成了一门有一点点函数式,一些些面向对象,完完全全的命令式编程语言。Elm学习了Haskell社区过去30多年的精华,而Haskell社区也有数十年的数学和计算机工作的沉淀。并且Elm脚手架的设计是Evan在函数响应式编程方面发表的论文结果实现,这几年来一直不断改善优化。(Controlling Time and Space 提到了它的设计理念)
  • Elm是为前端开发人员而生,旨在简化开发工作。(Let’s Be Mainstream深刻地说明了这一点)

未来展望

我们无法预知未来的趋势,但至少我们可以做有根据的猜测。以下是我的一些拙见:

能转译成JavaScript的这类语言将会有大进展;

存在40多年的函数式编程思想将重新被挖掘出来,用来解决我们目前遇到的复杂问题;

目前的硬件,比如廉价的内存,快速的处理器,使得函数式技术普及成为可能;

CPU不会变快,但是内核的数量会持续增加;

在复杂项目系统中可变性将成为最大要害之一。

我之所以写这系列文章,是因为我相信函数式编程是未来趋势,而且过去几年我学得挺费劲,当然我现在也还在学习中。

我希望我能帮助你们更容易更快地学会这些概念,帮助你们提升技能,然后未来有更好的出路。

即使我预言Elm将会普及的观点是错误的,我敢说函数式编程和Elm语言是未来中的一部分。

我希望你读完这个系列之后,你对这些概念有了清晰的掌握,对自己也更有信心。