【问题标题】:How can I prevent a tail recursive function from reversing the order of a List?如何防止尾递归函数反转列表的顺序?
【发布时间】:2023-04-08 14:02:02
【问题描述】:

我正在尝试函数式List 类型和结构共享。由于 Javascript 没有 Tail Recursive Modulo Cons 优化,我们不能像这样写 List 组合子,因为它们不是堆栈安全的:

const list =
  [1, [2, [3, [4, [5, []]]]]];


const take = n => ([head, tail]) =>
  n === 0 ? []
    : head === undefined ? []
    : [head, take(n - 1) (tail)];


console.log(
  take(3) (list) // [1, [2, [3, []]]]
);

现在我尝试递归实现take tail,这样我既可以依赖 TCO(在 Ecmascript 中仍然是一个不稳定的Promise),也可以使用蹦床(为了简单起见,示例中省略了):

const list =
  [1, [2, [3, [4, [5, []]]]]];


const safeTake = n => list => {
  const aux = (n, acc, [head, tail]) => n === 0 ? acc
    : head === undefined ? acc
    : aux(n - 1, [head, acc], tail);

  return aux(n, [], list);
};


console.log(
  safeTake(3) (list) // [3, [2, [1, []]]]
);

这可行,但新创建的列表顺序相反。如何以纯粹的功能方式解决此问题?

【问题讨论】:

  • 您是否要求明确实现“尾调用模数”实现?
  • 最简单的解决方案是来自safeTakereturn reverse(aux(n, [], list))
  • @Bergi 我昨天在another question 上询问了一个明确的TRMC。
  • @Bergi 我个人认为缺少 TRMC 是 JS 的最大缺点之一(更​​不用说 TCO)。但是话又说回来,我不知道任何实现这种优化的动态语言。
  • JS 无论如何都没有不可变的链表,所以它几乎不需要 TRMC。我猜你真的想改用 PureScript :-)

标签: javascript recursion functional-programming tail-recursion


【解决方案1】:

懒惰免费为您提供尾递归模缺点。因此,显而易见的解决方案是使用 thunk。但是,我们不只是想要任何类型的 thunk。我们想要weak head normal form 中的表达式的thunk。在 JavaScript 中,我们可以使用lazy getters 来实现这一点,如下所示:

const cons = (head, tail) => ({ head, tail });

const list = cons(1, cons(2, cons(3, cons(4, cons(5, null)))));

const take = n => n === 0 ? xs => null : xs => xs && {
    head: xs.head,
    get tail() {
        delete this.tail;
        return this.tail = take(n - 1)(xs.tail);
    }
};

console.log(take(3)(list));

使用惰性 getter 有很多优点:

  1. 普通属性和惰性属性的使用方式相同。
  2. 您可以使用它来创建无限的数据结构。
  3. 您不必担心会炸毁堆栈。

希望对您有所帮助。

【讨论】:

  • 这确实很有帮助! WHNF 是 Haskell 社区中有时被称为保护递归的底层机制吗?我一直认为受保护的递归将是一个复杂的编译器优化。但现在看来,这本质上是懒惰的副作用。
  • WHNF 不是一种机制。懒惰是机制。一种思考方式是,如果 laziness 是动词,那么 WHNF 是名词。 WHNF 中的表达式可能只被评估到最外层的构造函数。构造函数的字段可能尚未评估(由于懒惰)。因此,惰性允许表达式在 WHNF 中。反过来,WHNF 通过隐式蹦床启用受保护的递归。因此,通过传递性,惰性可以免费为您提供受保护的递归。
  • 但是请注意,受保护的递归不需要惰性。例如,您可以在严格的语言(如 Scheme)中使用受保护的递归。在这种情况下,受保护的递归(即尾递归模 cons)确实是一种复杂的编译器优化。
【解决方案2】:

防止列表反转的一种方法是使用延续传递样式。现在只需将它放在您选择的蹦床上......

const None =
  Symbol ()

const identity = x =>
  x

const safeTake = (n, [ head = None, tail ], cont = identity) =>
  head === None || n === 0
    ? cont ([])
    : safeTake (n - 1, tail, answer => cont ([ head, answer ]))

const list =
  [ 1, [ 2, [ 3, [ 4, [ 5, [] ] ] ] ] ]

console.log (safeTake (3, list))
// [ 1, [ 2, [ 3, [] ] ] ] 

这是在蹦床上

const None =
  Symbol ()

const identity = x =>
  x

const call = (f, ...values) =>
  ({ tag: call, f, values })

const trampoline = acc =>
{
  while (acc && acc.tag === call)
    acc = acc.f (...acc.values)
  return acc
}

const safeTake = (n = 0, xs = []) =>
{
  const aux = (n, [ head = None, tail ], cont) =>
    head === None || n === 0
      ? call (cont, [])
      : call (aux, n - 1, tail, answer =>
          call (cont, [ head, answer ]))
  return trampoline (aux (n, xs, identity))
}

const list =
  [ 1, [ 2, [ 3, [ 4, [ 5, [] ] ] ] ] ]

console.log (safeTake (3, list))
// [ 1, [ 2, [ 3, [] ] ] ] 

【讨论】:

  • 当我用loop/recur 实现safeTake 并将它应用到一个巨大的列表时,我在answer => cont ([ head, answer ]) 得到一个堆栈溢出。你能重现吗?
  • @ftor,我在上面添加了一个蹦床演示。还记得chainRec 的问题吗?我认为可以使用我们的堆栈安全延续 monad 或使用堆栈安全 unfold 来重写它
  • 嗯,这似乎也导致了堆栈溢出:your example。这是几个小时前的my shot。我不太明白,但似乎延续结束了一个真正的调用堆栈。
  • @ftor 将call 添加到aux 的所有返回路径中——这是it works 使用不太聪明的测试的验证:D
  • 谢谢,我无法解决这个问题。我很讨厌 CPS。
猜你喜欢
  • 2021-05-31
  • 2019-09-30
  • 1970-01-01
  • 1970-01-01
  • 2019-09-14
  • 2019-06-25
  • 2021-04-24
  • 1970-01-01
  • 2012-06-15
相关资源
最近更新 更多