日常 JavaScript 函数式编程:使用 Maybe
请阅读西班牙语版本。
你听说过 Monad 吗?它有多棒?也许你听说过,但还是不明白。好吧……我不是来告诉你它们是什么的,也不会试图向你推销它们,我只是举个例子,告诉你在 JavaScript 中使用它们会是什么样子。
我们会做一些有趣的事情,让我们以一种不必要的复杂方式解决一个相当简单的问题。
假设我们有一个存储在 json 文件或普通 js 对象中的字典。
{
"accident": ["An unexpected, unfortunate mishap, failure or loss with the potential for harming human life, property or the environment.", "An event that happens suddenly or by chance without an apparent cause."],
"accumulator": ["A rechargeable device for storing electrical energy in the form of chemical energy, consisting of one or more separate secondary cells.\\n(Source: CED)"],
"acid": ["A compound capable of transferring a hydrogen ion in solution.", "Being harsh or corrosive in tone.", "Having an acid, sharp or tangy taste.", "A powerful hallucinogenic drug manufactured from lysergic acid.", "Having a pH less than 7, or being sour, or having the strength to neutralize alkalis, or turning a litmus paper red."],
// ... moar words and meanings
"Paris": ["The capital and largest city of France."]
}
我们想要一个表单,让用户搜索其中一个单词,然后显示其含义。这很简单,对吧?还有什么可能出错呢?
因为每个人都喜欢 HTML,所以我们就从它开始。
<form id="search_form">
<label for="search_input">Search a word</label>
<input id="search_input" type="text">
<button type="submit">Submit</button>
</form>
<div id="result"></div>
在第一个版本中,我们将尝试根据用户输入获取这些值之一。
// main.js
// magically retrieve the data from a file or whatever
const entries = data();
function format(results) {
return results.join('<br>'); // I regret nothing
}
window.search_form.addEventListener('submit', function(ev) {
ev.preventDefault();
let input = ev.target[0];
window.result.innerHTML = format(entries[input.value]);
});
当然,我们首先搜索的是“酸”。结果如下。
一种能够在溶液中转移氢离子的化合物。
具有刺鼻或腐蚀性。
味道酸涩、刺激或刺激。
一种由麦角酸制成的强效致幻药。pH
值小于7,或呈酸性,或具有中和碱性的强度,或使石蕊试纸变红。
现在我们搜索“paris”,我确定它在那里。结果是什么?什么也没有。不完全是,什么也没有。
类型错误:结果未定义
我们还得到了一个难以预测的提交按钮,它有时能用,有时不能用。那么我们想要什么呢?我们真正想要的是什么?安全,不会让应用程序崩溃的对象,我们需要可靠的对象。
我们要做的是实现一个容器,让我们能够描述执行流程,而不必担心它们所持有的值。听起来不错吧?让我用一段 JavaScript 代码来演示一下。试试看。
const is_even = num => num % 2 === 0;
const odd_arr = [1,3,4,5].filter(is_even).map(val => val.toString());
const empty_arr = [].filter(is_even).map(val => val.toString());
console.log({odd_arr, empty_arr});
它是否因为空数组而抛出了异常?(如果它确实让我知道的话)。这不是很棒吗?知道即使没有任何可用数据,数组方法也会执行正确的操作,是不是感觉很温暖很温馨?这正是我们想要的。
你可能会想,难道我们不能只写几句if
语句就搞定吗?嗯……是的,但那样还有什么乐趣呢?我们都知道函数链很酷,而且我们是函数式编程的粉丝,所以我们会像每个精通函数式编程的人一样:把代码隐藏在函数下面。
因此,我们将隐藏一个if
语句(或者可能是几个),如果我们评估的值是未定义的,我们将返回一个包装器,无论发生什么情况,它都会知道如何表现。
// maybe.js
// (I would like to apologize for the many `thing`s you'll see)
function Maybe(the_thing) {
if(the_thing === null
|| the_thing === undefined
|| the_thing.is_nothing
) {
return Nothing();
}
// I don't want nested Maybes
if(the_thing.is_just) {
return the_thing;
}
return Just(the_thing);
}
这些包装器不会像Maybe
你在正规函数式编程语言中看到的那样,成为你的标准。为了方便起见,为了避免副作用,我们会稍微“作弊”。此外,它们的方法将以 Rust 中 Option 类型的方法命名(我更喜欢这些名字)。这就是奇迹发生的地方。
// maybe.js
// I lied, there will be a lot of cheating and `fun`s.
function Just(thing) {
return {
map: fun => Maybe(fun(thing)),
and_then: fun => fun(thing),
or_else: () => Maybe(thing),
tap: fun => (fun(thing), Maybe(thing)),
unwrap_or: () => thing,
filter: predicate_fun =>
predicate_fun(thing)
? Maybe(thing)
: Nothing(),
is_just: true,
is_nothing: false,
inspect: () => `Just(${thing})`,
};
}
function Nothing() {
return {
map: Nothing,
and_then: Nothing,
or_else: fun => fun(),
tap: Nothing,
unwrap_or: arg => arg,
filter: Nothing,
is_just: false,
is_nothing: true,
inspect: () => `Nothing`,
};
}
这些方法的目的是什么?
map
:将函数应用于Maybefun
并将the_thing
其再次包装起来以使派对继续进行......我的意思是保持对象的形状,这样您就可以继续链接函数。and_then
:这基本上是一个逃生舱。应用它的功能fun
,然后听天由命吧。or_else
:这是else
你的map
和and_then
。另一条路。“如果没有呢?”tap
:这个只是为了显示副作用。如果你看到了它,那么它可能正在影响它作用域之外的某些东西(或者可能只是放 的最佳位置console.log
)。- 过滤器:如果谓词函数返回真实值,它就会“让你通过”。
unwrap_or
:这就是你the_thing
退出的方法。当你完成链接方法并准备回到命令式世界时,你会需要这个。
让我们回到表单,看看它的实际效果。我们将创建一个函数search
,该函数可能会检索到与用户查询匹配的结果,也可能不会。如果检索到匹配结果,我们将链接其他将在“安全上下文”中执行的函数。
// main.js
const search = (data, input) => Maybe(data[input]);
const search_word = word => search(entries, word)
.map(format)
.unwrap_or('word not found');
现在我们用新的 safe(r) 功能取代不道德的旧方法。
window.search_form.addEventListener('submit', function(ev) {
ev.preventDefault();
let input = ev.target[0];
- window.result.innerHTML = format(entries[input.value]);
+ window.result.innerHTML = search_word(input.value);
});
现在我们测试一下。搜索“accident”。
意外、不幸的事故、故障或损失,可能对人类生命、财产或环境造成损害。
指无明显原因而突然或偶然发生的事件。
现在是巴黎。搜索“巴黎”。
未找到单词
它没有冻结按钮,这很好。但我知道巴黎在那里。如果你检查一下,你会发现那是“Paris”。我们只需将用户输入大写即可,这样他们就不必这样做了。首先,我们会尝试搜索精确的输入,如果失败,我们会尝试大写。
// main.js
function create_search(data, exact) {
return input => {
const word = exact ? input : capitalize(input);
return Maybe(data[word]);
}
}
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
更改搜索功能。
- const search = (data, input) => Maybe(data[input]);
+ const search = create_search(entries, true);
+ const search_name = create_search(entries, false);
-
- const search_word = word => search(entries, word)
+ const search_word = word => search(word)
+ .or_else(() => search_name(word))
.map(format)
.unwrap_or('word not found');
非常棒。如果你想看全貌,这就是我们在 main.js 中目前得到的结果。
// main.js
const entries = data();
function create_search(data, exact) {
return input => {
const word = exact ? input : capitalize(input);
return Maybe(data[word]);
}
}
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
function format(results) {
return results.join('<br>');
}
const search = create_search(entries, true);
const search_name = create_search(entries, false);
const search_word = word => search(word)
.or_else(() => search_name(word))
.map(format)
.unwrap_or('word not found');
window.search_form.addEventListener('submit', function(ev) {
ev.preventDefault();
let input = ev.target[0];
window.result.innerHTML = search_word(input.value);
});
但这就是我们人生的全部吗?当然不是,我们想要的是爱,但既然 JavaScript 无法满足我们的需求,那我们就满足于一个小小的“建议词”功能。我想搜索“accu”,然后弹出一个确认对话框,告诉我“您指的是 accumulator 吗?”
我们需要这方面的帮助,我们将引入一个依赖项,它可以对条目执行模糊搜索:fuzzy-search。因此,我们添加以下内容。
// main.js
import FuzzySearch from 'https://unpkg.com/fuzzy-search@3.0.1/src/FuzzySearch.js';
const fzf = new FuzzySearch(
Object.keys(entries),
[],
{caseSensitive: false, sort: true}
);
但是我们又无法执行安全的操作,因为一旦我们尝试从空数组中获取匹配项,整个过程就会崩溃。那么我们该怎么办呢?我们把它隐藏在一个函数下面。
// main.js
function suggest(word) {
const matches = fzf.search(word);
return Maybe(matches[0]);
}
模糊搜索已完成,现在我们来添加一个超赞的确认对话框。你一定会喜欢的。
// main.js
function confirm_word(value) {
if(value && confirm(`Did you mean ${value}`)) {
return value;
}
}
我们将新功能与我们的结合起来search
。
// main.js
const suggest_word = value => () => suggest(value)
.map(confirm_word)
.map(search);
将该功能添加到search_word
。
const search_word = word => search(word)
.or_else(() => search_name(word))
+ .or_else(suggest_word(word))
.map(format)
.unwrap_or('word not found');
成功了!不过,假设我们对if
语句过敏,更别提undefined
从函数中 return 语句太不礼貌了。我们可以做得更好。
function confirm_word(value) {
- if(value && confirm(`Did you mean ${value}`)) {
- return value;
- }
+ return confirm(`Did you mean ${value}`);
}
const suggest_word = value => () => suggest(value)
- .map(confirm_word)
+ .filter(confirm_word)
.map(search);
我有点烦。我搜索“accu”,弹出对话框,我确认了建议,结果也出来了。但是“accu”仍然在输入框里,真尴尬。我们用正确的词更新一下输入框吧。
const update_input = val => window.search_form[0].value = val;
const suggest_word = value => () => suggest(value)
.filter(confirm_word)
+ .tap(update_input)
.map(search);
想看看实际效果吗?那就这样吧。
奖励曲目
警告:这篇文章的重点(也就是我展示的那个 Codepen 示例)已经完成了。接下来是一个奇怪的实验,看看我能否让那个
Maybe
函数支持异步操作。如果你觉得累了,可以直接跳过所有内容,直接查看最后的示例代码。
现在你可能会说:这很可爱,但是在“现实世界”中,我们发出 http 请求、查询数据库、进行各种异步操作,这在那种情况下仍然有用吗?
我懂你的意思。我们目前的实现只支持普通的阻塞任务。你必须在a出现的Maybes
那一刻就打破这个链条。Promise
但是如果……听着……我们做出一个有意识的承诺Just
。我们可以做到,对吧AsyncJust
?JustAsync
哦,那太糟糕了。
如果你不知道,aPromise
是 JavaScript 用来协调未来事件的一种数据类型。为了实现这一点,它使用一个名为 的方法,then
该方法接受一个回调函数(catch
当出现问题时,它也接受回调函数)。所以,如果我们劫持了传入回调函数的内容then
,就能保持我们良好的Maybe
界面。
您跟进一系列回调的能力如何?
好了,我来给你看一下Future
。
// Don't judge me.
function Future(promise_thing) {
return {
map: fun => Future(promise_thing.then(map_future(fun))),
and_then: fun => Future(promise_thing.then(map_future(fun))),
or_else: fun => Future(promise_thing.catch(fun)),
tap: fun => Future(promise_thing.then(val => (fun(val), val))),
unwrap_or: arg => promise_thing.catch(val => arg),
filter: fun => Future(promise_thing.then(filter_future(fun))),
is_just: false,
is_nothing: false,
is_future: true,
inspect: () => `<Promise>`
};
}
如果我们消除噪音也许我们就能理解得更好。
// In it's very core is callbacks all the way.
{
map: fun => promise.then(fun),
and_then: fun => promise.then(fun),
or_else: fun => promise.catch(fun),
tap: fun => promise.then(val => (fun(val), val))),
unwrap_or: arg => promise.catch(val => arg),
filter: fun => promise.then(fun),
}
map
/and_then
:这些做同样的事情,因为你无法摆脱Promise
。or_else
:将回调放入catch
方法中以模仿else
行为。tap
:用于then
查看值。由于这是为了产生副作用,所以我们再次返回该值。unwrap_or
:它将返回 Promise,以便您可以使用await
。如果一切顺利,则Promise
在执行 时将返回的原始值await
;否则,将返回提供的参数。无论哪种方式,Promise 都不会抛出错误,因为已将方法Future
附加catch
到它。filter
:这一种是特殊的,map
这就是为什么filter_future
存在。- 几乎所有这些方法都会返回一个新的
Future
“原因”promise.then
返回一个新的Promise
。
真正Future
奇怪的是里面发生的事情map
。记得吗map_future
?
function map_future(fun) { // `fun` is the user's callback
return val => {
/* Evaluate the original value */
let promise_content = val;
// It needs to decide if the value of the Promise
// can be trusted
if(Maybe(promise_content).is_nothing) {
Promise.reject();
return;
}
// If it is a Just then unwrap it.
if(promise_content.is_just) {
promise_content = val.unwrap_or();
}
/* Evaluate the return value of the user's callback */
// Use Maybe because I have trust issues.
// For the javascript world is undefined and full of errors.
const result = Maybe(fun(promise_content));
if(result.is_just) {
// If it gets here it's all good.
return result.unwrap_or();
}
// at this point i should check if result is a Future
// if that happens you are using them in a wrong way
// so for now I don't do it
// There is something seriously wrong.
return Promise.reject();
}
}
现在filter_future
。
function filter_future(predicate_fun) { // the user's function
return val => {
const result = predicate_fun(val);
// Did you just returned a `Promise`?
if(result.then) {
// You did! That's why you can't have nice things.
// peek inside the user's promise.
const return_result = the_real_result => the_real_result
? val
: Promise.reject();
// keep the promise chain alive.
return result.then(return_result);
}
return result ? val : Promise.reject();
}
}
我还想做最后一件事,那就是创建一个辅助函数,将常规值转换为Future
。
Future.from_val = function(val) {
return Future(Promise.resolve(val));
}
我们现在要做的就是支持这Future
一点Maybe
。
function Maybe(the_thing) {
if(the_thing === null
|| the_thing === undefined
|| the_thing.is_nothing
) {
return Nothing();
}
-
- if(the_thing.is_just) {
+ if(the_thing.is_future || the_thing.is_just) {
return the_thing;
}
return Just(the_thing);
}
但价值百万美元的问题仍然存在:它真的有效吗?
我有这个的CLI 版本。这是同一个 Codepen 示例,但做了一些调整:我添加了Future
相关函数,确认对话框实际上是一个对话框(这个await
),事件监听器现在是一个可以返回结果的异步函数。
奖金编辑
这就是我们作弊的时候的样子。如果我们不作弊,就会像这样。
其他资源
- 奇妙而神秘的 JavaScript Maybe Monad
- JavaScript、Python、Ruby、Swift 和 Scala 中的 Option/Maybe、Either 和 Future Monad
- Monad 迷你系列:函子(视频)
- 哦,可组合的世界!(视频)
感谢您的时间。如果您觉得这篇文章有用,并希望支持我的努力,请在ko-fi.com/vonheikemen留言。
鏂囩珷鏉ユ簮锛�https://dev.to/vonheikemen/function-programming-for-your-everyday-javascript-using-a-maybe-126j