可以通过monadic类型实现延迟评估吗?

时间:2017-05-31 16:01:33

标签: javascript haskell functional-programming lazy-evaluation

我目前正在研究与Javascript中的monad一起进行的懒惰评估,以及哪些用例可能会从这些中发展出来。所以我尝试实现一个lazy类型,它实现了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中的monadic动作序列吗?
  • 是一种懒惰的评估,是一种在严格评估的语言中monad无法实现的效果吗?

我甚至不确定我的研究是否有任何意义,所以请随时投票支持关闭这个问题。

3 个答案:

答案 0 :(得分:3)

这取决于“实施懒惰评估”的含义。你当然可以制作一个“延迟”类型,它将是一个单子。但通常我们认为类型A -> State S B的函数是“从AB的有状态函数”。对于像A -> Delay B这样的东西,似乎对于论证A我们已经“强迫”了它。看起来我们真的想要更像Delay A -> Delay B的东西。

事实证明,有多种方法可以将表达式转换为monadic样式。一种按值调用的方式,这是常用的方式,也是一种按名称调用的方式。 Phil Wadler在1992年的论文Comprehending MonadsPDF)中对此进行了讨论。毫不奇怪,这些与类似的事实有关,即有两种转换为延续传递方式(CPS):按值调用的一种和按名称调用的一种。实际上,这些正是使用continuation monad进行的call-by-value / -name monadic样式转换。 CPS的目的是将目标,实施语言的评估顺序与源语言的评估顺序分开。如果使用按值调用的CPS转换来实现源语言,则无论目标语言的评估顺序是什么,它都将具有按值调用的语义。类似地,如果使用按名称调用CPS转换,则无论目标语言的评估顺序如何,您都将获得按名称调用语义。

我不确切地知道在使用Delay monad进行按值调换时会发生什么事情,但我怀疑它通常会“稍微”关闭并“纠正”它将更多地转向名称翻译。

答案 1 :(得分:1)

  

这显然毫无意义,因为动作序列根本不是懒惰的。 Lazy.join只是过早地触发评估

是。所以不要这样做:

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

(尽管你可以,并且出于性能原因可能会丢弃Lazy包装或制作Lazy = id

  

总是急切地评估Haskell中的monadic动作序列吗?

不,在你告诉它之前,Haskell并没有评估任何事情。 Monads也不例外,它们的工作方式与其他数据类型相同。

  

是一种懒惰的评估效果,monad在严格评估的语言中无法实现这种效果吗?

不,它完美无缺。

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

答案 2 :(得分:1)

玩耍

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

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

你的join被打破了 - 尽管看起来很反直觉,你必须确保包装解包过程!把它想象成添加一个包装器,但删除两个;你仍然删除了一层嵌套,这实现了预期的结果

您的monadic return(或本例中为of)也被打破; return并不总是与您的数据构造函数相同 - 即Lazy.of(2)应该等同于Lazy($ => 2)

你的代码激励了我,所以我修改了它,直到我结束了这一点。我想你会很高兴^ _ ^ Bergi也建议Lazy不应重新评估其结果。我在runLazy方法中使用安全备忘录处理了这个问题。对此的反馈将被赞赏< 3

  

个人代码风格

     

作为惯例,我将thunk写为$ => expr而不是() => expr。在JavaScript中编写函数式程序时,最终会出现很多() 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身上,但也许你或其他人在那里有一些想法!

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

<强>函子

// 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())
}

<强>含半幺群

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

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

开放式探索

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