有没有更好的方法来模仿 JS 中的 do 符号?
Is there a better way to mimic do notation in JS?
单子计算在 JS 中很快变得混乱:
const chain = fm => xs =>
xs.reduce((acc, x) => acc.concat(fm(x)), []);
const of = x => [x];
const main = xs => ys => zs =>
chain(x =>
x === 0
? []
: chain(y =>
chain(z => [[x, y, z]]) (zs)) (ys)) (xs);
console.log("run to completion",
main([1, 2]) (["a", "b"]) ([true, false]));
console.log("short circuiting",
main([0, 2]) (["a", "b"]) ([true, false]));
在Haskell do
中可以使用符号来隐藏嵌套的函数调用。但是,do
是 Javascript 所缺乏的编译时技术。
生成器函数似乎很适合,但它们不适用于提供优先选择的单子。所以我一直在寻找一种替代方法,最近想出了一种 monadic applicator 来稍微理清嵌套计算:
const chain = fm => xs =>
xs.reduce((acc, x) => acc.concat(fm(x)), []);
const of = x => [x];
const id = x => x;
const infixM3 = (w, f, x, g, y, h, z) =>
f(x_ =>
w(x_, w_ => g(y_ =>
w_(y_, w__ => h(z_ =>
w__(z_, id)) (z))) (y))) (x);
const mainApp = xs => ys => zs => infixM3(
(x, k) =>
x === 0
? []
: k((y, k) =>
k((z, k) => [[x, y, z]])),
chain, xs,
chain, ys,
chain, zs);
console.log("run to completion",
mainApp([1, 2]) (["a", "b"]) ([true, false]));
console.log("short circuiting",
mainApp([0, 2]) (["a", "b"]) ([true, false]));
涂抹器在中缀位置模仿链条,因此得名。它基于局部延续,因此提升函数需要一对分别由绑定值和延续组成的参数。如果不应用延续,则计算短路。看似复杂,其实是一个机械的过程。
比较显式版本和抽象版本我认为这是可读性方面的改进:
chain(x =>
x === 0
? []
: chain(y =>
chain(z => [[x, y, z]]) (zs)) (ys)) (xs);
infixM3(
(x, k) =>
x === 0
? []
: k((y, k) =>
k((z, k) => [[x, y, z]])),
chain, xs,
chain, ys,
chain, zs);
提升函数中的延续以及应用程序是 arity 感知的事实都很麻烦。此外,它看起来根本不像做符号。我们能否更接近类似于 do
表示法的语法?
您可以使用 immutagen 库来使用生成器编写 monadic 代码。
const monad = bind => regen => (...args) => function loop({ value, next }) {
return next ? bind(value, val => loop(next(val))) : value;
}(immutagen.default(regen)(...args));
const flatMap = (array, callback) => array.flatMap(callback);
const list = monad(flatMap);
const main = list(function* (xs, ys, zs) {
const x = yield xs;
if (x === 0) return [];
const y = yield ys;
const z = yield zs;
return [[x, y, z]];
});
console.log("run to completion", main([1, 2], ["a", "b"], [true, false]));
console.log("short circuiting", main([0, 2], ["a", "b"], [true, false]));
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://unpkg.com/immutagen@1.0.9/immutagen.js"></script>
如您所见,这也适用于非确定性 monad。但是,它有两个缺点。
- 效率低下,因为需要创建和重放多个生成器,导致时间复杂度呈二次方增长。
- 它只适用于纯单子和纯计算,因为需要创建和重放多个生成器。因此,副作用会被错误地执行多次。
然而,即使您不使用生成器,在 JavaScript 中编写 monadic 代码也不是那么糟糕。
const flatMap = (array, callback) => array.flatMap(callback);
const main = (xs, ys, zs) =>
flatMap(xs, x =>
x === 0 ? [] :
flatMap(ys, y =>
flatMap(zs, z =>
[[x, y, z]])));
console.log("run to completion", main([1, 2], ["a", "b"], [true, false]));
console.log("short circuiting", main([0, 2], ["a", "b"], [true, false]));
.as-console-wrapper { max-height: 100% !important; top: 0; }
归根结底,如果您想两全其美,那么您要么需要使用一种可以编译为 JavaScript 的语言,要么使用像 Babel 或 sweet.js 这样的预处理器.
单子计算在 JS 中很快变得混乱:
const chain = fm => xs =>
xs.reduce((acc, x) => acc.concat(fm(x)), []);
const of = x => [x];
const main = xs => ys => zs =>
chain(x =>
x === 0
? []
: chain(y =>
chain(z => [[x, y, z]]) (zs)) (ys)) (xs);
console.log("run to completion",
main([1, 2]) (["a", "b"]) ([true, false]));
console.log("short circuiting",
main([0, 2]) (["a", "b"]) ([true, false]));
在Haskell do
中可以使用符号来隐藏嵌套的函数调用。但是,do
是 Javascript 所缺乏的编译时技术。
生成器函数似乎很适合,但它们不适用于提供优先选择的单子。所以我一直在寻找一种替代方法,最近想出了一种 monadic applicator 来稍微理清嵌套计算:
const chain = fm => xs =>
xs.reduce((acc, x) => acc.concat(fm(x)), []);
const of = x => [x];
const id = x => x;
const infixM3 = (w, f, x, g, y, h, z) =>
f(x_ =>
w(x_, w_ => g(y_ =>
w_(y_, w__ => h(z_ =>
w__(z_, id)) (z))) (y))) (x);
const mainApp = xs => ys => zs => infixM3(
(x, k) =>
x === 0
? []
: k((y, k) =>
k((z, k) => [[x, y, z]])),
chain, xs,
chain, ys,
chain, zs);
console.log("run to completion",
mainApp([1, 2]) (["a", "b"]) ([true, false]));
console.log("short circuiting",
mainApp([0, 2]) (["a", "b"]) ([true, false]));
涂抹器在中缀位置模仿链条,因此得名。它基于局部延续,因此提升函数需要一对分别由绑定值和延续组成的参数。如果不应用延续,则计算短路。看似复杂,其实是一个机械的过程。
比较显式版本和抽象版本我认为这是可读性方面的改进:
chain(x =>
x === 0
? []
: chain(y =>
chain(z => [[x, y, z]]) (zs)) (ys)) (xs);
infixM3(
(x, k) =>
x === 0
? []
: k((y, k) =>
k((z, k) => [[x, y, z]])),
chain, xs,
chain, ys,
chain, zs);
提升函数中的延续以及应用程序是 arity 感知的事实都很麻烦。此外,它看起来根本不像做符号。我们能否更接近类似于 do
表示法的语法?
您可以使用 immutagen 库来使用生成器编写 monadic 代码。
const monad = bind => regen => (...args) => function loop({ value, next }) {
return next ? bind(value, val => loop(next(val))) : value;
}(immutagen.default(regen)(...args));
const flatMap = (array, callback) => array.flatMap(callback);
const list = monad(flatMap);
const main = list(function* (xs, ys, zs) {
const x = yield xs;
if (x === 0) return [];
const y = yield ys;
const z = yield zs;
return [[x, y, z]];
});
console.log("run to completion", main([1, 2], ["a", "b"], [true, false]));
console.log("short circuiting", main([0, 2], ["a", "b"], [true, false]));
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://unpkg.com/immutagen@1.0.9/immutagen.js"></script>
如您所见,这也适用于非确定性 monad。但是,它有两个缺点。
- 效率低下,因为需要创建和重放多个生成器,导致时间复杂度呈二次方增长。
- 它只适用于纯单子和纯计算,因为需要创建和重放多个生成器。因此,副作用会被错误地执行多次。
然而,即使您不使用生成器,在 JavaScript 中编写 monadic 代码也不是那么糟糕。
const flatMap = (array, callback) => array.flatMap(callback);
const main = (xs, ys, zs) =>
flatMap(xs, x =>
x === 0 ? [] :
flatMap(ys, y =>
flatMap(zs, z =>
[[x, y, z]])));
console.log("run to completion", main([1, 2], ["a", "b"], [true, false]));
console.log("short circuiting", main([0, 2], ["a", "b"], [true, false]));
.as-console-wrapper { max-height: 100% !important; top: 0; }
归根结底,如果您想两全其美,那么您要么需要使用一种可以编译为 JavaScript 的语言,要么使用像 Babel 或 sweet.js 这样的预处理器.