日常 JavaScript 函数式编程:使用 Maybe

2025-06-10

日常 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."]
  }
Enter fullscreen mode Exit fullscreen mode

我们想要一个表单,让用户搜索其中一个单词,然后显示其含义。这很简单,对吧?还有什么可能出错呢?

因为每个人都喜欢 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>
Enter fullscreen mode Exit fullscreen mode

在第一个版本中,我们将尝试根据用户输入获取这些值之一。

// 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]);
});
Enter fullscreen mode Exit fullscreen mode

当然,我们首先搜索的是“酸”。结果如下。

一种能够在溶液中转移氢离子的化合物。
具有刺鼻或腐蚀性。
味道酸涩、刺激或刺激。
一种由麦角酸制成的强效致幻药。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});
Enter fullscreen mode Exit fullscreen mode

它是否因为空数组而抛出了异常?(如果它确实让我知道的话)。这不是很棒吗?知道即使没有任何可用数据,数组方法也会执行正确的操作,是不是感觉很温暖很温馨?这正是我们想要的。

你可能会想,难道我们不能只写几句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);
}
Enter fullscreen mode Exit fullscreen mode

这些包装器不会像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`,
  };
}
Enter fullscreen mode Exit fullscreen mode

这些方法的目的是什么?

  • map:将函数应用于Maybefun并将the_thing其再次包装起来以使派对继续进行......我的意思是保持对象的形状,这样您就可以继续链接函数。
  • and_then:这基本上是一个逃生舱。应用它的功能fun,然后听天由命吧。
  • or_else:这是else你的mapand_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');
Enter fullscreen mode Exit fullscreen mode

现在我们用新的 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);
 });
Enter fullscreen mode Exit fullscreen mode

现在我们测试一下。搜索“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);
}
Enter fullscreen mode Exit fullscreen mode

更改搜索功能。

- 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');
Enter fullscreen mode Exit fullscreen mode

非常棒。如果你想看全貌,这就是我们在 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);
});
Enter fullscreen mode Exit fullscreen mode

但这就是我们人生的全部吗?当然不是,我们想要的是爱,但既然 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}
);
Enter fullscreen mode Exit fullscreen mode

但是我们又无法执行安全的操作,因为一旦我们尝试从空数组中获取匹配项,整个过程就会崩溃。那么我们该怎么办呢?我们把它隐藏在一个函数下面。

// main.js

function suggest(word) {
  const matches = fzf.search(word);
  return Maybe(matches[0]);
}
Enter fullscreen mode Exit fullscreen mode

模糊搜索已完成,现在我们来添加一个超赞的确认对话框。你一定会喜欢的。

// main.js

function confirm_word(value) {
  if(value && confirm(`Did you mean ${value}`)) {
    return value;
  }
}
Enter fullscreen mode Exit fullscreen mode

我们将新功能与我们的结合起来search

// main.js

const suggest_word = value => () => suggest(value)
  .map(confirm_word)
  .map(search);
Enter fullscreen mode Exit fullscreen mode

将该功能添加到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');
Enter fullscreen mode Exit fullscreen mode

成功了!不过,假设我们对if语句过敏,更别提undefined从函数中 return 语句太不礼貌了。我们可以做得更好。

 function confirm_word(value) {
-  if(value && confirm(`Did you mean ${value}`)) {
-    return value;
-  }
+  return confirm(`Did you mean ${value}`);
 }
Enter fullscreen mode Exit fullscreen mode
 const suggest_word = value => () => suggest(value)
-  .map(confirm_word)
+  .filter(confirm_word)
   .map(search);
Enter fullscreen mode Exit fullscreen mode

我有点烦。我搜索“accu”,弹出对话框,我确认了建议,结果也出来了。但是“accu”仍然在输入框里,真尴尬。我们用正确的词更新一下输入框吧。

const update_input = val => window.search_form[0].value = val;
Enter fullscreen mode Exit fullscreen mode
 const suggest_word = value => () => suggest(value)
   .filter(confirm_word)
+  .tap(update_input)
   .map(search);
Enter fullscreen mode Exit fullscreen mode

想看看实际效果吗?那就这样吧。

奖励曲目

警告:这篇文章的重点(也就是我展示的那个 Codepen 示例)已经完成了。接下来是一个奇怪的实验,看看我能否让那个Maybe函数支持异步操作。如果你觉得累了,可以直接跳过所有内容,直接查看最后的示例代码。

现在你可能会说:这很可爱,但是在“现实世界”中,我们发出 http 请求、查询数据库、进行各种异步操作,这在那种情况下仍然有用吗?

我懂你的意思。我们目前的实现只支持普通的阻塞任务。你必须在a出现的Maybes那一刻就打破这个链条。Promise

但是如果……听着……我们做出一个有意识的承诺Just。我们可以做到,对吧AsyncJustJustAsync哦,那太糟糕了。

如果你不知道,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>`
  };
}
Enter fullscreen mode Exit fullscreen mode

如果我们消除噪音也许我们就能理解得更好。

// 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), 
}
Enter fullscreen mode Exit fullscreen mode
  • 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();
  }
}
Enter fullscreen mode Exit fullscreen mode

现在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();
  }
}
Enter fullscreen mode Exit fullscreen mode

我还想做最后一件事,那就是创建一个辅助函数,将常规值转换为Future

Future.from_val = function(val) {
  return Future(Promise.resolve(val));
}
Enter fullscreen mode Exit fullscreen mode

我们现在要做的就是支持这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);
 }
Enter fullscreen mode Exit fullscreen mode

但价值百万美元的问题仍然存在:它真的有效吗?

我有这个的CLI 版本。这是同一个 Codepen 示例,但做了一些调整:我添加了Future相关函数,确认对话框实际上是一个对话框(这个await),事件监听器现在是一个可以返回结果的异步函数。

奖金编辑

这就是我们作弊的时候的样子。如果我们不作弊,就会像这样

其他资源


感谢您的时间。如果您觉得这篇文章有用,并希望支持我的努力,请在ko-fi.com/vonheikemen留言

给我买杯咖啡

鏂囩珷鏉ユ簮锛�https://dev.to/vonheikemen/function-programming-for-your-everyday-javascript-using-a-maybe-126j
PREV
使用 git 修复错误
NEXT
我最喜欢的 5 个前端备忘单和书签 1. CSS Grid 备忘单 2. CSS Flex 备忘单 3. 缓动函数 4. CSS clip-path maker 5. CSS-TRICKS.com