问题
I implemented a Scott encoded List
type in Javascript along with an overloaded append
function that mimics the Semigroup
typeclass.
append
works just fine but for large lists it will blow the stack. Here is the decisive part of my implementation:
appendAdd("List/List", tx => ty => tx.runList({
Nil: ty,
Cons: x => tx_ => Cons(x) (append(tx_) (ty))
}));
Usually I use a trampoline to avoid a growing stack, but this presupposes tail recursion and thus won't work in this case.
Since this implementation is based on Haskell's, I guess lazy evaluation and guarded recursion/tail recursion modulo cons make the difference:
(++) [] ys = ys
(++) (x:xs) ys = x : xs ++ ys
Provided I understand it correctly, due to lazy evaluation (almost) nothing happens in Haskell when I append a list to another, until I actually do something with this new list. In the example below I fold it.
What I don't understand is how guarded recursion can keep the recursion from growing the call stack and whether this behavior can be implemented explicitly in a strictly evaluated language like Javascript.
Hopefully this question isn't too broad.
For a better understanding, here is the full implementation/example:
// 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 List = Type("List");
// data constructors
const Cons = x => tx => List("Cons") (cases => cases.Cons(x) (tx));
const Nil = List("Nil") (cases => cases.Nil);
// 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("...");
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);
// List instance for Semigroup
appendAdd("List/List", tx => ty => tx.runList({
Nil: ty,
Cons: x => tx_ => Cons(x) (append(tx_) (ty))
}));
// fold
const foldr = f => acc => {
const aux = tx =>
tx.runList({
Nil: acc,
Cons: x => tx_ => f(x) (aux(tx_))})
return aux;
};
// data
const tx = Cons(1) (Cons(2) (Nil));
const ty = Cons(3) (Cons(4) (Nil));
const tz = append(tx) (ty);
// run
console.log(
foldr(x => acc => `${x}${acc}`) (0) (tz) // "12340"
);
回答1:
This isn't a real answer but conclusions I drew after further study:
- Tail Recursion modulo Cons - TRMC (and "modulo" for other operations) refers only to a strictly evaluated context, whereas guarded recursion refers to a lazy evaluated one
- TRMC is an expensive compiler technique and it (probably) doesn't make sense to implement it in userland
- TRMC requires the operation to be associative (form at least a Semigroup), so that the parentheses can be rearranged
This Q&A is also helpful: a tail-recursion version list appending function .
来源:https://stackoverflow.com/questions/50288939/how-to-implement-guarded-recursion-in-strictly-evaluated-languages