javascript函数传-我从五年的 JavaScript 函数式编程学校学到了什么?

作者|维克多·纳科里亚科夫

译者|无知

编辑|秦云

这不是一篇关于学习 FP(函数编程)原理或 JavaScriptFP 库的文章。 关于这个主题的文章有很多,但本文将重点讨论在项目中切换到 JavaScriptS 函数式编程的危险过程及其后果。

在故事开始之前,我已经是一名拥有10多年经验的专业程序员了。 首先是 C++,然后是 C#,然后是 Python。 我还可以编写各种代码,而且我对模式和原理的掌握早已让我足够自信,我认为没有必要学习新东西。 我觉得我“已经掌握了 90% 的编程知识”。

2016年5月,我们开始开发XOD项目()。 XOD是一款专为数字爱好者打造的可视化编程IDE。 为了保持随意性,我们必须提供网页版的IDE。 网页版? 一定是JavaScript! 适用于所有 JavaScript 开发的 IDE? 是的,但是我们不能用 jQuery 凑合,我们需要更好的东西。

那时,出现了一种新的重量级后端开发技术:一种名为React的技术以及与之相伴的Flux/Redux模式。 在他们的文档和各种相关文章中,总是伴随着函数式编程的概念。 于是我开始学习FP。

哇! 这就像发现了一个新的台湾。 其实我也听说过Haskell、OCaml、LISP,但我认为这种程序员是边缘知识分子,他们编程是为了编程,而不是为了发布产品。 现在我开始怀疑自己的专业能力了。

函数式和响应式编程原则已深深植根于 XOD 的 DNA 中,但直到开发开始才显现出来。 不过,我“发明”或从其他产品借用的很多东西都是基于 FP 的。 为此,我们将使用一些重量级的现代 FRP JavaScript 创建一个 FRP 编程环境。

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

打破障碍

NPM 上可以找到大量 JavaScript 函数式编程库,其中最著名的是 Ramda()。 它有点像 Lodash 或 Underscore,但它是基于 FP 的。 Ramda 提供了数十种用于操作数据和组合函数的函数。

函数本身还不错,但是需要配合一些FP对象使用。 另一个库 RamdaFantasy() 提供了此类 FP 对象。 除此之外,还有其他 FP 库,如 Sanctuary、Fluture、Daggy。 但建议先从Ramda开始,让你的大脑先进入状态。

这是你可能遇到的第一个坎javascript函数传,就是当你看FP库的文档时,你会发现很多令人困惑的问题。 混乱的参数顺序、外语术语、不清楚的函数值,这类问题会让你停止尝试并退回到传统的编程模式。

首先,当你刚开始学习 FP 时,请阅读与特定编程语言或库无关的文章。 您首先需要了解整体的基本概念和优点,并评估如何将现有代码转换为新的编程模型。

许多关于函数式编程的文章都是由书呆子物理学家撰写的,在没有经过一些初步训练的情况下阅读它们是危险的:态射会扰乱你的思维,你最终将一无所获。

幸运的是,现在有很多好文章。 对我影响最大的是:

无意义的疯狂

当开始学习 FP 时,遇到的第一个不寻常的概念是策略编程,也称为无点编码或无意义编码。

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

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 和纯度的概念。 也就是说,从现在开始,函数不能有任何副作用。 他们不能引用这个,时间或随机,除了给定参数之外的任何东西,甚至不能引用全局字符串常量或 Pi。 你获取参数、工厂函数、生成器函数,从最里面的函数沿着嵌套链传递到内部,然后展开函数签名,现在你知道哪些是 Reader 或 Statemonad。 你用零星的映射和链感染你的代码,一碗日本面条已经计划好了!

第二点是,函数式编程不是关于lambdacalculus、monad、态射和组合子,而是关于如何定义许多不影响全局状态变化及其参数和输入输出的大型可组合函数。

换句话说,如果无意义的流在特定情况下效果更好,请使用它。 否则,请勿使用它。 不要仅仅因为可以使用 monad,而应该在它们真正解决问题时使用它们。 顺便说一句,你知道 Array 和 Promise 似乎是 monad 吗? 即使您不知道,也不影响您使用它们。 您应该训练您的直觉,直到您知道何时使用 monad,何时不使用。 练习需要一些时间,在真正理解新事物之前不要过度使用它们。

尽可能使用没有副作用的大型可组合函数是值得的,所以尝试一下。

抛出异常或返回 null

转用FP风格后,有一个问题让我感到很苦恼。 在传统 JS 中,至少有两种方法来发出错误信号:

使用 FP 时您始终可以执行此操作,并且还有附加的 Either 和 Maybemonad。 那么现在应该如何处理错误呢? API应该如何设计?

Maybe/Either 在某些方面可能是更“正确”的方法,但用户可能不熟悉。 他们更习惯使用null和异常,但总是在控制台看到“undefineddisnotafunction”之类的错误。

第三,不要害怕用 Maybe 和 Either 处理错误。

我们来看看面向HSR的编程模型()。 如果你在公共 API 中使用 Maybe,又怕它们不好理解,那就提供带有后缀的包装器,比如 Unsafe、Nullable、Exc 等,方便在命令式 JS 中使用。

令人上瘾的清晰

如果您正在开发一个使用函数式编程原理的项目,那么您很快就会看到其后果。 例如,代码审查需要较少的认知负荷。 当看一个函数的时候,你只需要考虑函数本身,而不需要担心如果一个组件的数组改变了会发生什么。 你不需要考虑是使用浅拷贝、深拷贝还是引用,你需要考虑的也不需要超出函数本身的几十行代码。

后来,当你听到新式的代码时,他们总是显得很可疑。 “嗯……为什么要改变一些对象的数组?为什么要在这个数组中保存这个值,它会在某个时候改变我的对象状态吗?”

第四点,一定要选择一个兼容FP的库和一个懂FP的朋友,前者尤其重要。 如果团队中出现争议,有的人努力使用FP,有的人则随意破坏FP原则,最终FP会失败。

雇用 FPJS 开发人员更加困难,因为它设定了很高的门槛。 而且,一旦您找到这样的人,他们可能是对您的产品最专业的人。 在 XOD,我们都是 FP 专家,我很高兴你们能够一起工作。

损失与收益

函数式编程与主流形式有很大不同,因此主流工具可能没有用。

Flow 和 Typescript 效果不佳,因为它们难以表达柯里化和参数多态性。 例如,Ramda虽然有绑定,但一般会提供误报javascript函数传,而当确实存在错误时,错误消息却很模糊。

有些库在运行时执行类型检查,我们发现了这一点。 不幸的是,它的扩展性不是很好,但性能损失通常小于函数本身的运行成本。 所以只能通过显式的方法进行类型检查,比如单元测试。

例如,如果您在深度函数组合中犯了错误并混淆了输入和输出类型,那么当您听到堆栈消息时,您可能会哭泣。

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的源代码。

在 CSS 上设置断点并增量调试不是更有意义吗? CSS 文件的覆盖范围是多少? 事实上,不会是100%。 当您从声明式切换回命令式时,该工具仍然有效,但问题是,如今您的代码对于开发工具来说是碎片化的,并且开发体验已经发生了巨大的变化。

第五点,刚接触FP的时候,会感觉不太愉快。 当我刚从 Windows 切换到 Linux 时,我也有同样的体味。 从成熟的 IDE 切换到 Vim 也是如此,希望你能理解这些见解。

我们能否将两全其美结合在一起? 不需要函数式编程的疯狂,却能获得函数式编程的绝佳体验? 我认为这是可能的。 还有其他基于 JS 的语言从一开始就面向函数式编程:Elm、PureScript、OCaml (BuckleScript) 和 ReasonML。

原文链接

后端顶部

前端之巅”是InfoQ旗下的一个专注于大后端技术的垂直社区。 紧跟时代时尚,分享一线技术,欢迎关注。

后端顶部

InfoQ 大后端技术社区

活动推荐

GMTC全球大后端技术大会携手顶级共创合作伙伴:APICloud企业互联网生态平台,历时半年为您梳理当前大后端领域的最新动态,并邀请国内一线谷歌、 Twitter、Instagram等后端专家来分享他们的后端前沿技术和最佳实践,还有iOS社区专家Mattt、ApolloGraphQL领军人物等大咖亮相,干货满满,值得一看不容错过。