【问题标题】:Can lazy evaluation be implemented by a monadic type?单子类型可以实现惰性求值吗?
【发布时间】:2017-11-01 12:57:52
【问题描述】:

我目前正在研究结合 Javascript 中的 monad 的惰性求值以及哪些用例可能会从这些演变而来。所以我尝试实现一个惰性类型,它实现了 functor/monad 类型类。相应的构造函数意味着它的参数和结果是惰性的。这是我想出的:

// a lazy type

// (() -> a) -> () -> b
const Lazy = thunk => () => thunk();

// (b -> a -> b) -> b -> Lazy a -> b
Lazy.fold = f => acc => tx => f(acc) (tx());

// (a -> b) -> Lazy a -> Lazy b
Lazy.map = f => tx => Lazy(() => f(tx()));

// Lazy (a -> b) -> Lazy a -> Lazy b
Lazy.ap = tf => tx => Lazy(() => tf() (tx()));

Lazy.of = Lazy;

// Lazy (Lazy a) -> Lazy a
Lazy.join = ttx => ttx();

// (a -> Lazy b) -> Lazy a -> Lazy b
Lazy.chain = ft => tx => Lazy.join(Lazy.map(ft) (tx));

// recursive bind (or chain in Javascript)

// Number -> (a -> b) -> a -> Lazy b
const repeat = n => f => x => {
  const aux = m => y => m === 0
   ? Lazy(() => y)
   : Lazy.chain(aux(m - 1)) (Lazy(() => f(y)));

  return aux(n) (x);
};

// impure function to observe the computation

const inc = x => (console.log(++x), x);

// and run

console.log(repeat(5) (inc) (0)); // logs 1, 2, 3, 4, 5, () => thunk()

现在这显然没有意义,因为动作序列根本不是惰性的。 Lazy.join 只是过早地触发了评估。因此,出现了以下问题:

  • Haskell 中的一元动作序列是否总是急切地评估?
  • 惰性求值是一种无法由严格求值语言中的 monad 实现的效果吗?

我什至不确定我的研究是否有意义,所以请随意投票以结束这个问题。

【问题讨论】:

  • 我想对所有这些速记变量名进行一些解释
  • join 应该是tx => Lazy(tx())
  • @evolutionxbox tx = 花式类型,ft = 返回花式类型的函数,花式类型 = 表示具有值的上下文的类型(Lazy 的上下文是惰性求值和 thunk 分别。() -> a 是 thunk 的签名,至少在 Javascript 中)
  • 我不知道这是否有帮助,但我喜欢从“相反”的角度思考你的情况。不是“单子可以实现惰性吗?”但是“我制作的这种 thunk 类型是单子的懒惰模型吗?/它有什么代数结构?”从类型开始,然后发现结构。
  • @luqui 这种隐性知识通常很有帮助,谢谢。我想我犯了一个常见的错误(新手),试图将每个效果包装到一个单子中。

标签: javascript haskell functional-programming lazy-evaluation


【解决方案1】:

这显然没有意义,因为动作序列一点也不懒惰。 Lazy.join 只是过早地触发了评估

是的。所以不要那样做:

// (() -> (() -> a)) -> (() -> a)
Lazy.join = ttx => Lazy(() => ttx()());
//                         ^^ this function prevents `ttx` from being evaluated immediately

(尽管出于性能原因,您可以并且可能应该删除 Lazy 包装器或制作 Lazy = id

Haskell 中的一元动作序列是否总是急切地评估?

不,Haskell 不会评估任何东西,直到你告诉它去做。 Monad 也不例外,它们的工作方式与任何其他数据类型一样。

惰性求值是一种无法由严格求值语言中的 monad 实现的效果吗?

不,它工作得很好。

但是,您可能希望注意到您尚未完全实现lazy implementation,这也意味着共享结果而不是重新评估。不过,这确实需要可变性,并且只有在评估函数是纯函数时才能正常工作。

【讨论】:

  • 是的,join 也必须偷懒。
  • "结果共享​​>" 绝对有意义。我将更仔细地研究这一点。你又拯救了我的一天,谢谢!
  • 抱歉,还有一件事:“删除 Lazy 包装器” - 单子不只是类型约束,即需要存在类型吗?如果我放弃(() -> a) -> (() -> b),那会是什么类型?只是函数类型?
  • @ftor 我的意思是调用Lazy 函数,而不是类型包装器(即使在你的情况下,它也更像是一个别名,给定Lazy == id)。使用const Lazy = thunk => ({run: thunk});,您可能会得到更有趣的结果
  • Haskell 中的 {runX :: Type} 只是一种方便的方法来检索底层值,而无需显式定义全局函数。在 Javascript 中,您可以只获取值,但使用.runLazy 可能更清楚调用代码。我认为人们在 Javascript 中定义所有这些 .getX.runY 很有趣,尽管它们不在全局命名空间中。如果它们仅仅是.run/.get,那么可以定义相应的通用吸气剂并写run(tf)get(tx) 而不是tf.runLazy()tx.getConst()
【解决方案2】:

到处玩

你的问题很有趣,我还没有探索过这样的单子,所以我想玩弄它。

正如 Bergi 指出的那样,您的数据构造函数中不需要额外的 thunk 包装器 - 即,Lazy 期望它的参数已经是一个 thunk。

您的join 被破坏了——尽管看起来违反直觉,但您必须确保对解包过程进行了包装!将其视为添加一个包装器,但删除两个;还是去掉一层嵌套,就达到了预期的效果

你的单子return(或of在这种情况下)也坏了; return 并不总是与您的数据构造函数相同——即Lazy.of(2) 应该等同于Lazy($ => 2)

你的代码启发了我,所以我一直在修改它,直到我完成了这个。我想你会很高兴^_^ Bergi 还建议Lazy 不应该重新评估它的结果。我通过runLazy 方法中的安全记忆处理了这个问题。对此的反馈将不胜感激

个人代码风格

按照惯例,我将 thunk 写为 $ => expr 而不是 () => expr。在用 JavaScript 编写函数式程序时,您最终会得到很多 ()s,通常与其他 ()s 相邻,这有时会损害可读性。我认为Lazy($ => f()) 的阅读(至少)略好于Lazy(() => f())。由于这是一个教育网站,我认为这值得一提。我感觉小改动有助于提高可读性,但我也不想让任何人感到困惑。

对于遇到困难的任何人,请随时用() 替换下面代码中的所有$。现在继续...

// data Lazy = Lazy (Unit -> a)
const Lazy = t => ({
  memo: undefined,
  // runLazy :: Lazy a -> Unit -> a
  runLazy () {
    return this.memo === undefined
      // console.log call just for demonstration purposes; remove as you wish
      ? (this.memo = t(), console.log('computed:', this.memo), this.memo)
      : this.memo
  },
  // chain :: Lazy a -> (a -> Lazy b) -> Lazy b
  chain (f) {
    return Lazy($ => f(this.runLazy()).runLazy())
  }
})

// Lazy.of :: a -> Lazy a
Lazy.of = x =>
  Lazy($ => x)
  
// repeat :: Int -> (a -> a) -> a -> Lazy a
const repeat = n => f => x =>
  n === 0
    ? Lazy.of(x)
    : Lazy.of(f(x)).chain(repeat (n-1) (f))

// m :: Lazy Number
const m = repeat (5) (x => x * 2) (1)

console.log('computations pending...')
// ...
console.log(m.runLazy()) // (1 * 2 * 2 * 2 * 2 * 2) => 32
console.log(m.runLazy()) // => 32

至于满足其他类别,这里有更多Lazy 的方法实现。我挂断了 empty 的 Monoid,但也许你或其他人有一些想法!

我还看到你从f => join(map(f)) 派生了chain,这也很好

函子

// map :: Lazy a -> (a -> b) -> Lazy b
map (f) {
  return Lazy($ => f(this.runLazy()))
}

适用

// apply :: (a -> b) -> a -> b
const apply = f => x => f (x)

// ap :: Lazy (a -> b) -> Lazy a -> Lazy b
ap (m) {
  return Lazy($ => apply (this.runLazy()) (m.runLazy()))
}

单子

// Lazy.of :: a -> Lazy a
Lazy.of = x =>
  Lazy($ => x)

// chain :: Lazy a -> (a -> Lazy b) -> Lazy b
chain (f) {
  return Lazy($ => f(this.runLazy()).runLazy())
}

// join :: Lazy (Lazy a) -> Lazy a
join () {
  return Lazy($ => this.runLazy().runLazy())
}

Monoid

// empty
empty () {
  // unsure on this one
}

// concat :: (Monoid a) => Lazy a -> Lazy a -> Lazy a
concat (m) {
  return Lazy($ => this.runLazy().concat(m.runLazy()))
}

开放探索

这个话题对我来说很有趣,所以我很乐意讨论我在这里写的任何内容或任何其他关于这个问题/答案中提出的想法的内容。让我们玩得更开心!

【讨论】:

  • 如果您想要更多乐趣,请尝试在不使用runLazy 的情况下实现mapapjoin,仅针对.chain()of() :-)
  • Lazy 不是幺半群。你在concat 中所做的是实现Monoid a => Monoid(Lazy a),用Haskell 术语来说。但我不认为这真的有用。 (编辑:嗯,Haskell does implement Monoid a => Monoid (Identity a) 和其他类似的东西,很好......)
  • Bergi 有趣的挑战。我今晚试试^^
  • 我相信empty 会是return Lazy($ => this.runLazy().empty())
  • @4castle 没有。箭头函数中没有this,也没有你可以打开的runLazyempty 需要凭空产生一个值。只有当你知道要包装什么数据类型时,你才能编写 monoid 实例 - 然后你可以使用 Lazy.of(A.empty())
【解决方案3】:

这取决于您所说的“实施惰性求值”是什么意思。你当然可以制作一个“延迟”类型,它将是一个单子。但通常我们认为A -> State S B 类型的函数是“从AB 的有状态函数”。对于A -> Delay B 之类的东西,对于A 的论点,我们似乎已经“强制”了它。看来我们真的想要更像Delay A -> Delay B 的东西。

事实证明,有多种方法可以将表达式转换为单子样式。一种按值调用的方式(这是通常的方式)和一种按名称调用的方式。 Phil Wadler 在他 1992 年的论文 Comprehending Monads (PDF) 中对此进行了讨论。毫不奇怪,这些都与一个类似的事实有关,即有两种转换为连续传递风格 (CPS):一种是按值调用,另一种是按名称调用。实际上,这些正是按值调用/名称单子样式的翻译,只是带有延续单子。 CPS 的目的是将目标语言的评估顺序与源语言的评估顺序分开。如果您使用按值调用 CPS 转换来实现源语言,则无论目标语言的评估顺序如何,它都将具有按值调用语义。同样,如果您使用名称调用 CPS 转换,您同样会获得名称调用语义,而不管目标语言的评估顺序如何。

我不知道在使用带有Delay monad 的按值调用翻译时事情会如何发展,但我怀疑它通常会“稍微”关闭并且“纠正”它会移动更倾向于点名翻译。

【讨论】:

  • ...它将更倾向于点名翻译。 你说的完全正确!当你有一个基于 Delay 类型的单子计算并将一个表达式注入单子时,这个表达式会立即被计算,因为对于初始计算步骤,它还没有包含在一个 thunk 中(const of = x => Delay(() => x) where x在 JS 中立即评估)。所以确实是“有点”偏离和“纠正”。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-12-21
  • 1970-01-01
  • 2016-05-13
  • 2011-11-03
  • 2011-06-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多