延续抽象副作用:Gleam 中延续的应用及业务逻辑抽象实现

延续抽象副作用:Gleam 中延续的应用及业务逻辑抽象实现 用延续Continuations抽象副作用将错误表示为值是编写可靠程序的强大工具。结果值可以包装所需的值也可以包含计算失败原因的信息。显式的结果类型在类型系统中体现了失败的可能性返回结果的函数会迫使调用者处理失败情况。结果类型是一个更广泛模式的具体实例Promise 表示异步计算返回一个 Promise 时调用者必须等待计算完成才能访问值。结果类型和 Promise 各自跟踪计算的特定细节若要对计算细节进行泛化可以使用延续。使用延续意味着“我不清楚你将如何获取值但当你获取到之后接下来应该这样处理。”Filinski 在 1994 年发表的论文《Representing Monads》中证明延续可以表示任何单子比如结果类型和 Promise。本文不会深入探讨证明的数学原理而是展示如何在 Gleam 中使用延续。一个示例我们使用一个 fetch 函数它根据键返回对应的值。这个函数由调用者提供接受一个 String 类型的键并返回一个 String 类型的值。将 fetch 作为参数传入能让我们根据需要从不同数据源中获取值。一个简单的 fetch 实现可以忽略键始终返回 yes。业务逻辑如下对于一个固定的键列表将每个键转换为大写然后返回关联值的长度。企业级需求我们的 simple_func 运行良好业务开始拓展。业务逻辑保持不变但企业客户的数据存储方式多种多样。有些 fetch 实现无法总是返回值。为了处理这种情况我们创建了一个新的业务逻辑版本其中 fetch 返回一个 Result(String, Nil)。这个新函数取得了不错的效果不久后又有了对异步数据存储的需求。另一个接受返回 Promise(String) 的 fetch 函数的实现解决了这个问题。此时我们有了三个实现来满足所有不同的需求。每个版本的业务逻辑相同函数也很相似但我们没有在不同实现之间复用业务逻辑。更糟糕的是我们还面临着对可能失败的异步实现的需求即 fetch 返回 Promise(Result(String, Nil))。我们无法复用之前的可能失败或异步的实现需要为函数编写第四个实现。缺失的抽象不同实现之间的差异仅在于 fetch 执行的计算类型直接计算、可能失败的计算、异步计算或其他类型。每个实现的共同点是如何创建键以及在值可用时如何处理它。延续允许我们表示这种“前后”关系同时对计算类型进行泛化。我们创建一个新的版本接受返回 Continuation(t, String) 的 fetch。这个任务现在对计算类型进行了泛化。调用者将通过提供的 fetch 实现来选择 t 的具体类型。为了提取最终值调用者需要提供一个最终回调函数。基于延续的任务的调用者通常被称为运行器或解释器。简单运行器在简单情况下fetch 没有额外的副作用并且总是返回一个字符串。它仍然返回一个延续因此最终值用 continuation.return 包装。由于最终值会被解包所以将恒等函数作为最终回调函数传入。这个简单运行器返回一个 List(Int)表明 fetch 本身是可靠且同步的。可能失败的运行器当 fetch 返回 Result(String, Nil) 时我们的最终回调函数也必须返回一个结果。因此最终回调函数用 Ok 包装值。then 是恢复任务的回调函数它在值存在时被调用。异步运行器当 fetch 是异步时运行器将 t 选为 Promise(List(Int))。最终回调函数使用 promise.resolve 来匹配 t。结论有了 task我们对业务逻辑有了单一的定义。它描述了如何使用值而不考虑值是如何获取的。运行器决定如何获取值。任何以这种方式编写的任务都可以使用相同的运行器。这种逻辑和副作用的分离就是延续为我们提供的抽象。我正在构建 EYG这是一个关于构建更好的语言和工具的实验这里的“更好”是有一定衡量标准的。所有进展都会在我的不定期时事通讯中汇报。