在JavaScript中实现单子

16

现在node.js支持ECMAScript Harmony生成器,我们可以像Haskell中的do块一样简洁地编写单子代码:

function monad(unit, bind) {
    return function (f) {
        return function () {
            var g = f.apply(this, arguments);

            return typeOf(g) === "Generator" ? send() : unit(g);

            function send(value) {
                var result = g.next(value);
                if (result.done) return unit(result.value);
                else return bind(result.value, send);
            }
        };
    };
}

function typeOf(value) {
    return Object.prototype.toString.call(value).slice(8, -1);
}

在上面的代码中,monad 是一个函数,可用于创建像以下这样的确定性单子:

var maybe = monad(function (a) {
    return {just: a};
}, function (m, f) {
    return m === null ? null : f(m.just);
});

你现在可以使用maybe,如下所示:

var readZip = maybe(function * (a, b) {
    var a = yield readList(a);
    var b = yield readList(b);
    return _.zip(a, b);
});

上述函数readZip接受两个字符串,将它们转换为列表,然后将它们压缩。如果有错误,则会立即返回null。它依赖于以下函数:

function readList(string) {
    try {
        var value = JSON.parse(string);
        return value instanceof Array ? {just: value} : null;
    } catch (error) {
        return null;
    }
}

我们进行测试以检查它是否按预期工作:

console.log(readZip('[1,2,3,4]', '["a","b"]')); // [[1,"a"],[2,"b"],[3,"c"]]
console.log(readZip('hello', '["a","b"]'));     // null
console.log(readZip('[1,2,3,4]', 'world'));     // null

同样地,我们可以创建任何其他确定性单子。例如,我最喜欢的 cont 单子:

var cont = monad(function (a) {
    return function (k) {
        return k(a);
    };
}, function (m, k) {
    return function (c) {
        return m(function (a) {
            return k(a)(c);
        });
    };
});

现在我们可以使用cont来简洁地创建以续传方式编写的函数:

var fib = cont(function * (n) {
    switch (n) {
    case 0: return 0;
    case 1: return 1;
    default:
        var x = yield fib(n - 1);
        var y = yield fib(n - 2);
        return x + y;
    }
});
您可以按如下方式使用 fib 函数:
fib(10)(function (a) { console.log(a); }); // 55

不幸的是,monad 只适用于确定性 monad。它不适用于非确定性 monad,如 list monad,因为你只能从特定位置恢复生成器一次。

我的问题是:是否有其他简洁的方法在 JavaScript 中实现非确定性 monad,例如 list monad?


不要介意我自己的宣传:https://github.com/elclanrs/doM.js - elclanrs
1
@elclanrs 这是作弊。虽然它能工作,但由于您正在构建一个全新的函数,因此无法在调用现场保留原始函数的词汇环境。 - Aadit M Shah
你的意思是什么?能详细说明一下吗? - elclanrs
1
你可以使用类似Coffeescript或Livescript这样的语言来获得更好的语法,这是一个选项吗? - elclanrs
1
LiveScript绝对值得。我已经转换了 : )。检查我的答案,看看是否有帮助。 - elclanrs
显示剩余4条评论
5个回答

7
我的问题是:在JavaScript中是否有其他简洁的实现非确定性monad,例如列表monad?我建议使用这种monad实现,我已经应用到不同的monad 这里
var extend = function(a, b) {
  for (var i in b)
    a[i] = b[i];
  return a;
};

// Chain a new `this`
var fluent = function(f) {
  return function() {
    var clone = extend(Object.create(null), this);
    f.apply(clone, arguments);
    return clone;
  };
};

var toArray = function(x) {
  return Array.prototype.slice.call(x);
};

var List = {
  unit: fluent(function() {
    this.x = toArray(arguments);
  }),
  bind: function(f) {
    var fx = this.x.map(f.bind(this));
    var a = fx[0];
    for (var i=1; i<fx.length; i++)
      a.x = a.x.concat(fx[i].x);
    return a;
  },
  lift: function(f) {
    return function(x) {
      return List.unit(f(x));
    }
  },
  valueOf: function() {
    return this.x;
  }
};

var add1 = function(x) {
  return x + 1;
};

// Laws
var m = List.unit(3);
var f = List.lift(add1);

var laws = [
  m.bind(f)[0] == f(3)[0],
  m.bind(function(x){ return List.unit(x) })[0] == m[0],
  m.bind(function(x){ return f(x).bind(f) })[0] == m.bind(f).bind(f)[0]
];

console.log(laws); //=> [true, true, true]

// lift
var result = List.unit(1,2).bind(List.lift(add1)); //=> [2,3]

console.log(result.valueOf());

// do
var result = List.unit(1,2).bind(function(x) {
  return this.unit(3,4).bind(function(y) {
    return this.unit(x + y);
  });
});

console.log(result.valueOf()); //=> [4,5,5,6]

显然,“do”语法导致回调地狱,但在LiveScript中,您可以缓解这种痛苦。
result = do
  x <- List.unit 1 2 .bind
  y <- @unit 3 4 .bind
  @unit x + y

你也可以创造性地为你的bind方法命名:

result = do
  x <- List.unit 1 2 .\>=
  y <- @unit 3 4 .\>=
  @unit x + y

2
所以我的问题是:是否有其他方法可以在JavaScript中简洁地实现类似于列表单子的非确定性单子? 是的,您可以使用生成器在JavaScript中简洁地实现类似于列表单子的非确定性单子,就像 immutagen 一样。但是,由于JavaScript中的生成器不能从特定位置多次恢复,因此您必须通过创建和重放多个生成器来模拟此行为。这种解决方案有几个缺点。
  1. 它效率低下,因为需要创建和重放多个生成器,导致时间复杂度呈二次增长。
  2. 它仅适用于纯单子和纯计算,因为需要创建和重放多个生成器。因此,副作用会被错误地执行多次。
为了创建诸如列表单子之类的非确定性单子,我们需要的是不可变生成器。不可变生成器可以从特定位置多次恢复。不幸的是,JavaScript不支持原生的不可变生成器。但是,我们可以通过创建和重放多个可变生成器来模拟它。因此,让我们看看如何创建不可变生成器。

我们需要解决的第一个问题是如何将可变生成器回放到特定点。我们使用一类被称为“重新生成器”的特殊函数来实现这一点。重新生成器是任何返回可变生成器的函数。最简单的这种函数示例是function* () {}。因此,每个生成器函数都是重新生成器,但并不是每个重新生成器都是生成器函数。您可以通过使用以下函数来提前旧的重新生成器来创建新的重新生成器。

// type Regenerator = Arguments -> MutableGenerator

// next :: (Regenerator, Arguments) -> Regenerator
const next = (regen, ...args) => data => {
    const gen = regen(...args);
    return gen.next(data), gen;
};

next 函数可用于将生成器推进到特定点。例如,考虑以下代码片段。

const next = (regen, ...args) => data => {
    const gen = regen(...args);
    return gen.next(data), gen;
};

const regen1  = next(regen0, 1, 2, 3);
const regen2  = next(regen1, undefined); // first value of mutable generator ignored
const regen3  = next(regen2, 10);

const gen1 = regen3(20);
const gen2 = regen3(30);

const result1 = gen1.next(40).value; // 10 + 20 + 40
const result2 = gen2.next(50).value; // 10 + 30 + 50

console.log(result1, result2);

function* regen0(a, b, c) {
    const x = yield a;
    const y = yield b;
    const z = yield c;
    return x + y + z;
}

正如您所看到的,我们可以使用next函数推进一个regenerator,或者将regenerator应用于值以获得可变生成器。既然我们有了重播可变生成器到特定点的能力,我们可以按以下方式创建不可变生成器。
// immutagen :: Regenerator -> Arguments -> ImmutableGenerator
const immutagen = regen => (...args) => function loop(regen) {
    return (gen, data) => {
        const {value, done} = gen.next(data);
        if (done) return {value, next: null};

        let replay = false;
        const recur = loop(next(regen, data));
        return {value, next: value => {
            if (replay) return recur(regen(data), value);
            replay = true; return recur(gen, value);
        }};
    };
}(next(regen, ...args))(regen(...args));

immutagen函数可用于创建不可变的生成器函数,我们可以调用它来产生不可变的生成器。以下是一个示例,演示如何创建和使用不可变的生成器。

const next = (regen, ...args) => data => {
    const gen = regen(...args);
    return gen.next(data), gen;
};

const immutagen = regen => (...args) => function loop(regen) {
    return (gen, data) => {
        const {value, done} = gen.next(data);
        if (done) return {value, next: null};

        let replay = false;
        const recur = loop(next(regen, data));
        return {value, next: value => {
            if (replay) return recur(regen(data), value);
            replay = true; return recur(gen, value);
        }};
    };
}(next(regen, ...args))(regen(...args));

const foo = immutagen(function* (a, b, c) {
    const x = yield a;
    const y = yield b;
    const z = yield c;
    return x + y + z;
});

const bar = foo(1, 2, 3).next(10).next(20);

const result1 = bar.next(30).value; // 10 + 20 + 30
const result2 = bar.next(40).value; // 10 + 20 + 40

console.log(result1, result2);

最后,现在我们有了不可变的生成器,我们可以更简洁地实现非确定性单子,比如列表单子,如下所示:

const next = (regen, ...args) => data => {
    const gen = regen(...args);
    return gen.next(data), gen;
};

const immutagen = regen => (...args) => function loop(regen) {
    return (gen, data) => {
        const {value, done} = gen.next(data);
        if (done) return {value, next: null};

        let replay = false;
        const recur = loop(next(regen, data));
        return {value, next: value => {
            if (replay) return recur(regen(data), value);
            replay = true; return recur(gen, value);
        }};
    };
}(next(regen, ...args))(regen(...args));

const monad = bind => regen => (...args) => function loop({value, next}) {
    return next ? bind(value, val => loop(next(val))) : value;
}(immutagen(regen)(...args));

const flatMap = (array, callback) => array.flatMap(callback);

const list = monad(flatMap);

const foo = list(function* (xs, ys) {
    const x = yield xs;
    const y = yield ys;
    return [x * y];
});

console.log(foo([1, 2, 3], [4, 5, 6]));

请注意,monad函数只需要bind,不需要unit

1

如果在JS中一般情况下抽象嵌套的计算结构,就会损害效果层或失去单子根据先前值确定下一个效果的能力。

但至少可以通过应用像应用程序一样的单子来抽象出chain

const arrChain = mx => fm =>
  mx.reduce((acc, x) => arrAppend(acc) (fm(x)), []);

const arrAppend = xs => ys =>
  (xs.push.apply(xs, ys), xs);

const chain2 = chain => tx => ty => fm =>
  chain(chain(tx) (x => fm(x)))
    (gm => chain(ty) (y => gm(y)));

const main = chain2(arrChain)
  ([1,2])
    ([3,4])
      (x => [y => [x, y]]); // nested constructor application

// prev-val-next-eff-dependency:

const main2 = chain2(arrChain)
  ([1,2])
    ([3,4])
        (x =>
          x === 1
            ? []
            : [y => [x, y]]);

console.log(main);
console.log(main2);

这种计算方法比原始计算略微低效,因为每个效果都会执行一次,除了解开下一个操作。

这里是另一种混合单子和续传风格的方法。然而,它也不能替代do-notation:

const chainv = ({chain}) => {
  const go = (mx, ...ms) => fm =>
    ms.length === 0
      ? chain(mx) (fm)
      : chain(mx) (x => fm(x) (go(...ms)));

  return go;
};

const arrChain = xs => fm =>
  xs.flatMap(fm);

const main = chainv({chain: arrChain}) (
  [1,2],
  [3,4],
  [5,6])
    (x => k =>
      k(y => k =>
        k(z => [x, y, z])));

// [1, 3, 5, 1, 3, 6, 1, 4, 5, 1, 4, 6, 2, 3, 5, 2, 3, 6, 2, 4, 5, 2, 4, 6]

const main2 = chainv({chain: arrChain}) (
  [1,2],
  [3,4],
  [5,6])
    (x => k =>
      x === 1
        ? []
        : k(y => k =>
          k(z => [x, y, z])));

// [2, 3, 5, 2, 3, 6, 2, 4, 5, 2, 4, 6]

console.log("main:", main);
console.log("main2:", main2);


0
因为你只能从特定位置恢复生成器一次。
由于所有声称克隆迭代器的解决方案实际上都具有二次复杂度(技术上为O(|yield深度| * |迭代器数量|)),因此也许解决方案是尽可能避免使用迭代器,而是以传递样式编写程序的相关部分,尽管它很丑陋。毕竟,一个 continuation 可能会返回多次。我认为 continuation monad 是普适的,其他 monad 可以用它来实现。
也许我们可以进行某种语法转换(甚至在运行时),以使生活更轻松,不必拥有巨大的嵌套函数语句。虽然,在你之前的一个答案中,你确实有一个相当好的 call/cc-but-in-CPS。
我们可以使用 JavaScript 编写的 JavaScript 解析器,例如 https://esprima.org/,通过检查函数的 .toString() 来查看源代码。然后将其转换为 CPS。
例如,也许我们可以说:
decoCPS(
function square(x) {
    return x**2;
});

decoCPS(
function hypotenuse(a,b) {
    var sqrt = Math.sqrt;
    let aSquared = a*a;
    return sqrt(aSquared+square(b));
});

然后这将神奇地转换为 CPS... 如果用户想要使用可变状态或非协同数据结构(也许通过某种 continuation-and-state monad 通过状态传递状态... 原型继承很慢,但代理可能有所帮助),但如果我们目前只关心函数,我们可以说“既然我们可能希望返回一个闭合变量的函数,每当我们分配变量时... 或者每当我们调用一个函数时(甚至在表达式中间)...以递归方式生成该计算的 continuation”:
hypotenuse(a,b) {
    // original code
}

步骤1:参数转换:

hypotenuse.cps = function hypotenuseCps(k, a,b) {
    var sqrt = Math.sqrt;
    let aSquared = a*a;
    return sqrt(aSquared+square(b));
}

步骤2-9999:??? 在这里我迷失了... 我不确定确切的直接样式到 CPS 转换的详细过程。我看过一些考虑此处此处。如果在表达式中有函数调用,会变得复杂,但是解析器会给你一个 AST 来使用。

也许首先将返回语句转换为k(返回值,函数其余计算{...}),然后在转换完成后,如果全局变量设置为启用 trampoline 样式,则转换回为某种 thunk 或封装 thunk 的包装器,例如 return new TrampolineCallMeNext(k, {args:[returned value], callback:function restOfComputationHere{...}})。这将使用户有选择是否总是滥用 ecmascript 的 SetTimeout 机制,因为这可能会导致过度减速。

那么我不确定一般的语法转换是逐行或逐个子表达式向后进行的...我相信你对此了解更多,并可以从上面的链接中拼凑出来。不过我认为,也许一个解析器生成器(例如antlr或类似工具)可能有一些内置的方法来执行CPS转换,但我找不到这样的东西。

我不擅长CPS或单子,所以如果我在这里说错了,请有经验的人纠正我。然后,您可以使用延续单子实现任何想要的单子。第一个链接提供了使用延续实现列表单子的示例。

也许我低估了直接将大段程序转换为CPS的普遍程度,包括for循环和其他结构。


由于在“斜边”代码中没有单子“bind”(或生成器“yield”),我不明白为什么您要将其转换为任何内容。 - Bergi

0

似乎有一种巧妙的方法来实现类似于此的列表单子:

function* unit(value) {
    yield value;
}
function* bind(list, transform) {
    for (var item of list) {
        yield* transform(item);
    }
}
var result = bind(['a', 'b', 'c'], function (element) {
    return bind([1, 2, 3], function* (element2) {
        yield element + element2;
    });
});
for (var item of result) {
    console.log(item);
}

基于https://curiosity-driven.org/monads-in-javascript#list的编程


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接