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

时间:2018-02-24 20:57:08

标签: javascript functional-programming monads tail-recursion continuation-passing

我目前正在尝试使用continuation 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的上下文。在这种情况下,上下文只是“计算的其余部分”,即。反向的函数组成,结果返回预期值。但是它适用于任何monad吗?

要明确我的意思,请查看以下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)部分。)

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

[编辑]

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

2 个答案:

答案 0 :(得分:3)

祝福,

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

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

实现repeat就像你拥有它一样 - 有两个例外(感谢@Bergi来捕捉这个细节)。 1,loopdone 链接函数,因此chainRec回调必须返回延续。 2,我们必须使用run 标记一个函数,以便cont知道何时我们可以安全地折叠待处理续集的堆栈 - 粗体 < / p>

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。如果g标有run,则可以在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

哪里有错误?

提供的两个实现包含一个关键的区别。具体来说,它是展平结构的g(x)._runCont位。这个任务使用Cont的JS对象编码是微不足道的,因为我们可以通过简单地阅读._runCont

g(x)属性来展平
const Cont = f =>
  ({ _runCont: f
   , chain: g =>
       Cont (k =>
         Bounce (f, x =>
          // g(x) returns a Cont, flatten it
          Bounce (g(x)._runCont, k))) 
   })

在我们的新编码中,我们使用函数来表示cont,除非我们提供另一个特殊信号(就像我们对run所做的那样),否则没有在部分应用f之后访问cont之外的方式 - 请查看以下g (x)

const cont = f => g =>
  is (run, g)
    ? trampoline (f (g))
    : cont (k =>
        call (f, x =>
          // g (x) returns partially-applied `cont`, how to flatten?
          call (g (x), k))) 

在上方,g (x)将返回部分应用的cont,(即cont (something)),但这意味着整个cont函数可以无限嵌套。而不是cont - 包裹something,我们想要something

我花在这个答案上的时间至少有50%已经提出了各种方法来展平部分应用的cont。这个解决方案不是特别优雅,但它确实完成了工作并精确地突出了需要发生的事情。我真的很想知道你可能会找到其他编码 - 粗体

的变化
const FLATTEN =
  Symbol ()

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

所有在线系统,队长

使用cont展平补丁,其他一切都有效。现在看chainRec做一百万次迭代......

const TAG =
  Symbol ()

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

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

const FLATTEN =
  Symbol ()

const cont = f => g =>
  g === FLATTEN
    ? f
    : is (run, g)
      ? trampoline (f (g))
      : cont (k =>
          call (f, x =>
            call (g (x) (FLATTEN), 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 (1e6) (inc) (0))
// 1000000

cont的演变

当我们在上面的代码中引入cont时,如何推导出这样的编码并不是很明显。我希望对此有所了解。我们从希望我们如何定义cont

开始
const cont = f => g =>
  cont (comp (g,f))

const comp = (f, g) =>
  x => f (g (x))

在这种形式下,cont将无休止地推迟评估。我们可以执行的可用的事情是g总是创建另一个cont并推迟我们的行动。我们添加一个逃生舱,run,向cont发出信号,表示我们不想再推迟。

const cont = f => g =>
  is (run, g)
    ? f (g)
    : cont (comp (g,f))

const is = ...

const run = ...

const square = x =>
  of (x * x)

of (4) (square) (square) (run (console.log))
// 256

square (4) (square) (run (console.log))
// 256

上面,我们可以看到cont如何表达美丽而纯粹的节目。但是,如果在没有尾部调用消除的环境中,这仍然允许程序构建超出评估者堆栈限制的延迟函数序列。 comp直接链接函数,因此不在图中。相反,我们将使用我们自己制作的call机制对函数进行排序。当程序发出run信号时,我们会使用trampoline来解除调用堆栈。

下面,我们会看到应用展平修复之前的表格

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

const trampoline = ...

const call = ...

一厢情愿

我们上面使用的另一种技术是我的最爱之一。当我写is (run, g)时,我不知道我将如何立即代表isrun,但我可以稍后解决。我对trampolinecall使用了同样的一厢情愿。

我指出这一点,因为这意味着我可以将所有这些复杂性保留在cont之外,只关注其基本结构。我最终得到了一组给我这种“标记”行为的函数

// tag contract
// is (t, tag (t, value)) == true

const TAG =
  Symbol ()

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

const is = (t, x) =>
  x && x [TAG] === t

const run = x =>
  tag (run, x)

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

一厢情愿的想法就是编写你想要的程序,让你的愿望成真。一旦你满足了你的所有愿望,你的程序就会神奇地起作用!

答案 1 :(得分:1)

  

我是否搞砸了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)
};

循环语法(使用step调用初始化fdo/while代替do)并不重要,你的也很好,但重要部分是f(Loop, Done, v)返回延续。

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