如何在严格评估的设置中编码 corecursion/codata?
How to encode corecursion/codata in a strictly evaluated setting?
核心递归意味着在每次迭代时调用自己的数据大于或等于之前的数据。 Corecursion 适用于 codata,它们是递归定义的值。不幸的是,值递归在严格评估的语言中是不可能的。不过,我们可以使用显式 thunk:
const Defer = thunk =>
({get runDefer() {return thunk()}})
const app = f => x => f(x);
const fibs = app(x_ => y_ => {
const go = x => y =>
Defer(() =>
[x, go(y) (x + y)]);
return go(x_) (y_).runDefer;
}) (1) (1);
const take = n => codata => {
const go = ([x, tx], acc, i) =>
i === n
? acc
: go(tx.runDefer, acc.concat(x), i + 1);
return go(codata, [], 0);
};
console.log(
take(10) (fibs));
虽然这按预期工作,但这种方法似乎很尴尬。尤其是那对可怕的元组让我很烦。有没有更自然的方式来处理 JS 中的 corecursion/codata?
我会在数据构造函数本身中对 thunk 进行编码。例如,考虑。
// whnf :: Object -> Object
const whnf = obj => {
for (const [key, val] of Object.entries(obj)) {
if (typeof val === "function" && val.length === 0) {
Object.defineProperty(obj, key, {
get: () => Object.defineProperty(obj, key, {
value: val()
})[key]
});
}
}
return obj;
};
// empty :: List a
const empty = null;
// cons :: (a, List a) -> List a
const cons = (head, tail) => whnf({ head, tail });
// fibs :: List Int
const fibs = cons(0, cons(1, () => next(fibs, fibs.tail)));
// next :: (List Int, List Int) -> List Int
const next = (xs, ys) => cons(xs.head + ys.head, () => next(xs.tail, ys.tail));
// take :: (Int, List a) -> List a
const take = (n, xs) => n === 0 ? empty : cons(xs.head, () => take(n - 1, xs.tail));
// toArray :: List a -> [a]
const toArray = xs => xs === empty ? [] : [ xs.head, ...toArray(xs.tail) ];
// [0,1,1,2,3,5,8,13,21,34]
console.log(toArray(take(10, fibs)));
这样,我们就可以在 weak head normal form 中编码惰性。优点是消费者不知道给定数据结构的特定字段是惰性的还是严格的,不需要关心。
核心递归意味着在每次迭代时调用自己的数据大于或等于之前的数据。 Corecursion 适用于 codata,它们是递归定义的值。不幸的是,值递归在严格评估的语言中是不可能的。不过,我们可以使用显式 thunk:
const Defer = thunk =>
({get runDefer() {return thunk()}})
const app = f => x => f(x);
const fibs = app(x_ => y_ => {
const go = x => y =>
Defer(() =>
[x, go(y) (x + y)]);
return go(x_) (y_).runDefer;
}) (1) (1);
const take = n => codata => {
const go = ([x, tx], acc, i) =>
i === n
? acc
: go(tx.runDefer, acc.concat(x), i + 1);
return go(codata, [], 0);
};
console.log(
take(10) (fibs));
虽然这按预期工作,但这种方法似乎很尴尬。尤其是那对可怕的元组让我很烦。有没有更自然的方式来处理 JS 中的 corecursion/codata?
我会在数据构造函数本身中对 thunk 进行编码。例如,考虑。
// whnf :: Object -> Object
const whnf = obj => {
for (const [key, val] of Object.entries(obj)) {
if (typeof val === "function" && val.length === 0) {
Object.defineProperty(obj, key, {
get: () => Object.defineProperty(obj, key, {
value: val()
})[key]
});
}
}
return obj;
};
// empty :: List a
const empty = null;
// cons :: (a, List a) -> List a
const cons = (head, tail) => whnf({ head, tail });
// fibs :: List Int
const fibs = cons(0, cons(1, () => next(fibs, fibs.tail)));
// next :: (List Int, List Int) -> List Int
const next = (xs, ys) => cons(xs.head + ys.head, () => next(xs.tail, ys.tail));
// take :: (Int, List a) -> List a
const take = (n, xs) => n === 0 ? empty : cons(xs.head, () => take(n - 1, xs.tail));
// toArray :: List a -> [a]
const toArray = xs => xs === empty ? [] : [ xs.head, ...toArray(xs.tail) ];
// [0,1,1,2,3,5,8,13,21,34]
console.log(toArray(take(10, fibs)));
这样,我们就可以在 weak head normal form 中编码惰性。优点是消费者不知道给定数据结构的特定字段是惰性的还是严格的,不需要关心。