You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在严格求值语言中实现受控递归?以JS列表拼接为例

Fixing Stack Overflow in Scott-Encoded List Append for JavaScript

Let's break down why your current implementation hits a stack overflow with large lists, how Haskell avoids this, and how we can adapt this behavior to strict JavaScript.

Why Your Current JS Implementation Overflows

Your append implementation for List makes immediate recursive calls when building each Cons node:

appendAdd("List/List", tx => ty => tx.runList({
  Nil: ty,
  Cons: x => tx_ => Cons(x) (append(tx_) (ty)) // Immediate recursive call
}));

For a large list (say 10,000 elements), this creates 10,000 nested recursive calls before returning the final list. JavaScript's call stack has limited depth, so this quickly leads to a stack overflow.

How Haskell's (++) Avoids This

Haskell's (++) relies on two critical features:

  • Lazy Evaluation: The expression xs ++ ys isn't computed until its value is actually needed (like when folding or iterating over the list).
  • Tail Recursion modulo Cons: While (x:xs) ++ ys = x : (xs ++ ys) isn't strictly tail-recursive (the recursive call is wrapped in a constructor), laziness means xs ++ ys is stored as an unevaluated thunk instead of being computed right away. We only evaluate one step at a time when accessing elements, keeping the call stack flat.

Implementing Lazy List Append in JavaScript

To replicate this behavior in strict JavaScript, we need to introduce explicit laziness by wrapping recursive calls in thunks (zero-argument functions). Here's how to adjust your implementation:

Step 1: Modify List Constructors for Laziness

Update Cons to accept a thunk for the tail (instead of an already-evaluated List), and add a helper to force thunks when needed:

const FORCE = Symbol("FORCE");

// Modified Cons: tail is a thunk
const Cons = x => txThunk => List("Cons")(cases => cases.Cons(x)(txThunk));
const Nil = List("Nil")(cases => cases.Nil);

// Helper to force a thunk (resolves nested thunks recursively)
const force = tx => {
  while (typeof tx === "function" && FORCE in tx) {
    tx = tx();
  }
  return tx;
};

Step 2: Adjust Append to Use Lazy Recursion

Update the append implementation to return a thunk for the recursive part instead of calling it immediately:

appendAdd("List/List", tx => ty => {
  // Wrap append logic in a thunk to delay evaluation
  const appendThunk = () => tx.runList({
    Nil: ty,
    Cons: x => tx_ => Cons(x)(() => append(tx_)(ty)) // Recursive call is a thunk
  });
  // Mark the thunk so our force helper recognizes it
  appendThunk[FORCE] = true;
  return appendThunk;
});

Step 3: Update Fold to Handle Lazy Thunks

Modify foldr to force thunks as it traverses the list:

const foldr = f => acc => {
  const aux = tx => {
    const forcedTx = force(tx);
    return forcedTx.runList({
      Nil: acc,
      Cons: x => txThunk => f(x)(aux(txThunk))
    });
  };
  return aux;
};

Full Working Example

Here's the complete adjusted code, tested with large lists:

// type constructor
const Type = name => {
  const Type = tag => Dcons => {
    const t = new Tcons();
    Object.defineProperty(t, `run${name}`, { value: Dcons });
    t[TAG] = tag;
    return t;
  };
  const Tcons = Function(`return function ${name}() {}`)();
  Tcons.prototype[Symbol.toStringTag] = name;
  return Type;
};
const TAG = Symbol("TAG");
const FORCE = Symbol("FORCE");

const List = Type("List");

// Lazy data constructors
const Cons = x => txThunk => List("Cons")(cases => cases.Cons(x)(txThunk));
const Nil = List("Nil")(cases => cases.Nil);

// Helper to force lazy thunks
const force = tx => {
  while (typeof tx === "function" && FORCE in tx) {
    tx = tx();
  }
  return tx;
};

// overload binary functions
const overload2 = (name, dispatch) => {
  const pairs = new Map();
  return {
    [`${name}Add`]: (k, v) => pairs.set(k, v),
    [`${name}Lookup`]: k => pairs.get(k),
    [name]: x => y => {
      if (typeof x === "function" && (VALUE in x)) x = x(y);
      else if (typeof y === "function" && (VALUE in y)) y = y(x);
      const r = pairs.get(dispatch(x, y));
      if (r === undefined) throw new TypeError("No implementation found for the given types");
      else if (typeof r === "function") return r(x)(y);
      else return r;
    }
  };
};

const dispatcher = (...args) => args.map(arg => {
  const tag = Object.prototype.toString.call(arg);
  return tag.slice(tag.lastIndexOf(" ") + 1, -1);
}).join("/");

// Semigroup "typeclass"
const { appendAdd, appendLookup, append } = overload2("append", dispatcher);

// Lazy List instance for Semigroup
appendAdd("List/List", tx => ty => {
  const appendThunk = () => tx.runList({
    Nil: ty,
    Cons: x => tx_ => Cons(x)(() => append(tx_)(ty))
  });
  appendThunk[FORCE] = true;
  return appendThunk;
});

// Lazy-aware foldr
const foldr = f => acc => {
  const aux = tx => {
    const forcedTx = force(tx);
    return forcedTx.runList({
      Nil: acc,
      Cons: x => txThunk => f(x)(aux(txThunk))
    });
  };
  return aux;
};

// Test with a large list (10,000 elements)
const buildLargeList = n => {
  let list = Nil;
  for (let i = n; i > 0; i--) {
    list = Cons(i)(() => list);
  }
  return list;
};

const tx = buildLargeList(10000);
const ty = Cons(99999)(() => Nil);
const tz = append(tx)(ty);

// This won't stack overflow now
console.log(foldr(x => acc => `${x}, ${acc}`)("end")(tz));

Key Takeaways

  • Explicit Laziness: By wrapping recursive calls in thunks, we delay evaluation until the list is actually traversed (e.g., during folding), avoiding deep call stacks during append.
  • Thunk Forcing: Our force helper ensures we only evaluate thunks as needed, keeping the stack depth constant (O(1)) during traversal.
  • Tail Recursion Modulo Cons: This approach mimics Haskell's behavior—we build a structure that defers recursive work until later, rather than doing it all upfront.

内容的提问来源于stack exchange,提问作者user6445533

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 07:17:59