Что-то издали похожее на монады

После прочтения десятков «самых понятных введений в монады» и чтения (тоже) десятков обсуждений на разных форумах я пришёл к выводу, что существует группа абстрактных ОО-программистов, которым моя интерпретация «чего-то похожего на монады» может помочь немного приблизиться к правильному пониманию.

Итак, в этой публикации вы не найдете ответы на следующие вопросы:
1. Что такое монада?
2. Где и как использовать монады?
3. Почему монады лучше, чем их отсутствие?

В программировании есть такой феномен — «паттерны проектирования». Официально это набор лучших практик, которыми следует руководствоваться при решении «типичных задач». Неофициально — просто набор костылей для языков, в которых нет встроенных средств для решения типичных проблем.

Есть такой паттерн проектирования — Interpreter. Замечателен он в первую очередь тем, что позволяет сделать некое подобие виртуальной машины поверх любимого языка программирования, при этом:

1. Можно описать программу на языке, понятном виртуальной машине.
2. Можно грабить корованы во всех подробностях описать как виртуальная машина должна интерпретировать каждую инструкцию.

Всё что написано ниже имеет смысл только если любезный читатель минимально знаком с упомянутым паттерном.

Сравнительно каноничный пример:
function add(x) {
  return { op: "add", x: x };
}

function div(x) {
  return { op: "div", x: x };
}

function run(value, statements) {
  for(var i = 0; i < statements.length; ++i) {
    var statement = statements[i];
    var op = statement.op;
    var x = statement.x;
    if(op === "add") {
      value += x;
    } else if(op === "div") {
      value /= x;
    } else {
      throw new Error("Unknown operation " + op);
    }
  }
  return value;
}

var program = [
  add(10),
  div(3)
];

var result = run(0, program);
console.log(result); // 3.3333...

Любители GoF могут поспорить, мол, «это Command, а не Interpreter». Для них пусть это будет Command. В контексте статьи это не очень важно.

В этом примере, во-первых, есть программа, состоящая из двух инструкций: «добавить 10» и «разделить на 3». Что бы это ни значило. Во-вторых, есть исполнитель, который делает что-то осмысленное глядя на программу. Важно заметить, что «программа» влияет на результат своего исполнения очень косвенно: исполнитель совершенно не обязан выполнять инструкции сверху-вниз, он не обязан выполнять каждую инструкцию ровно 1 раз, он вообще может вызовы add() транслировать в «Hello», а div() — в «World».

Договоримся, что трансляция add() в console.log() нам неинтересна. Интересны вычисления. Поэтому немного упростим код, отказавшись от ненужной гибкости:

function add(x) { // add(2)(3) === 5
  return function(a) { return a + x; };
}

function div(x) { // div(10)(5) === 2
  return function(a) { return a / x; };
}

function run(value, statements) {
  for(var i = 0; i < statements.length; ++i) {
    var statement = statements[i];
    value = statement(value);
  }
  return value;
}

var program = [ add(10), div(3) ];
var result = run(program);
console.log(0, result); // 3.3333...

Здесь стоит остановиться. У нас есть некий инструмент, позволяющий отдельно описывать программу и отдельно «способ её исполнения». В зависимости от наших пожеланий к результату исполнения, реализация исполнителя может быть очень разной.

Например, хочется, чтобы как только где-то в вычислениях появляется NaN, null или undefined, вычисления прекращались и возвращался результат null:

...
function run(value, statements) {
  if(!value) {
    return null;
  }
  
  for(var i = 0; i < statements.length; ++i) {
    var statement = statements[i];
    value = statement(value);
    if(!value) {
      return null;
    }
  }
  
  return value;
}

console.log(run(undefined, [add(1)])); // null
console.log(run(1, [add(undefined)])); // null

Хорошо. А что если мы хотим одну и ту же программу выполнять для коллекции разных начальных значений? Тоже не вопрос:

...
function run(values, statements) {
  return values.map(function(value) {
    for(var i = 0; i < statements.length; ++i) {
      var statement = statements[i];
      value = statement(value);
    }
    return value;
  });
}

var program = [ add(10), div(3) ];
console.log(run([0, 1, 2], program)); // [3.333..., 3.666..., 4]

Здесь снова стоит остановиться. Мы используем одни и те же выражения, чтобы описывать программу, но в зависимости от исполнителя получаем очень разные результаты. Попробуем теперь снова немного переписать пример. В этот раз, во-первых, уберём ещё немного гибкости: выражения теперь выполняются строго от первых — к последним, а во-вторых, избавимся от цикла внутри run(). Результат назовём словом Context (чтобы никто не догадался):

...
function Context(value) {
  this.value = value;
}

Context.prototype.run = function(f) {
  var result = f(this.value);
  return new Context(result);
};

var result = new Context(0)
  .run(add(10))
  .run(div(3))
  .value;

console.log(result); // 3.3333... 

Реализация сильно отличается от предыдущих вариантов, но делает оно примерно то же самое. Здесь предлагается ввести термин мунада (от англ. moonad — «лунная реклама»). Здравствуй, Identity moonad:

...
function IdentityMoonad(value) {
  this.value = value;
}

IdentityMoonad.prototype.bbind = function(f) {
  var result = f(this.value);
  return new IdentityMoonad(result);
};

var result = new IdentityMoonad(0)
  .bbind(add(10))
  .bbind(div(3))
  .value;

console.log(result); // 3.3333... 

Эта штука чем-то отдалённо похожа на Identity monad.

Вспомним теперь про тот вариант исполнителя, где мы боролись с NaN и попробуем переписать его используя новый подход к реализации:

function MaybeMoonad(value) {
  this.value = value;
}

MaybeMoonad.prototype.bbind = function(f) {
  if(!this.value) {
    return this;
  }
  
  var result = f(this.value);
  return new MaybeMoonad(result);
};

var result = new MaybeMoonad(0)
  .bbind(add(10))
  .bbind(add(undefined))
  .bbind(div(3))
  .value;

console.log(result); // null

Можно даже более привычный пример:

var person = {
  // address: {
  //   city: {
  //     name: "New York"
  //   }
  // }
};

console.log(person.address.city.name); // падает

console.log(new MaybeMoonad(person)
  .bbind(function(person) { return person.address; })
  .bbind(function(address) { return address.city; })
  .bbind(function(city) { return city.name; })
  .bbind(function(cityName) { return cityName; })
  .value); // не падает, возвращает null

Издалека может показаться, что это Maybe monad. Любезному читателю предлагается самостоятельно реализовать что-то похожее на List monad.

При базовых навыках работы напильником не составит изменить IdentityMoonad таким образом, чтобы вызовы f() стали асинхронными. В результате получится Promise moonad (что-то похожее на q).

Теперь, если внимательно приглядеться к последним примерам, можно попробовать дать более-менее формальное определение мунады. Мунада — это штука, у которой есть 2 операции:
1. return — принимает обычное значение, помещает его в мунадический контекст и возвращает этот самый контекст. Это просто вызов конструктора.
2. bind — принимает функцию от обычного значения, возвращающую обычное значение, выполняет её в контексте мунадического контекста и возвращает монадический контекст. Это вызов `bbind()`.
Share post

Comments 23

    +1
    Ваше описание bind больше похоже на fmap: (a -> b) -> m a -> m b
      +9
      А вы не путаете мунады с монадами случайно? Теория котегорий появилась около двух часов назад. Аппарат ещё не разработан, школа отсутствует. Даже статьи на википедии ещё нет.
        0
        Справедливо.
          –7
          Вот он — XXI век во всей красе. Чего на Википедии нет, того и не существует (сарказм)
            0
            На самом деле в динамическом языке существует только один тип, соответственно в категории, где типы языка — объекты, а функции — стрелки, есть только один объект, со стрелками из себя в себя (группоид?), поэтому bbind :: (any -> any) -> any -> any. И bbind может быть как fmap, так и bind. Для нормальной работы list-монады fmap уже будет недостаточно для описания.
          0
          а теперь читаем статью и понимаем что написано не правильно habrahabr.ru/post/183150/
            +1
            Зато гораздо понятнее
              +2
              Значит цель достигнута. Дело в том, что абстрактный программист с ООП бэкграундом не может с первого раза понять что такое монады и зачем они нужны. Ему придётся прочитать википедию, десятки введений, и, скорее всего, поломать глаза о примеры кода на Haskell. Эта статья претендует на место одного из десятка введений, которые программист в любом случае будет читать.
                +2
                Ну так поясните что именно:) Я перечитал десятки статей про монады, и после этой статьи наконец-то понял. А тут вы — неправильно…
                  0
                  В статье нет монад. Если автор напишет что-то вроде
                  console.log(new ListMoonad(«x», [1, 2, 3])
                  .bbind(ListMoonad(«y», [4,5,6])
                  .bbind(add(«x», «y»))
                  .value); // [1+4, 1+5, 1+6, 2+4, 2+5, 2+6, 3+4, 3+5, 3+6]
                  Тогда будет монада. Точнее, это то — как я вижу решение, хранением словаря внутри Context. А так сплошные функторы с жестко заданной трубой преобразований, а монады позволяют комбинировать результаты разных функций (имитировать присваивание), чем получаются мощнее.
                    0
                    Да не хранит в себе монада ничего такого, о чем вы. Почитайте, во что разворачивается do notation, но я сразу скажу — это всего лишь цепочка лямбд, связываемых bind-ами.
                    Да, это не то, что написал автор, но ваш код еще дальше от истины, я бы сказал.
                      0
                      Автор в этом плане гениально решил обойтись мунадами, чтобы буквоеды проходили мимо. Расшифрую, что у меня написано. Bind умеет связать результат вычисления с некоторой мнемоникой, которая является именем параметра вложенной функции.
                      r :: [Integer]
                      r = do x {- [1,2,3]
                      y {- [4,5,6]
                      return (x+y)

                      { вместо знака больше, парсер тут крутой.

                      Как у автора привязать одно значение к «x», другое — к «y», чтобы использовать в третьей функции? Именно используя точки, а не do-нотацию со вложенными лямбдами?
                        0
                        Не, это уже скучно. Честные монады на JS уже где-то писали, выглядели они очень погано и никакого толка в них все равно не было.

                        По-моему, автор хорошо передал саму идею, зачем эти монады нужны и где они дают преимущества, а точного соответствия в данном случае и не нужно.
                      0
                      Уже непонятно… Я понимаю что вы понимаете, но к сожалению слишком сжатое изложение:(
                      +1
                      На самом деле эти мунады есть монады. Но… не честным путём они ими являются. А вот почему:

                      Для того, чтобы понять, чем мунады не совсем монады надо посмотреть на подпись:
                      moo_b (moo_a M).bbind (a_to_b function)  {...}
                      

                      или в нотации Хаскеля
                      bbind :: (a -> b) -> moo a -> moo b
                      
                      bbind = fmap
                      

                      Как видим, bbind на самом деле — функтор.

                      Монадами мунады стали бы, если можно было написать:
                      .bbind(function(cityName) { return new MaybeMoonad(cityName); })
                      


                      Кстати, легко видно как исправить положение — нам надо добавить по строчке в функцию bbind:
                      XMoonad.prototype.bbind = function(mf) {
                       f = mf.value;
                      ...
                      }
                      

                      И вот тут превратятся наши гадкие утята — мунады в настоящие монады!

                      Однако, мунады неотличимы от монад из-за одной хитрости, которые мы соблюдали: все конструкторы должны быть this.value = value;
                      Пы.Сы. ну и всегда используется в конце runMonad то бишь, M.value
                        0
                        прошу прощения, не так исправил в bbind, вот наша функция:
                        XMoonad.prototype.bbind = function(mf) {
                          ...
                          return f(this.value);
                        }
                        
                    0
                    Официально это набор лучших практик, которыми следует руководствоваться при решении «типичных задач». Неофициально — просто набор костылей для языков, в которых нет встроенных средств для решения типичных проблем.


                    Золотые слова!

                    Но как они к м*надам относятся?
                      +1
                      А чем в данной статье отличается Context от IdentityMoonad?
                      Несколько раз пролистал, думал, может какой-то подвох от автора. :)
                        +3
                        Ничем. В данном случае переименование позволяет показать, что монада в вырожденном случае — это способ сделать method chaining (builder, fluent interface) с передачей контекста (промежуточного значения).
                          +1
                          Как я понял, контекст это общая «база» для всех монад — обертка, в которой хранятся собственно данные. Этот шаг необходим для понимания.
                          То что в данном примере он ничем не отличается, не столь важно…
                          0
                          Имхо на википедии доступнее написано.
                              0
                              Я так понимаю, с Мэйби монадой небольшая ошибочка вышла:

                              MaybeMoonad.prototype.bbind = function(f) {
                                if(this.value == null) {   // было (!this.value)
                                  return this;
                                }
                              ...
                              

                              А то с Мэйби монадой прибавить ничего не сможете ))

                              Only users with full accounts can post comments. Log in, please.