如何为continuation monad实现stack-safe chainRec操作符?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何为continuation monad实现stack-safe chainRec操作符?相关的知识,希望对你有一定的参考价值。

我目前正在试验延续monad。 Cont实际上在javascript中很有用,因为它从回调模式中抽象出来。

当我们处理monadic递归时,总是存在堆栈溢出的风险,因为递归调用不在尾部位置:

const chain = g => f => k =>
  g(x => f(x) (k));

const of = x => k =>
  k(x);
  
const id = x =>
  x;

const inc = x =>
  x + 1;

const repeat = n => f => x => 
  n === 0
    ? of(x)
    : chain(of(f(x))) (repeat(n - 1) (f));

console.log(
  repeat(1e6) (inc) (0) (id) // stack overflow
);

但是,即使我们能够将某些情况转换为尾递归,我们仍然注定要失败,因为Javascript没有TCO。因此,我们必须在某个时刻回到循环。

puresrcipt有一个MonadRec类型类,带有tailRecM运算符,可以为某些monad进行尾递归monadic计算。所以我尝试在Javascript中实现chainRec主要是根据幻想的土地规范:

const chain = g => f => k => g(x => f(x) (k));
const of = x => k => k(x);
const id = x => x;

const Loop = x =>
  ({value: x, done: false});

const Done = x =>
  ({value: x, done: true});

const chainRec = f => x => {
  let step = f(Loop, Done, x);

  while (!step.done) {
    step = f(Loop, Done, step.value);
  }

  return of(step.value);
};

const repeat_ = n => f => x => 
  chainRec((Loop, Done, [n, x]) => n === 0 ? Done(x) : Loop([n - 1, f(x)])) ([n, x]);

console.log(
  repeat_(1e6) (n => n + 1) (0) (id) // 1000000
);

这有效,但它看起来很像作弊,因为它似乎绕过了monadic链接,从而绕过了Cont的背景。在这种情况下,上下文只是“计算的其余部分”,即。反向的函数组成,结果返回预期值。但它对任何单子都有效吗?

为了清楚我的意思,请查看以下outstanding answer的代码片段:

const Bounce = (f,x) => ({ isBounce: true, f, x })

const Cont = f => ({
  _runCont: f,
  chain: g =>
    Cont(k =>
      Bounce(f, x =>
        Bounce(g(x)._runCont, k)))
})

// ...

const repeat = n => f => x => {
  const aux = (n,x) =>
    n === 0 ? Cont.of(x) : Cont.of(f(x)).chain(x => aux(n - 1, x))
  return runCont(aux(n,x), x => x)
}

这里chain以某种方式结合到递归算法中,即可以发生monadic效应。不幸的是,我无法破译此运算符或将其与堆栈不安全版本协调(但Bounce(g(x)._runCont, k)似乎是f(x) (k)部分)。

最后,我的问题是,如果我搞砸了chainRecor的实施,误解了FL规范或者两者都没有,或者都没有?


[编辑]

通过从不同的角度看问题并且值得接受,两个给出的答案都非常有用。因为我只能接受一个 - 嘿stackoverflow,世界并不那么简单! - 我不接受任何。

答案

我是否搞砸了chainRec的实施,或误解了FantasyLand规格,或两者兼而有之?

可能是两者,或者至少是第一部分。请注意the type should be

chainRec :: ChainRec m => ((a -> c, b -> c, a) -> m c, a) -> m b

其中mContc是你的完成/循环包装在ab

chainRec :: ((a -> DL a b, b -> DL a b, a) -> Cont (DL a b), a) -> Cont b

但是你的chainRecrepeat实现根本不使用连续性!

如果我们只实现那种类型,而不需要它应该需要恒定的堆栈空间,它看起来就像

const chainRec = f => x => k =>
  f(Loop, Done, x)(step =>
    step.done
      ? k(step.value) // of(step.value)(k)
      : chainRec(f)(step.value)(k)
  );

或者如果我们甚至放弃懒惰要求(类似于将chaing => f => k => g(x => f(x)(k))转换为g => f => g(f)(即g => f => k => g(x => f(x))(k))),它看起来像

const chainRec = f => x =>
  f(Loop, Done, x)(step =>
    step.done
      ? of(step.value)
      : chainRec(f)(step.value)
  );

甚至掉落Done / Loop

const join = chain(id);
const chainRec = f => x => join(f(chainRec(f), of, x));

(我希望我不会因此而走得太远,但它完美呈现了ChainRec背后的想法)

有了懒惰的延续和非递归的蹦床,我们会写

const chainRec = f => x => k => {
  let step = Loop(x);
  do {
    step = f(Loop, Done, step.value)(id);
//                                  ^^^^ unwrap Cont
  } while (!step.done)
  return k(step.value); // of(step.value)(k)
};

循环语法(初始化stepf调用,do/while而不是do)并不重要,你的也很好,但重要的是f(Loop, Done, v)返回一个延续。

我将把repeat的实现作为练习留给读者:D (提示:如果你有重复的函数f已经使用continuation,它可能会变得更有用,也更容易正确)

另一答案

最好的祝福,

我想这可能就是你要找的东西,

const chainRec = f => x =>
  f ( chainRec (f)
    , of
    , x
    )

实现repeat就像你拥有它一样 - 有两个例外(感谢@Bergi来捕捉这个细节)。 1,loopdone是链接函数,因此chainRec回调必须返回延续。 2,我们必须使用run标记一个函数,所以cont知道什么时候我们可以安全地折叠挂起的延续堆栈 - 粗体更改

const repeat_ = n => f => x =>
  chainRec
    ((loop, done, [n, x]) =>
       n === 0
         ? of (x) (done)                // cont chain done
         : of ([ n - 1, f (x) ]) (loop) // cont chain loop
    ([ n, x ])

const repeat = n => f => x =>
  repeat_ (n) (f) (x) (run (identity))

但是,如果你像我们这里一样使用chainRec,当然没有理由定义中间repeat_。我们可以直接定义repeat

const repeat = n => f => x =>
  chainRec
    ((loop, done, [n, x]) =>
       n === 0
         ? of (x) (done)
         : of ([ n - 1, f (x) ]) (loop)
    ([ n, x ])
    (run (identity))

现在它可以工作,你只需要一个堆栈安全的延续monad - cont (f)构建一个延续,等待行动g。如果grun标记,那么它是时候在trampoline上反弹了。否则构造函数一个新的延续,为callf添加连续的g

// not actually stack-safe; we fix this below
const cont = f => g =>
  is (run, g)
    ? trampoline (f (g))
    : cont (k =>
        call (f, x =>
          call (g (x), k)))

const of = x =>
  cont (k => k (x))

在我们进一步讨论之前,我们将验证一切正常

const TAG =
  Symbol ()

const tag = (t, x) =>
  Object.assign (x, { [TAG]: t })
  
const is = (t, x) =>
  x && x [TAG] === t

// ----------------------------------------

const cont = f => g =>
  is (run, g)
    ? trampoline (f (g))
    : cont (k =>
        call (f, x =>
          call (g (x), k)))
  
const of = x =>
  cont (k => k (x))

const chainRec = f => x =>
  f ( chainRec (f)
    , of
    , x
    )
  
const run = x =>
  tag (run, x)
  
const call = (f, x) =>
  tag (call, { f, x })  

const trampoline = t =>
{
  let acc = t
  while (is (call, acc))
    acc = acc.f (acc.x)
  return acc
}

// ----------------------------------------

const identity = x =>
  x
  
const inc = x =>
  x + 1

const repeat = n => f => x =>
  chainRec
    ((loop, done, [n, x]) =>
       n === 0
         ? of (x) (done)
         : of ([ n - 1, f (x) ]) (loop))
    ([ n, x ])
    (run (identity))
      
console.log (repeat (1e3) (inc) (0))
// 1000

console.log (repeat (1e6) (inc) (0))
// Error: Uncaught RangeError: Maximum call stack size exceeded

以上是关于如何为continuation monad实现stack-safe chainRec操作符?的主要内容,如果未能解决你的问题,请参考以下文章

详解函数式编程之Monad

如何为缩略图悬停添加标题 - Bootstrap?

如何为 Kestrel 主机添加 NTLM 支持?

如何为单选按钮列表创建数据源?

如何为我使用 QNetworkAccessManager 上传的图像设置 QNetworkRequest::ContentTypeHeader 和 QNetworkRequest::ContentLe

函数式夜点心:Monad