原文链接:So You Want to be a Functional Programmer (Part 6)
翻译:野草
本文发表于前端早读课【第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也有对应的mao
,Filter
以及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
函数进行断言的过滤函数。
当我们给函数onlyEven
和onlyOdd
传入numbers
,isEven
和isOdd
获得了最后的参数,然后执行最终返回我们期望的数字。
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语言是未来中的一部分。
我希望你读完这个系列之后,你对这些概念有了清晰的掌握,对自己也更有信心。