Руководство по альфа-версии 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, вы используете их ежедневно. Передача значения, заключенного в обещание, в функцию, ожидающую развернутого значения. И снова возвращает новое значение, заключенное в обещание.

Ресурсы