翻译原文出处:
鄙人翻译略差且略有出入,别见笑。
很多时候我们会碰到:Uncaught TypeError: Cannot read property 'x' of undefined(无法读取未定义的属性“x”)。
我猜,如果你正好看到这个你以前不单只碰过还历历在目的东西,可能有那么一刻想把显示器给砸了。这里想起了我们尊敬的计算机领域的爵士——托尼·霍尔;他在Infoq办的大会演讲时,用到的主题是:“Null References: The Billion Dollar Mistake”(Null 引用:一个十亿美元级别的错误),讲演摘要中这样写的: “我把Null引用称为自己的十亿美元错误。它的发明是在1965年,那时我用一个面向对象语言( ALGOL W )设计了第一个全面的引用类型系统。我的目的是确保所有引用的使用都是绝对安全的,编译器会自动进行检查。但是我未能抵御住诱惑,加入了Null引用,仅仅是因为实现起来非常容易。它导致了数不清的错误、漏洞和系统崩溃,可能在之后40年中造成了十亿美元的损失。近年来,大家开始使用各种程序分析程序,比如微软的PREfix和PREfast来检查引用,如果存在为非Null的风险时就提出警告。更新的程序设计语言比如Spec#已经引入了非Null引用的声明。这正是我在1965年拒绝的解决方案。”那十亿美元级别的错误
幸运的是,我们可以使用一些功能性的编程技术,以清洁、简洁和可靠的方式缓解这带来疼痛。让我们想象一下,我们要从下面的对象中提取属性“c”的值,并附加字符串“is great”。
const a = { b: { c: "fp" }};
我们使用的简单方法可能是:
const appendString = (obj) => obj.b.c + " is great"; appendString(a);
这样的写法很棒,但可悲的是a
对象并非是一成不变的。因此,我们收回的数据有时会采取到以下的形式:
const a = { b: {}};// orconst a = {};
当这个时候我们调用了appendString函数时,整个宇宙将会爆炸的...
无法读取未定义的属性“c”
这个时候可能要我们对函数的传参进行空检查:
const appendString = (obj) => { if (!obj || !obj.b || !obj.b.c || !) return null; return obj.b.c + " is great";}
这是有效的,但它看起来很丑陋和很容易出错。我们必须对传参进行每种类型的对象定义特定的(正确的)空检查,这是不是很有趣(复杂)。
哈哈哈,这个时候可能Maybe
就派得上场了。 Maybe
的基本用法
基本上,我们都会将要构建的对象封装其值可能为null的概念,并且考虑到随之而来的复杂性。在学习Elm
(一门专注于Web前端的纯函数式语言)之后,我会在Maybe上的封装了两个概念状态Maybe.just
和Maybe.nothing
。对于初学者,我们简单地定义一个返回一个布尔值的isNothing
方法,告诉我们Maybe是否不包含任何内容:
const isNullOrUndef = (value) => value === null || typeof value === "undefined";const maybe = (value) => ({ isNothing: () => isNullOrUndef(value)});
甚至使用一个简单的工厂函数来创建我们的Maybe
- 考虑到往后可能会添加更多的方法,我们将使用一个对象来定义它:
const Maybe = { just: maybe, nothing: () => maybe(null)};
所以现在我们可以这样做:
const maybeNumberOne = Maybe.just("a value");const maybeNumberTwo = Maybe.nothing();maybeNumberOne.isNothing(); // falsemaybeNumberTwo.isNothing(); // true
一切都很好,但到目前为止还不是很实用。编程是关于转换数据的,所以我们需要一种改变我们的Maybe
的方法 - 一个map
函数。这个map
函数将使用一个表示我们希望进行转换的函数参数,并返回一个包含转换结果的新参数。重要的是,如果maybe
不包含任何内容,那么该函数将不会被应用,我们将返回一个新的maybe.nothing
方法。
const maybe = (value) => ({ isNothing: () => isNullOrUndef(value), map: (transformer) => !isNullOrUndef(value) ? Maybe.just(transformer(value)) : Maybe.nothing()});
现在我们可以这样来调用maybe
实现:
const maybeOne = Maybe.just(5);maybeOne.map(x => x + 1); // Maybe.just(6);const maybeTwo = Maybe.nothing();maybeTwo.map(x => x + 1) // Maybe.nothing();
关键一点的是maybe.map
返回一个新的maybe
,所以我们可以将这些操作链接在一起。回到我们现在可以做的最初的问题:
const a = { b: { c: "fp" }};const maybeA = Maybe.just(a) .map(a => a.b) .map(b => b.c) .map(c => c + " is great!");
这里的好处是,如果链中的任何步骤返回null,我们仍然会得到结果Maybe.nothing的结果,而不是运行时错误。
好了,在Github
上面有个maybe.js
库: :
它比Haskell
的实现更加灵活,而且还附带了一些额外的功能,考虑到
Point-free 链式函数
如果你看过我以前发布的文章,那么你就会想我们可以弄得比这更好。我们可以创建从对象中提取命名属性的高阶函数,以及用于追加字符串:
const prop = (propName) => (obj) => obj[propName];const append = (appendee) => (appendix) = appendee + appendix;
这里可以参考知乎上的里面的知识点。
所以现在我们可以在我们的map
链式中使用这个功能:
const a = { b: { c: "fp" }};const maybeA = Maybe.just(a) .map(prop("b")) .map(prop("c")) .map(append(" is great!"));
好了,我们现在终于到了这一步, 我们已经处理了空检查,并将其重构为point-free
形式。接下来需要做的就是使逻辑可重用性;我们想要的是能够将我们的功能传递给一个功能,并应用所有步骤,以便我们可以在许多不同的Maybe
上重新使用提取器:
const extractor = // what we're about to make extractor(Maybe.just(a)); // Maybe.just("fp is great")
我们需要的是一个需要我们的步骤的函数,并且每个步骤依次调用我们Maybe.map
方法。我们将调用函数Maybe.chain
,我们可以用reducer来实现:
const Maybe = { just: maybe, nothing: () => maybe(null), chain: (...fns) => (input) => fns.reduce((output, curr) => output.map(curr), input)};
我们现在可以构建一个可以应用于maybe
的可用功能:
const appendToC = Maybe.chain( prop("b"), prop("c"), append(" is great!"));
并将其用于各种输入:
const goodInput = Maybe.just({ b: { c: "fp" }});const badInput = Maybe.just({});appendToC(goodInput); // Maybe.just("fp is great!")appendToC(badInput); // Maybe.nothing()
虽然我在学习Elm
之前不习惯Maybe
的价值观概念,但是他们在JavaScript中不想落后啊。如果我们简简单单地去使用它,就只会使用到基础方法而已,所以我们要在它的基础上添加更多的功能函数。所以我将在稍后阅读关于使用Maybe
的后续文章。
::完毕::