微信号:frontshow

介绍:InfoQ大前端技术社群:囊括前端、移动、Node全栈一线技术,紧跟业界发展步伐。

我从两年的JavaScript函数式编程中学到了什么?

2018-06-08 17:43 无明 译
作者|Victor Nakoryakov
译者|无明
编辑|覃云

这不是一篇关于学习 FP(Functional Programming,函数式编程)原则或 JavaScript FP 库的文章。这方面的文章有很多,而本文将着重讲述在一个项目中切换到 JavaScriptS 函数式编程的冒险过程以及它所产生的后果。

在故事开始之前,我已经是一个拥有 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。

  原文链接

https://hackernoon.com/two-years-of-functional-programming-in-javascript-lessons-learned-1851667c726

前端之巅

「前端之巅」是 InfoQ 旗下关注大前端技术的垂直社群。紧跟时代潮流,共享一线技术,欢迎关注。


前端之巅

InfoQ大前端技术社群

 活动推荐

GMTC全球大前端技术大会携手顶级共创伙伴:APICloud企业互联网化生态平台,历时半年筹备,为大家梳理了目前大前端领域的最新动态,并邀请到了来自Google、Twitter、Instagram等国外一线前端专家前来分享他们的前端前沿技术和最佳实践,更有iOS社区大神Mattt、Apollo GraphQL负责人等大牛的助阵,可谓干货满满,不容错过。

目前大会倒计时门票热销中,团购更优惠,购票咨询:18514549229(同微信)戳阅读原文或识别下图二维码,了解更多干货详情!


 
前端之巅 更多文章 我从React身上得到的那些经验教训 引擎V8推出“并发标记”,可节省60%-70%的GC时间 为什么前端开发这么不稳定? 你需要了解的23种JavaScript设计模式 Node之父ry发布新项目deno:下一代Node
猜您喜欢 musical.ly微服务改造及国际化架构挑战 今日好书丨《VR开发实战》 网络直播内容审核:拒绝黄拒绝赌拒绝黄赌毒! 向Scrum之父Ken Schwaber提问 说一下我对豌豆荚被收购的思考