发布于 2026-01-06 4 阅读
0

Fluture简介——Promise的功能性替代方案

Fluture简介——Promise的功能性替代方案

弗卢图

GitHub 标志 fluture-js / Fluture

🦋 符合 Fantasy Land 规范的(单子)Promises 替代方案

弗卢图

构建状态 代码覆盖率 依赖状态 NPM 包 Gitter聊天

Fluture 提供了一种类似于 Promises、Tasks、Deferreds 等的控制结构。我们姑且称之为 Futures。

与 Promises 类似,Future 表示异步操作(I/O)成功或失败所产生的值。但与 Promises 不同的是,Future 是惰性的并且遵循单子接口

Fluture提供的一些功能包括:

更多信息:

安装

使用 NPM

$ npm install --save fluture
Enter fullscreen mode Exit fullscreen mode

从 CDN 捆绑

要将 Fluture 直接加载到浏览器、CodePen 或Deno中,请使用 JSDelivr 内容分发网络提供的以下下载链接之一。这些都是单个……

在本文中,我们将介绍如何使用 Futures,假设《破碎的承诺》一书已经充分解释了为什么需要使用 Futures 。


我们将详细介绍 Fluture 的五大核心概念:

  1. 函数式编程:函数式编程模式如何决定 Fluture API。
  2. 未来实例:未来实例代表什么,以及如何创建未来实例。
  3. 未来消费:什么是未来消费,以及我们何时以及如何应用它。
  4. 未来转型:在我们消费未来之前,我们可以对未来做些什么,以及为什么这很重要。
  5. 分支和错误处理:Fluture 的“拒绝分支”简介,以及它与被拒绝的 Promises 的区别。

函数式 API

Fluture API 的设计旨在与函数式编程范式以及该生态系统中的库(例如RamdaSanctuary)完美兼容。因此,你会发现它几乎没有方法,并且库提供的所有函数都使用了函数柯里化

因此,一段基于 Promise 的代码可能如下所示:

promiseInstance
.then(promiseReturningFunction1)
.then(promiseReturningFunction2)
Enter fullscreen mode Exit fullscreen mode

简单地将其翻译成基于 Fluture 的代码(使用chain)会得到:

chain (futureReturningFunction2)
      (chain (futureReturningFunction1)
             (futureInstance))
Enter fullscreen mode Exit fullscreen mode

虽然我使用了函数式风格的缩进来使这段代码更易读,但我不得不承认,基于 Promise 的代码读起来更好。

但这种看似疯狂的做法其实是有道理的:该 API 经过精心设计,可以很好地与函数组合配合使用。例如,我们可以使用flowLodash * 中的函数组合,使同一个程序看起来更像基于 Promise 的代码:

_.flow ([
  chain (futureReturningFunction1),
  chain (futureReturningFunction2),
]) (futureInstance)
Enter fullscreen mode Exit fullscreen mode

* 还有pipe来自 SanctuarypipeRamda等众多品牌。

更棒的是,函数组合将在未来版本的 JavaScript 中以管道操作符 (Pipeline Operator)的形式加入。一旦这项功能被引入,我们编写的代码将与基于 Promise 的代码完全相同。

futureInstance
|> chain (futureReturningFunction1)
|> chain (futureReturningFunction2)
Enter fullscreen mode Exit fullscreen mode

虽然看起来完全相同,但这种基于函数的代码更加解耦,也更容易重构。例如,我可以提取管道的一部分并将其封装成一个函数:

+const myFunction = chain (futureReturningFunction1)
+
 futureInstance
-|> chain (futureReturningFunction1)
+|> myFunction
 |> chain (futureReturningFunction2)
Enter fullscreen mode Exit fullscreen mode

对流畅的方法链进行这样的操作就没那么简单了:

+const myFunction = promise => promise.then(promiseReturningFunction1)
+
+(
 promiseInstance
-.then(promiseReturningFunction1)
+|> myFunction
+)
 .then(promiseReturningFunction2)
Enter fullscreen mode Exit fullscreen mode

由于管道操作符目前仍处于语言提案阶段,我们可能身处一个尚未启用该操作符的环境中。Fluture 提供了一种pipe方法来模拟使用管道操作符的体验。它具备管道操作符的所有机制优势,但代码略显冗长。

futureInstance
.pipe (chain (futureReturningFunction1))
.pipe (chain (futureReturningFunction2))
Enter fullscreen mode Exit fullscreen mode

创建未来实例

Future 实例与 Promise 实例略有不同,前者代表异步计算,后者代表异步获取的值。不过,创建 Future 实例与创建 Promise 实例非常相似。最简单的方法是使用resolve` rejectresolve` 或 `rejected` 函数,它们分别创建已解决或已拒绝的 Future。目前,我们将重点关注通用构造函数 `future.get()` Future,以及它与 Promise 构造函数的比较。

const promiseInstance = new Promise ((res, rej) => {
  setTimeout (res, 1000, 42)
})
Enter fullscreen mode Exit fullscreen mode
const futureInstance = Future ((rej, res) => {
  const job = setTimeout (res, 1000, 42)
  return function cancel(){
    clearTimeout (job)
  }
})
Enter fullscreen mode Exit fullscreen mode

一些显著差异:

  1. new关键字并非必需。在函数式编程中,我们不区分返回对象的函数和返回任何其他类型数据的函数。

  2. and参数rejres位置互换了,这与函数式编程领域的一些约定有关,其中“更重要的”泛型类型通常放在最右边。

  3. 我们在 Future 构造函数中返回一个取消函数cancel。这使得 Fluture 能够在不再需要正在运行的计算时进行清理。更多内容请参见“消费 Future”部分。


上面使用的构造函数Future是创建新 Future 的最灵活方式,但还有更具体的创建 Future的方法。例如,要从 Node 风格的回调函数创建 Future,我们可以使用 Fluture 的node函数:

const readText = path => node (done => {
  fs.readFile (path, 'utf8', done)
})
Enter fullscreen mode Exit fullscreen mode

这里我们创建了一个函数readText,给定一个文件路径,该函数返回一个 Future,该 Future 可能会返回一个 Error,或者返回从 utf8 解码的相应文件的内容。

使用灵活的 Future 构造函数来实现同样的功能则需要更多工作:

const readText = path => Future ((rej, res) => {
  fs.readFile (path, 'utf8', (err, val) => err ? rej (err) : res (val))
  return () => {}
})
Enter fullscreen mode Exit fullscreen mode

正如我们所见,node它处理了空的取消函数,并巧妙地处理了回调参数。此外,还有 Future 构造函数,可以减少处理底层 Promise 函数或抛出异常的函数时的样板代码。欢迎探索。所有这些都列在Fluture 文档的“创建 Future”部分。

在日常使用中,你会发现Future构造函数只在最特殊的情况下才需要,使用更专业的构造函数也能达到很好的效果。

消费期货

与 Promise 不同,Future 最终必须被“消耗”。这是因为——正如我之前提到的——Future 代表的是计算过程,而非数值。因此,我们需要在某个时刻指示 Future 执行计算。“指示 Future 执行计算”就是我们所说的 Future 的消耗。

使用 Future 的首选方法是通过 `.` 函数fork。此函数接受两个延续(或回调),一个用于 Future 被拒绝时,另一个用于 Future 被解决时。

const answer = resolve (42)

const consume = fork (reason => {
  console.error ('The Future rejected with reason:', reason)
}) (value => {
  console.log ('The Future resolved with value:', value)
})

consume (answer)
Enter fullscreen mode Exit fullscreen mode

当我们实例化answerFuture 时,什么也没发生。这适用于我们通过任何方式实例化的任何 Future。Future 会一直保持“冷”状态,直到被消耗掉。这与 Promise 不同,Promise 会在创建后立即执行其计算。因此,只有上面示例中的最后一行代码才真正启动了 Future 所代表的计算answer

在这种情况下,如果我们运行这段代码,就能立即看到答案。这是因为我们resolve (42)事先就知道答案。但许多 Future 需要一些时间才能得到答案——可能是因为网络连接速度慢,或者需要生成僵尸网络来计算答案。这也意味着等待时间可能过长例如用户感到无聊,或者其他来源提供了令人满意的答案。对于这些情况,我们可以取消订阅 Future:

const slowAnswer = after (2366820000000000000) (42)
const consume = value (console.log)
const unsubscribe = consume (slowAnswer)

setTimeout (unsubscribe, 3000)
Enter fullscreen mode Exit fullscreen mode

在这个例子中,我们使用 ` afterto` 创建一个 Future,它大约需要七百五十万年才能计算出答案。我们使用 ` valueto` 来消费这个 Future,并将其输出赋值给unsubscribe`.`

等了三秒钟都没等到回复,我们觉得无聊,于是取消了订阅。之所以能取消订阅,是因为大多数消费函数都返回了自己的取消订阅函数。取消订阅时,Fluture 会使用底层构造函数中定义的取消函数(在本例中,就是由 `func` 函数创建的取消函数after)来停止所有正在运行的计算。更多相关信息请参阅Fluture README 文件中的“取消”部分。

消费 Future 可以理解为将异步计算的结果转化为它最终将要存储的值。除了这种方式之外,还有其他方式fork可以消费 Future。例如,promise函数可以消费 Future 并返回一个包含其最终结果的 Promise。

不消费期货

与 Promise 不同,我们可以选择暂时消费 Future。只要 Future 尚未被消费,我们就可以随意扩展、组合、合并、传递以及以其他方式对其进行各种操作。这意味着我们将异步计算视为常规值,可以像操作普通值一样对其进行操作。

操控未来(我们可是时间领主啊!)正是 Fluture 库的核心所在——我在这里列举一些可能性。你不必对这些可能性过度解读:它们只是为了让你了解你可以做的事情。我们也会在下面的示例中使用这些函数。

  • chain使用返回另一个 Future 的函数来转换 Future 中的值。
  • map使用函数转换 Future 中的值,以确定它应该保存的新值。
  • both接受两个 Future,并返回一个新的 Future,该 Future 并行运行这两个 Future,最终返回一个包含它们值的对。
  • and接受两个 Future 对象,并返回一个新的 Future 对象,该对象按顺序运行这两个 Future 对象,并以第二个 Future 对象运行的值作为最终结果。
  • lastly接受两个 Future 对象,并返回一个新的 Future 对象,该对象按顺序运行这两个 Future 对象,并以第一个 Future 对象运行的值作为最终结果。
  • parallel接受一个 Future 列表,并返回一个新的 Future,该 Future 会并行运行所有这些 Future,并设置用户选择的限制,最后返回一个包含每个 Future 的解析值的列表。

还有更多功能。所有这些功能的目的都是为了让我们能够完全掌控异步计算:顺序执行或并行执行、运行或不运行、从故障中恢复等等。只要 Future 对象尚未被使用,我们就可以随意修改它。

将异步计算表示为常规值——或者说“一等公民”——赋予了我们一种难以言喻的灵活性和控制力,但我会尽力解释。我将演示一个与我之前遇到的问题类似的问题,并说明我提出的解决方案只有在异步计算被赋予一等公民身份的情况下才有可能实现。假设我们有一个如下所示的异步程序:

//This is our readText function from before, reading the utf8 from a file.
const readText = path => node (done => fs.readFile (path, 'utf8', done))

//Here we read the index file, and split out its lines into an Array.
const eventualLines = readText ('index.txt')
                      .pipe (map (x => x.split ('\n')))

//Here we take each line in eventualLines, and use the line as the path to
//additional files to read. Then, using parallel, we run up to 10 of those
//file-reads in parallel, obtaining a list of all of their texts.
const eventualTexts = eventualLines
                      .pipe (map (xs => xs.map (readText)))
                      .pipe (chain (parallel (10)))

//And at the end we consume the eventualTexts by logging them to the console.
eventualTexts .pipe (value (console.log))
Enter fullscreen mode Exit fullscreen mode

本例中解决的问题是基于异步问题

如果程序运行时间过长,我们想找出程序中哪个部分耗时最长,该怎么办?传统上,我们需要修改转换函数,添加调用console.time。而使用 Futures,我可以定义一个自动执行此操作的函数:

const time = tag => future => (
  encase (console.time) (tag)
  .pipe (and (future))
  .pipe (lastly (encase (console.timeEnd) (tag)))
)
Enter fullscreen mode Exit fullscreen mode

让我们逐行分析这个函数,看看它是如何将异步计算作为一等公民来实现其功能的。

  1. 我们接收两个参数,tag分别是 `a` 和 ` futureb`。需要注意的是 `a` future。这个函数演示了我们很少对 Promise 做的事情,那就是将它们作为函数参数传递。
  2. 我们通常会将encase调用包装console.time在一个 Future 对象中。这样可以防止它立即执行,并允许我们将其与其他 Future 对象组合使用。这是使用 Future 对象时的一种常见模式。将任何具有副作用的代码包装在 Future 对象中,可以更轻松地管理副作用,并控制其发生的位置、时间和是否发生。
  3. 我们过去常常and将作为参数传入的未来与启动计时器的未来结合起来。
  4. 我们过去常常lastly将计算(现在包括启动计时器,然后执行任意任务)与使用最后一步将计时结果写入控制台相结合console.timeEnd

实际上,我们创建的是一个函数,它接受任何Future 对象,并返回一个具有相同类型的新 Future 对象,但该新 Future 对象包装在两个副作用中:定时器的初始化和终止。

有了它,我们就可以自由地在代码中穿插定时器,而不必担心副作用(由time函数的返回值表示)会在错误的时间发生:

//Simply pipe every file-read Future through 'time'.
const readText = path => node (done => fs.readFile (path, 'utf8', done))
                         .pipe (time (`reading ${path}`))

//Measure reading and processing the index as a whole.
const eventualLines = readText ('index.txt')
                      .pipe (map (s => s.split ('\n')))
                      .pipe (time ('getting the lines'))

const eventualTexts = eventualLines
                      .pipe (map (ss => ss.map (readText)))
                      .pipe (chain (parallel (10)))

//And finally we insert an "everything" timer just before consumption.
eventualTexts .pipe (time ('everything')) .pipe (value (console.log))
Enter fullscreen mode Exit fullscreen mode

time函数只是将一个计算从一个“指令列表”转换为另一个“指令列表”,并且新的计算总是会在我们要测量的指令之前和之后插入计时指令。

这一切的目的在于阐明“一流异步计算”的优势;如果没有异步计算,像这样的实用函数time是不可能实现的。例如,使用 Promise 时,当 Promise 被传递给time函数时,它实际上已经在运行了,因此时间上会存在偏差。


本节标题为“不消耗 Futures”,它强调了一个我非常想重点阐述的观点:为了修改计算,计算不应该在运行之前就执行。因此,我们应该尽可能长时间地避免消耗计算资源。

一般来说,每个程序都只有一个地方会使用 Future,那就是在程序的入口点附近。

分支和错误处理

到目前为止,本文只讨论了异步计算的“正常路径”。但我们都知道,异步计算偶尔也会失败;这是因为在 JavaScript 中,“异步”通常意味着 I/O 操作,而 I/O 操作可能会出错。因此,Fluture 提供了一个“拒绝分支”,使其能够用于一种有时被称为“铁路导向编程”(Railway Oriented Programming)的编程风格。

map当使用诸如上述的转换函数或转换函数来转换 Future 时chain,我们只会影响其中一个分支,而不会影响另一个分支。例如,map (f) (reject (42))如果 `is` 等于 ` reject (42):`,则转换没有生效,因为 Future 的值位于拒绝分支中。

还有一些函数只影响拒绝分支,例如 ` mapRejreject` 和 `reject` chainRej。以下程序打印答案 42,因为我们从一个被拒绝的Future 开始,并对拒绝分支应用转换。在最后一次使用 `reject` 的转换中,我们通过返回一个已解决的chainRejFuture将其切换回解决分支

const future = reject (20)
               .pipe (mapRej (x => x + 1))
               .pipe (chainRej (x => resolve (x + x)))

future .pipe (value (console.log))
Enter fullscreen mode Exit fullscreen mode

最后,还有一些函数会同时影响两个分支,例如 ` bimapand` 和coalesce`。它们当然有其用途,但你使用它们的频率会比较低。


我有时会把未来的两条分支想象成两条平行的铁轨,各种转换功能则用连接点来表示,这些连接点会影响铁轨和列车的有效载荷。我来画一下。想象一下这两条线都是铁轨,列车从上往下行驶,分别沿着其中一条铁轨行驶。

                 reject (x)  resolve (y)
                       \      /
                  :     |    |     :
         map (f)  :     |   f y    :  The 'map' function affects the value in
                  :     |    |     :  the resolution track, but if the train
                  :     |    |     :  would've been on the rejection track,
                  :     |    |     :  nothing would've happened.
                  :     |    |     :
                  :     |    |     :
       chain (f)  :     |   f y    :  The 'chain' function affects the value in
                  :     |   /|     :  the resolution track, and allowed the
                  :     |  / |     :  train to change tracks, unless it was
                  :     | /  |     :  already on the rejection track.
                  :     |/   |     :
                  :     |    |     :
coalesce (f) (g)  :    f x  g y    :  The 'coalesce' function affects both
                  :      \   |     :  tracks, but forces the train to switch
                  :       \  |     :  from the rejection track back to the
                  :     _  \ |     :  resolution track.
                  :     |   \|     :
                  :     |    |     :
         and (m)  :     |    m     :  The 'and' function replaces a train on
                  :     |   /|     :  the resolution track with another one,
                  :     |  / |     :  allowing it to switch tracks.
                  :     | /  |     :
                  :     |/   |     :
                  :     |    |     :
    chainRej (f)  :    f y   |     :  The 'chainRej' function is the opposite
                  :     |\   |     :  of the 'chain' function, affecting the
                  :     | \  |     :  rejection branch and allowing a change
                  :     |  \ |     :  back to the resolution track.
                  :     |   \|     :
                  :     |    |     :
                        V    V
Enter fullscreen mode Exit fullscreen mode

这种编程模型与 Bash 脚本中的管道机制有些类似,其中 stderr 和 stdout 分别相当于拒绝分支和解决分支。它允许我们只关注成功路径,而无需担心失败路径会干扰程序运行。

从某种意义上说,Promise 也存在类似的问题,但 Fluture 对拒绝分支的用途采取了略有不同的立场。这种差异最明显地体现在抛出异常的处理方式上。对于 Promise,如果我们抛出一个异常,它最终会进入拒绝分支,并与其他可能存在的情况混杂在一起。这意味着,从根本上讲,Promise 的拒绝分支没有严格的类型限制。这使得 Promise 的拒绝分支成为代码中可能产生任何意外值的地方,因此,它并不适合“铁路式”的控制流。

Future 的拒绝分支旨在简化控制流程,因此不会混入抛出的异常。这也意味着 Future 的拒绝分支可以严格限定类型,并生成我们预期类型的​​值。

在使用 Fluture(以及一般的函数式编程方法)时,异常实际上并不适合作为控制流的结构。相反,抛出异常的唯一合理理由是开发者犯了错误,通常是类型错误。Fluture 秉持函数式编程的理念,会乐于让这些异常传播。

这种理念认为,异常意味着错误,而错误对代码行为的影响应该尽可能小。在编译型语言中,这种故障路径的分类更加明显,一种发生在编译时,另一种发生在运行时。

总之

  1. Fluture API的设计基于函数式编程范式。它非常重视函数组合而非流畅的方法链,并且能够很好地与其他函数式库兼容。
  2. Fluture 提供了几个特定的​​函数和一个通用构造函数来创建 Future。Future 代表异步计算,而非最终值。因此,它们可以被取消,并且可以用来封装副作用
  3. Future 所代表的异步计算可以通过消费Future转化为其最终值。
  4. 但是,不消耗 Future 对象更有意思,因为只要我们有未被消耗的 Future 实例,我们就可以以有趣且有用的方式转换组合和操作它们。
  5. Future 对象有一个类型安全的失败分支,用于描述、处理和从运行时 I/O 故障中恢复。TypeError 和 bug 不应该出现在这里,它们只能在 Future 被使用时处理。

这就是关于 Fluture 的全部内容了。祝您阅读愉快!

文章来源:https://dev.to/avaq/fluture-a-functions-alternative-to-promises-21b