Руководство по альфа-версии JavaScript
Монады для разработчиков JavaScript
Что такое монада? Чтобы понять это, не нужно быть экспертом в теории категорий. Вы должны знать обещания JavaScript.
Как и любой другой программист, я хотел знать, что такое монады. Но каждый раз, когда вы ищете Monads в Интернете, вы получаете поток статей по теории категорий. И другие ресурсы тоже не имеют особого смысла.
Чтобы узнать, что такое монады, я проделал нелегкий путь. Я начал изучать Haskell. Только для того, чтобы через несколько месяцев понять, что люди слишком много суетятся из-за Монад. Если вы разработчик JavaScript, то наверняка используете их ежедневно. Вы просто этого не замечаете.
Мы не будем вдаваться в подробности теории категорий или Haskell, но вам нужно знать одну вещь. Когда вы ищете Monads в Интернете, вы не можете пропустить это определение:
(>>=) :: m a -> (a -> m b) -> m b
Это определение оператора bind
в Haskell. На разных языках эта операция называется по-разному, но все они означают одно и то же. Некоторые из альтернативных имен - chain
, bind
, flatMap
, then
, andThen
.
Монадический контекст
(>>=) :: m a -> (a -> m b) -> m b m :: monadic context a, b :: value inside the context (string, number, ..)
Монадический контекст - это просто блок, который реализует все, что необходимо для того, чтобы этот блок был монадой. Очень простой (немонадический) блок может быть примерно таким:
const Box = val => ({ val }); const foo = Box("John");
Это поле - просто обернутое значение. Коробка не имеет никакого поведения, потому что у нее нет никаких методов.
Чтобы что-то было Монадой, вы сами должны заставить его вести себя как Монада.
Итак, вернемся к (>>=) :: m a -> (a -> m b) -> m b
. (>>=)
используется как инфиксный оператор: m a >>= (a -> m b)
. и результат операции (>>=)
- m b
.
Эта проблема
Вы заметили, что у нас есть m a
, но функция принимает a
в качестве аргумента? В этом суть Monads.
Операция (>>=)
заключается в том, чтобы взять значение в монадическом контексте m a
, развернуть его, поэтому мы получаем только a
и передаем его по конвейеру функции (a -> m b)
. И это не волшебство. Вы должны сами кодировать такое поведение. Мы увидим это позже.
Обещания JavaScript похожи на монады
Проще говоря, у них поведение монадного. Чтобы что-то было монадой, оно также должно реализовывать интерфейсы Functor и Applicative. Я упоминаю это только для полноты картины, но мы не будем углубляться в это.
В JavaScript Promises реализован монадический интерфейс с .then()
методом. Давай посмотрим об этом.
// p :: m a :: Promise { 42 }
const p = Promise.resolve(42);
Это в основном создает коробку. У нас есть значение 42
внутри обещания.
☝️ Это наш m a
.
Затем у нас есть функция, которая делит число на два. Входные данные не заключаются в обещание. Но возвращенная функция заключена в обещание.
// divideByTwo :: (a -> m b) const divideByTwo = val => Promise.resolve(val / 2);
☝️ Это наш (a -> m b)
.
Опять же, обратите внимание, что у нас есть значение 42
внутри обещания, но функция divideByTwo
принимает развернутое значение. И мы все еще можем связать их.
// p :: m a :: Promise { 42 } const p = Promise.resolve(42); // p2 :: m a :: Promise { 21 } const p2 = p.then(divideByTwo); // p3 :: m a :: Promise { 10.5 } const p3 = p2.then(divideByTwo);
Или, что более очевидно:
// p :: m a :: Promise { 10.5 } const p4 = p.then(divideByTwo).then(divideByTwo);
Это самая важная особенность монад.
У вас есть значение в поле - Promise { 42 }
. У вас есть функция, которая принимает развернутое значение - 42
. Типы не совпадают - m a
против a
. И вы по-прежнему можете применить функцию к значению в рамке.
Как это возможно
Потому что Promise реализует then
метод для работы таким образом. В большинстве случаев код, выполняемый внутри обещания, является асинхронным. Но монадное поведение Promise в любом случае позволяет связывать последовательность функций.
Монады абстрагируются от управления вспомогательными данными, потока управления или побочных эффектов. Превращение возможно сложной последовательности функций в лаконичный конвейер.
Пользовательский класс Monad-ish
Я собрал очень простой пример монадного класса в TypeScript. Он не выполняет никаких побочных эффектов, но позволяет объединять функции в цепочки.
class Dummy<T> { constructor(private val: T) {} chain<TResult>(fn: (value: T) => Dummy<TResult>): Dummy<TResult> { return fn(this.val); } static unit<T>(val: T): Dummy<T> { return new Dummy(val); } } const d = new Dummy(41); d.chain(val => new Dummy(val + 1)) .chain(val => new Dummy("The answer is: " + val));
Законы монад
Есть некоторые законы, которым должен следовать класс с поведением Монады.
- левая личность
- правильная личность
- ассоциативность
Вы можете прочитать об этом больше в Интернете. Я приведу здесь фрагмент кода, доказывающий, что класс Dummy следует этим правилам.
const m = Dummy.unit(1); const f = (val: number) => new Dummy(val + 1); const g = (val: number) => new Dummy(val + 2); // 1. left identity Dummy.unit(1).chain(f) ==== f(1) // 2. right identity m.chain(Dummy.unit) ==== m // 3. associativity const m1 = Dummy.unit(1); m.chain(f).chain(g) ==== m.chain(val => f(val).chain(g)
==
или ===
здесь не работают; ссылки на объекты разные. Для этой цели я использую ====
, которого не существует, но я понимаю его как сравнение внутреннего значения монадного объекта.
Подведение итогов
Я надеюсь, что это проливает свет на то, что такое монады. Если вы разработчик JavaScript, вы используете их ежедневно. Передача значения, заключенного в обещание, в функцию, ожидающую развернутого значения. И снова возвращает новое значение, заключенное в обещание.