从事编程十年的程序员深谈JavaScript函数式

在故事开始之前,我已经是一个拥有 10 年以上经验的专业程序员。先是 C++,然后是 C#,再然后是 Python。我能够写各种代码,我对模式和原则的掌握程度已经让我自信到看不到有学习新东西的必要。我认为自己已经“掌握了 90%的编程精髓”。

2016 年 5 月,我们开始开发 XOD 项目 (https://xod.io)。XOD 是一款为数字爱好者打造的可视化编程 IDE。为了保持它的随意性,我们必须提供一个 Web 版的 IDE。Web 版?那肯定非 JavaScript 莫属了!全部使用 JavaScript 开发的 IDE?是的,但我们不能将就使用 jQuery,我们需要更好的东西。

那时候,一种新的重量级前端开发技术出现了:一项叫做 React 的技术以及伴随而来的 Flux/Redux 模式。在它们的文档和各种相关文章中,总是伴随着函数式编程的概念。于是我开始研究 FP。

哇!就像发现了新大陆一样。当然,我也听说过 Haskell、OCaml、LISP,但我曾经认为这些程序员是那种存粹为了编程而编程,而不是为了发布产品而编程的边缘知识分子。而现在,我开始对自己的专业水平感到怀疑。

函数式和反应式编程原理根植于 XOD 的基因当中,但在开始开发之前并不明显。我“发明”的或从其他产品借鉴的很多东西其实都是以 FP 为基础。因此,我们将用一些重量级的现代 FRP JavaScript 创建一个 FRP 编程环境。

FP 为项目带来了坚实的基础和灵活性,我不想再回到“经典”的编程模式,并且在可预见的未来,我会继续基于函数编程原则来开发所有的新项目。

打破障碍

在 NPM 上可以找到大量的 JavaScript 函数式编程库,其中最值得一提的是 Ramda(http://ramdajs.com)。它有点像 Lodash 或 Underscore,不过它是基于 FP 的。Ramda 提供了几十个函数用于处理数据和组合函数。

函数本身是很好的,不过需要与一些 FP 对象配合使用。另一个库 Ramda Fantasy(https://github.com/ramda/ramda-fantasy)就提供了这样的 FP 对象。除此之外,还有其他一些 FP 库,如 Sanctuary、Fluture、Daggy。不过建议先从 Ramda 开始,先让你的大脑进入状态。

这是你可能会遇到的第一个障碍,就是在查看 FP 库的文档时,你会发现很多让人抓狂的问题。混乱的参数顺序、外来语术语、不明确的函数值,这些问题会促使你停止尝试并退回到传统的编程模式。

第一点,在刚开始学习 FP 时,阅读与特定编程语言或库无关的文章。你首先需要了解整体的基本概念和优势,并评估如何将现有代码转化到新的编程模式下。

关于函数式编程的许多文章都是由书呆子数学家撰写的,在没有经过初步训练的情况下就阅读它们是很危险的:morphism 会扰乱你的思路,最后什么也学不到。

幸运的是,现在有很多优秀的文章。对我来说最有影响力的是:

  • Mostly Adequate Guide to Functional Programming

​ https://mostly-adequate.gitbooks.io/mostly-adequate-guide

  • Thinking in Ramda

    http://randycoulman.com/blog/categories/thinking-in-ramda

  • Professor Frisby Introduces Composable Functional JavaScript

    https://egghead.io/courses/professor-frisby-introduces-composable-functional-javascript

  • Functors, Applicatives, And Monads In Pictures

    http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html

无意义的疯狂

在开始学习 FP 时,碰到的第一个不同寻常的概念是缄默编程(tatic programming),也称为无点流(point-free style)或无意义编程(pointless coding)。

其基本思想是省略函数参数名,或者更准确地说,省略参数:

export const snapNodeSizeToSlots = R.compose(
  nodeSizeInSlotsToPixels,
  pointToSize,
  nodePositionInPixelsToSlots,
  offsetPoint({ x: WIDTH * 0.75, y: HEIGHT * 1.1 }),
  sizeToPoint
);

这是一个典型的函数定义,完全由其他函数组成。它没有声明输入参数,但在调用时需要指定。即使没有上下文,你也可以理解这个函数的作用——输入大小然后产生一些像素坐标。但如果想要知道具体的细节,需要深入了解那些被组合的函数,而那些函数有可能是由其他函数组成的,并以此类推。

只要不被滥用,这算得上是一种非常强大的技术。当我们开始疯狂地使用 FP 技巧时,我们把所有东西都转换成无点流问题,然后像解决一个个谜题一样再把它们解开:

// Instead of
const format = (actual, expected) => {
  const variants = expected.join(‘, ‘);
  return `Value ${actual} is not expected here. Possible variants are: ${variants}`;
}
// you write
const format = R.converge(
  R.unapply(R.join(‘ ‘)),
  [
    R.always(“Value”),
    R.nthArg(0),
    R.always(“is not expected here. Possible variants are:”),
    R.compose(R.join(‘, ‘), R.nthArg(1))
 ]
);

好吧,搞定了,接下来在代码评审中与其他人分享这个谜题吧。

接下来,你会接触到 monad 和 purity 的概念。也就是说,从现在开始,函数不能有任何副作用。它们不能引用 this,不能引用 time 和 random,不能引用除给定参数以外的任何东西,甚至是全局字符串常量或圆周率 Pi。你带着参数、工厂函数、生成器函数,从最外层的函数顺着嵌套链一直传递到内部,然后展开函数签名,现在你知道什么是 Reader 或 State monad 了。你用零零星星的映射和链条来感染你的代码,一碗意大利面已经准备好了!

第二点,函数式编程不是关于 lambda calculus、monad、morphism 和 combinator,而是关于如何定义很多不影响全局状态变化的小型可组合函数以及它们的参数和输入输出。

换句话说,如果无点流在特定情况下可以更好地发挥作用,那么就用它。否则,就不要用。不要仅仅因为可以使用 monad 就随便用,而是要在它们确实可以解决问题的时候才用。顺便说一句,你知道 Array 和 Promise 其实就是 monad 吗?就算你不知道,也不影响你使用它们。你应该训练自己的直觉,直到知道什么时候应该用 monad,以及什么时候不该用。这需要一些时间来练习,在你真正了解新事物之前,不要过度使用它们。

在可能的情况下使用没有副作用的小型可组合函数是有好处的,所以可以尝试一下。

抛出异常或返回 null

切换到 FP 风格后,有一个问题曾经让我感到很烦恼。在传统的 JS 中,你至少有两种方式来表示错误:

  • 返回 null/undefined
  • 抛出一个异常

在使用 FP 时,你仍然可以这么做,并且还有额外的 Either 和 Maybe monad。那么现在应该如何处理错误?API 应该怎么设计?

从某种程度上来看,Maybe/Either 可能是一种更“正确”的方式,但对使用者来说可能并不熟悉。他们更习惯于使用 null 和异常,只不过总是会在控制台看大“undefined is not a function”这样的错误。

第三点,不要害怕通过 Maybe 和 Either 来处理错误。

让我们来看看面向铁路的编程模式(https://fsharpforfunandprofit.com/rop)。如果你在公共 API 中使用 Maybe,又担心它们不好理解,那么就提供带有后缀的包装器,如 Unsafe,Nullable,Exc 等,以便在命令式 JS 中使用。

让人上瘾的明晰

如果你所在的项目使用了函数式编程原则,很快你就会看到它所带来的后果。比如,代码评审要求的认知负载更低了。在查看一个函数时,你只需要考虑函数本身,不需要担心某个组件的字段被修改了会出现什么后果。你不需要考虑是使用浅拷贝、深拷贝还是引用,你需要思考的东西不需要超出函数本身的那几十行代码。

然后,当你看到旧式的代码时,它们看起来总是很可疑。 “嗯… 为什么修改了某个对象的字段?为什么把这个值保存在这个字段里,它会在某个时刻修改我的对象状态吗?“

第四点,你必须选择 FP 兼容库和懂 FP 的同事,而后者尤为重要。如果团队中存在争议,一部分人努力使用 FP,而另一部分人随意破坏 FP 原则,最终 FP 将会失败。

雇佣 FP JS 开发人员比较困难,因为它设定了一个很高的门槛。但是,一旦你找到了这样的人,他们对于你的产品来说很可能是最专业的。在 XOD,我们都是 FP 能手,我很高兴大家能够在一起工作。

失与得

函数式编程与主流方式相比差别很大,所以主流的工具可能派不上用场。

Flow 和 Typescript 无法正常运行,因为它们很难表达柯里化和参数多态性。例如,虽然 Ramda 具有绑定功能,但它通常会提供虚假警报,而当确实存在错误时,错误信息又含糊不清。

可以找一些在运行时执行类型检查的库,我们找到了这个 https://github.com/xodio/hm-def。可惜,它不能很好地进行扩展,而且性能损失通常高于函数本身的运行成本。所以你只能通过显式的方式进行类型检查,例如单元测试。

例如,如果你在深度函数组合中犯了一个错误,混淆了输入和输出类型,那么在看到堆栈信息时,你很可能会大哭一场。

Error: Can’t find prototype Patch of Node with Id “HJbQvOPL-” from Patch “@/main”
 at /home/nailxx/devel/xod/packages/xod-func-tools/dist/monads.js:88:9
 at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:2491:23
 at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:860:20
 ...

在查找问题根源时,大部分堆栈信息都是毫无意义的。幸运的是,一旦 FP 代码能够成功运行,你就可以认为它是坚如磐石的,将来不会给你带来任何意外。如果你在 JS 中使用 FP,那么很明显的,你需要进行一系列彻底的单元测试。

代码覆盖率和断点也会出问题。FP 代码更像是 CSS 而不是 JS。请看看 XOD 的源代码https://github.com/xodio/xod/tree/master/packages/xod-project/src。

将断点放在 CSS 上并逐步调试是不是更有意义?CSS 文件的覆盖率是多少?当然,它不会是 100%。在你从声明式切换回命令式时,这些工具仍然奏效,但问题是现在你的代码对于开发工具来说是碎片化的,并且开发体验也发生了巨大的变化。

第五点,当你刚开始接触 FP 时,你会感到不那么愉快。当我刚从 Windows 切换到 Linux 时,我有同样的感受。从成熟的 IDE 切换到 Vim 也是如此,希望你能够明白这种感受。

我们能否把这两个世界最美好的部分集中在一起?不需要函数式编程的疯狂,却又能获得函数式编程的极佳体验?我想是可以的。有其他一些基于 JS 的语言,它们从一开始就是面向函数式编程:Elm、PureScript、OCaml(BuckleScript)和 ReasonML。

更多前端学习内容文章干货请关注我的知乎专栏(不断更新),以后有新文章会提醒您,下面的链接是我知乎文章的集合,我把所有重要的文章放在这个目录里面,供大家阅读,希望能对大家有用

[阿里名厂标准web前端高级工程师教程目录大全,从基础到进阶,看完保证您的

你可能感兴趣的:(前端,javascript)