使用JavaScript进行两年函数式编程的经验教训

本文与学习FP原理或JavaScript FP库无关。 那里有很多关于该主题的好文章。 本文介绍了在一个项目中切换到功能性JS的冒险和后果。

当这个故事开始时,我已经是一个拥有10多年经验的专业程序员。 C ++,然后是C#,然后是Python。 我能够编程任何东西。 我对所获得的模式和原则的信心扩展到了一点,使我认为没有理由学习新的东西。 我想:“我知道90%的编程方面都不错。”

幸运的是,2016年5月,我们开始开发XOD项目 。 XOD是面向电子爱好者的可视化编程IDE。 为了保持休闲,我们必须使用IDE的Web版本。 网络? JavaScript! JavaScript中功能完善的IDE? 是的,我们将以快速而肮脏的jQuery结束一切; 我们需要更好的东西。

当时,正在出现一种用于大量前端开发的新技术:一种叫做React的技术及其随附的Flux / Redux模式。 在文档和文章中,它们与功能编程的概念高度交错。 我开始探索FP。

哇! 就像我发现了另一个大陆。 开发的澳大利亚,程序员在这里颠倒过来,数据在另一边流动。 当然,我听说过Haskell,OCaml,LISP,但是我曾经认为这样的开发人员是边缘化的知识分子,他们为了编程而编程,而不是发布产品。 我对自己专业知识水平的信念很快消失了。

XOD是在其基因中具有功能性和反应性编程原理的产品。 在开发开始之前还不清楚。 我“发明”或从其他产品中借来的许多东西确实是FP基础知识。 因此,与之相匹配的是,我们将使用一些沉重的现代FRP JavaScript创建一个FRP编程环境。

预测事件,值得付出努力。 FP为该项目提供了一个非常坚实而灵活的框架。 我不想再回顾“经典”编程了,并且肯定会在可预见的将来开发所有具有功能性编程原理的新项目。

突破障碍

您可以在NPM上找到许多JavaScript函数式编程库 。 最著名的之一是Ramda 。 这是一种“破折号”或“下划线”,但要牢记FP优先。 Ramda为您提供了数十种函数来处理数据和编写函数。

单独使用函数是好的,但是您需要使用一些FP对象。 另一个图书馆Ramda Fantasy将把它们带给您。 您可能还会注意到其他流行的FP库,例如SanctuaryFlutureDaggy 。 当您开始了解时,请检查一下。 但是,仅从Ramda开始,以保​​持大脑就位。

这是您偶然遇到的第一个障碍。 如果您查看任何FP库的文档,最好的情况下都会遇到许多WTF问题。 野蛮的论证顺序,外来的术语,某些函数的实际价值不清楚,将使您停止尝试并切换回常规编程。 所以…

点#1 。 通过与特定语言或库无关的文章开始学习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))
]
);

啊,什么? 你是一个很酷的人,你已经解决了。 在代码审查中与其他人分享难题。

接下来,您将学习单子和纯净。 好的,我的函数从现在开始不会有任何副作用。 他们不能引用this (很好),他们不能引用时间和随机性(oo-ok),他们不能引用给定的参数,甚至是全局字符串常量,甚至是数学Pi。 从最外部的函数到嵌套内部,从最外部的函数携带必要的arg,工厂和生成器,使签名爆炸,然后学习Reader或State monad。 哎呀,您用零星的一元映射和链条感染了所有代码,并且准备好了意大利面!

所以,组合器! 多么有趣的野兽。 哦,Y-combinator不仅是启动加速器,还是递归替换。 下次遇到递归或简单的“ reduce”调用可解决的问题时,让我们使用它。

点#2 。 函数式编程与lambda微积分,单子,态射和组合器无关 。 这是关于具有许多定义良好的可组合小功能,而不会导致全局状态,其参数和IO发生变化。

换句话说,如果无点样式在特定情况下有助于更好地沟通,请使用它。 否则,请不要。 不要使用monad,因为您可以在它们精确解决问题时使用它们。 顺便说一句,您知道ArrayPromise是单子吗? 如果没有,它不会阻止您正确应用它们。 当您了解所需的monad或更好的是根本不需要monad时,应该在一定程度上训练自己的直觉。 它伴随着实践,在您舒舒服服地思考它之前,不要过度使用新内容。

仅在可能的情况下切换到小的可组合函数而没有副作用将为您带来大部分好处。 从它开始。

引发异常,或者返回null

切换到FP风格的一个方面曾经让我很烦。 在经典JS中,您至少有两个选择来显示错误:

  • 返回null / undefined而不是结果
  • 引发异常

当您拿起FP时,您仍然可以选择这些选项,此外,还可以获得EitherMaybe monad。 我现在应该如何处理错误? 我的图书馆的公共API应该是什么样的?

从一个角度看, Maybe / Either是一种更“正确”的方式,但是对于图书馆消费者来说可能并不熟悉。 空和异常是习惯性的,但是在控制台中,您总是以` undefined is not a function '结尾。 长话短说…

点#3 。 不要害怕通过MaybeEither进行错误处理。 这对夫妇是您在单子世界中获得的最好的东西。

看一下出色的面向铁路的编程模式。 使用Maybes你的公共API中如果你怕你会不理解,提供薄膜包装的卫星由势在必行JS像`Unsafe`,`Nullable`,`Exc`后缀消费

清晰度是一种药物

当您在使用功能性编程原理开发的项目中进行协作时,您会很快注意到结果。 现在,进行审核需要更低的认知负担。 如果您看一下功能,那么功能代码就是您应该考虑的全部。 您不再需要想象改变该字段对该组件的后果。 您不认为在这里浅表副本,深层副本或仅参考是更合适的。 您只需要考虑比现在正在查看的十行代码更广阔的范围即可。

然后,当您看到老式代码时,它总是看起来可疑。 “嗯……为什么它会改变我的物体的视野? 为什么将其存储在字段中,是否会在未经许可的情况下随机更改我的对象?” 经典代码开始看起来是错误的。

点#4 。 您必须选择FP兼容的库和FP兼容的同事。 后者尤其重要。 如果摩擦区域很大,并且团队的一部分争取FP,另一部分自由破坏原则,则最终FP将在项目中被击败。

雇用FP JS开发人员比较困难,因为它设置了较高的最低级别。 但是,一旦找到一个,就很可能为您的产品找到最好的专业人士。 在XOD中,我们都是FP专家,很高兴我们能够共同努力。

受害人带来利益

函数式编程与主流编程有很大不同,您正在使用的面向主流的工具将停止工作。

Flow和Typescript无法正常工作,因为它们很难表达所有这些currying和arguments多态性。 例如,尽管存在Ramda的绑定,但它们通常会向您发出错误警报,并且当确实存在错误时,该消息将非常隐秘且不清楚。

您可以找到一些在运行时执行类型检查的库。 我们使用这样的一个 。 las,它们伸缩性不好。 性能损失通常高于函数执行本身的成本。 因此,您只能通过显式启用它来进行检查,例如用于单元测试。

如果您在深层构造中犯了错误(例如,混乱的输入和输出类型),则在看到堆栈跟踪时会哭泣。

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
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:14
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_arity.js:7:53
at src/project.js:887:5
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
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:14
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:27
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_arity.js:5:45
at _filter (/home/nailxx/devel/xod/node_modules/ramda/src/internal/_filter.js:7:9)
at /home/nailxx/devel/xod/node_modules/ramda/src/filter.js:47:7
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_dispatchable.js:39:15
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_curry2.js:20:46
at f1 (/home/nailxx/devel/xod/node_modules/ramda/src/internal/_curry1.js:17:17)
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:14
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_arity.js:5:45
at src/typeDeduction.js:171:37
at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:2491:23
at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:864:20
at src/project.js:618:33
at _Right.chain (/home/nailxx/devel/xod/node_modules/ramda-fantasy/src/Either.js:67:10)
at src/project.js:617:8
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
at _map (/home/nailxx/devel/xod/node_modules/ramda/src/internal/_map.js:6:19)
at map (/home/nailxx/devel/xod/node_modules/ramda/src/map.js:57:14)
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_dispatchable.js:39:15
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_curry2.js:20:46
at f1 (/home/nailxx/devel/xod/node_modules/ramda/src/internal/_curry1.js:17:17)
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:14
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:27
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:27
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_arity.js:5:45
at validateProject (src/project.js:1031:3)
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:27
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:27
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_arity.js:5:45
at src/flatten.js:1021:5
at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:2491:23
at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:864:20
at Context.<anonymous> (test/flatten.spec.js:1805:27)

在寻找问题根源时,大多数跟踪都是毫无意义的。 幸运的是,一旦FP代码首次成功运行,您就可以确定它是坚如磐石,并且将来不会给您带来任何惊喜。 如果您在JS中执行FP,则显而易见的结果是需要一个完整的单元测试套件。

代码覆盖率和断点也会中断。 FP代码更像CSS,而不是JS。 看一下XOD来源 。 将断点放置到CSS并逐步执行它是否有意义? CSS文件的覆盖范围是什么? 当然效果不是100%。 在您从声明式转换为命令式的地方,这些工具仍然有效。 但是现在您的代码对于devtools来说是零散的,并且体验发生了巨大的变化。

点#5 。 触摸FP后,您会感到不高兴和生气。 当我从Windows切换到Linux时,我曾经历过同样的情绪,并且了解到我俩都糟透了,我无法撤消这些知识。 从成熟的IDE到Vim的切换也是如此。 希望你理解这个主意。

我们能兼顾两全其美吗? 同时获得功能性编程,而又没有疯狂和出色的开发人员经验? 我认同。 从一开始,还有其他针对JS的语言可以使用: ElmPureScriptOCaml(BuckleScript)ReasonML

我最近在实践中尝试过ReasonML,但这是另一个故事。 如果您想听,请拍几下;)

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值