Реализация монад в JavaScript
Теперь, когда node.js поддерживает генераторы гармоник ECMAScript, мы можем написать монодичный код лаконично ala do
блоки в Haskell:
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
работает только для детерминированных монадов. Это не работает для недетерминированных монадов, таких как монада list
, потому что вы можете только один раз возобновить генератор из определенной позиции.
Итак, мой вопрос заключается в следующем: есть ли другой способ реализовать в JavaScript недетерминированные монады, такие как монаха list
?
Ответы
Ответ 1
Поэтому мой вопрос заключается в следующем: есть ли другой способ реализации недетерминированных монад, таких как монада list
кратко в JavaScript?
Да, вы можете реализовать недетерминированные монады, такие как монада списка, в JavaScript с использованием генераторов, как, например, immutagen. Однако, поскольку генераторы в JavaScript не могут быть возобновлены с определенной позиции несколько раз, вы должны эмулировать это поведение, создавая и воспроизводя несколько генераторов. Это решение имеет несколько недостатков.
- Это неэффективно, поскольку необходимо создавать и воспроизводить несколько генераторов, что приводит к квадратичному росту сложности времени.
- Это работает только для чистых монад и чистых вычислений, потому что необходимо создать и воспроизвести несколько генераторов. Следовательно, побочные эффекты будут неправильно выполняться несколько раз.
То, что нам нужно для создания недетерминированных монад, таких как монада списка, является неизменяемым генератором. Неизменный генератор может быть возобновлен с определенной позиции несколько раз. К сожалению, 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;
}
Ответ 2
Итак, мой вопрос таков: есть ли другой способ реализовать не-детерминированные монады, такие как список монад лаконично в JavaScript?
Я предлагаю эту реализацию монады, которую я применил к различным монадам здесь:
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
Ответ 3
Кажется, есть отличный способ реализовать монаду списка следующим образом:
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