0

我有兴趣在我的 JavaScript 函数组合中尝试类似 Haskell 的 IO monad。

FolktaleTask似乎与 Haskell 的 IO 相似,因为它是惰性的,因此在技术上是纯粹的。它代表将来可能发生的动作。但我有几个问题。

当所有后面的函数都依赖于组合中初始不纯函数的返回值时,如何形成函数组合?必须首先运行实际的任务,将返回的数据隐式传递给更远的函数。一个人不能只是传递一个未解决的任务来做任何有用的事情,或者可以吗?它看起来像。

compose(doSomethingWithData, getDataFromServer.run());

我可能遗漏了一些关键的东西,但这样做有什么好处?

一个相关的问题是对不纯函数的惰性求值有什么具体优势?当然,它提供了引用透明性,但理解问题的核心是不纯函数返回的数据结构。后面所有通过管道传输数据的函数都依赖于数据。那么不纯函数的引用透明性对我们有什么好处呢?

编辑:所以在查看了一些答案之后,我能够通过链接轻松地编写任务,但我更喜欢使用 compose 函数的人体工程学。这行得通,但我想知道它是否对函数式程序员来说是惯用的:

const getNames = () =>
  task(res =>
    setTimeout(() => {
      return res.resolve([{ last: "cohen" }, { last: "kustanowitz" }]);
    }, 1500)
);

const addName = tsk => {
  return tsk.chain(names =>
    task(resolver => {
      const nms = [...names];
      nms.push({ last: "bar" });
      resolver.resolve(nms);
    })
  );
};
const f = compose(
  addName,
  getNames
);

const data = await f()
  .run()
  .promise();
// [ { last: 'cohen' }, { last: 'kustanowitz' }, { last: 'bar' } ]

然后,另一个可能与风格更相关的问题是,现在我们必须编写所有处理任务的函数,这似乎不如那些处理数组/对象的函数那么优雅和通用。

4

2 回答 2

1

当所有后面的函数都依赖于组合中初始不纯函数的返回值时,如何形成函数组合?

chain方法用于组成 monad。考虑以下裸骨Task示例。

// Task :: ((a -> Unit) -> Unit) -> Task a
const Task = runTask => ({
    constructor: Task, runTask,
    chain: next => Task(callback => runTask(value => next(value).runTask(callback)))
});

// sleep :: Int -> Task Int
const sleep = ms => Task(callback => {
    setTimeout(start => {
        callback(Date.now() - start);
    }, ms, Date.now());
});

// main :: Task Int
const main = sleep(5000).chain(delay => {
    console.log("%d seconds later....", delay / 1000);
    return sleep(5000);
});

// main is only executed when we call runTask
main.runTask(delay => {
    console.log("%d more seconds later....", delay / 1000);
});

必须首先运行实际的任务,将返回的数据隐式传递给更远的函数。

正确的。但是,可以推迟任务的执行。

一个人不能只是传递一个未解决的任务来做任何有用的事情,或者可以吗?

chain正如我上面演示的,您确实可以使用该方法编写尚未开始的任务。

一个相关的问题是对不纯函数的惰性求值有什么具体优势?

这是一个非常广泛的问题。也许您可能会对以下 SO 问题感兴趣。

Lazy I/O 有什么不好?

那么不纯函数的引用透明性对我们有什么好处呢?

引用维基百科[ 1 ]

引用透明性的重要性在于它允许程序员编译器将程序行为作为重写系统进行推理。这有助于证明正确性、简化算法、协助修改代码而不破坏代码,或通过记忆公共子表达式消除惰性求值并行化来优化代码。

于 2019-12-15T15:03:21.687 回答
0

我们如何在 Javascript 中表达 Haskell 的 IO 类型?实际上我们做不到,因为在 Haskell 中 IO 是一种非常特殊的类型,与运行时深度交织在一起。我们可以在 Javascript 中模仿的唯一属性是带有显式 thunk 的惰性求值:

const Defer = thunk => ({
  get runDefer() {return thunk()}
}));

通常懒惰的评估伴随着分享,但为了方便起见,我省略了这个细节。

现在你将如何组成这样的类型?好吧,您需要在 thunk 的上下文中编写它。懒惰地编写 thunk 的唯一方法是嵌套它们,而不是立即调用它们。因此,您不能使用函数组合,它仅提供函数的函数实例。您需要应用程序 ( ap/ of) 和 monad ( chain) 实例Defer来链接或嵌套它们。

applicatives 和 monads 的一个基本特征是你不能逃避它们的上下文,即一旦你的计算结果在 applicatives/monad 中,你就不能再次打开它。*所有后续计算都必须在各自的范围内进行语境。正如我已经Defer在上下文中提到的那样,是 thunk。

因此,最终当您使用ap/组合 thunk 时,chain您将构建一个嵌套的延迟函数调用树,该树仅在您调用runDefer外部 thunk 时进行评估。

这意味着您的 thunk 组合或链接在第一次runDefer调用之前保持纯净。这是我们所有人都应该追求的非常有用的属性。


*当然,您可以在 Javascript 中转义 monad,但它不再是 monad,您将失去所有可预测的行为。

于 2019-12-15T14:27:35.897 回答