Мы как программисты иногда попадаем в "программистский ад", место где наши обычные абстракции не справляются с решением ряда повторяющихся проблем.


В данной статье будут рассмотрены такие проблемы, синтаксические конструкции используемые для их решения и наконец как эти проблемы могут быть решены единообразно с помощью монад.


Ад проверки на null


Данная проблема возникает когда несколько частичных функций (функции которые могут не вернуть значение) нужно выполнить последовательно.


Такие функции обычно приводят в глубоко вложенному и сложно читаемому коду с чрезмерным количеством синтаксического шума.


var a = getData();
if (a != null) {
  var b = getMoreData(a);
  if (b != null) {
     var c = getMoreData(b);
     if (c != null) {
        var d = getEvenMoreData(a, c)
        if (d != null) {
          print(d);
        }
     }
  }
}

Встроенное решение: Элвис-оператор


Это специальный синтаксис (?.) помогающий перемещаться между вызовами частичных функций. К сожалению он излишне завязан на объектно ориентированный стиль записей и доступа к методам.


var a = getData();
var b = a?.getMoreData();
var c = b?.getMoreData();
var d = c?.getEvenMoreData(a);
print(d);

Монада Maybe


Если наши функции будут явно возвращать тип Maybe (иногда он называет Option), мы можем соединить эти функции в цепочку с помощью do нотации (используя тот факт что Maybe/Option монадические).


do
  a <- getData
  b <- getMoreData a
  c <- getMoreData b
  d <- getEvenMoreData a c
  print d

Ад for цикла


Проблема возникает когда нужно пройтись по нескольким зависимым наборам данных. Так же как и при проверке на null, код становится глубоко вложенным с большим количеством синтаксического шума


var a = getData();
for (var a_i in a) {
  var b = getMoreData(a_i);
  for (var b_j in b) {
    var c = getMoreData(b_j);
    for (var c_k in c) {
      var d = getMoreData(c_k);
      for (var d_l in d) {
        print(d_l);
      }
    }
  }
}

Встроенное решение: Списковое включение


Более элегантное решение проблемы было найдено с введением специальной синтаксической конструкции называемой списковое включение, сильно похожее на SQL (например в C# сходство достигает максимума прим. перев.).


[
  print(d)
  for a in getData()
  for b in getMoreData(a)
  for c in getMoreData(b)
  for d in getEvenMoreData(a, c)
]

List Монада


Подметив что списки это монады и использую do-нотацию можно написать такое же элегантное решение без каких либо дополнительный синтаксических конструкции.


do
  a <- getData
  b <- getMoreData a
  c <- getMoreData b
  d <- getEvenMoreData a c
  print d

Ад колбэков


Самый известный и возможно наиболее болезненный круг ада. Здесь инверсия контроля необходима для реализации асинхронности что веден к глубоко вложенному коду и синтаксическому шуму, сложности в отслеживании обработки ошибок и ряду других болячек.


getData(a =>
  getMoreData(a, b =>
    getMoreData(b, c =>
      getEvenMoreData(a, c, d =>
        print(d),
        err => onErrorD(err)
      )
      err => onErrorC(err)
    ),
    err => onErrorB(err)
  ),
  err => onErrorA(err)
)

Встроенное решение: async/await


Что бы преодолеть данную сложность, был придуман еще один специальный синтаксис — async/await. Обычно данный подход делегирует обработку ошибок с помощью try/catch синтаксиса, который сам по себе ведет еще в один ад.


async function() {
  var a = await getData
  var b = await getMoreData(a)
  var c = await getMoreData(b)
  var d = await getEvenMoreData(a, c)
  print(d)
}

Встроенное решение: Promises


Promises – это еще одно возможное решение (так же Futures/Tasks). В то время как проблема с вложениями частично решена, использовать результат промисов в нескольким местах заставляет нас в ручную создавать лексический скоуп для таких значений. Это приводить в одному уровню вложения для каждой переменной используемой в нескольких местах. К тому же, использование промисов напрямую через then синтаксис выглядит не так чисто как использование async/await


getData().then(a => getMoreData(a)
  .then(b => getMoreData(b))
  .then(c => getEvenMoreData(a, c))
  .then(d => print(d)
);

Монада Continuation


Уже не должно быть сюрпризом что мы можем решить данную проблемы с помощью такого же подхода как и для двух предыдущих проблем (отметив что промисы образую монаду).


do
  a <- getData
  b <- getMoreData a
  c <- getMoreData b
  d <- getEvenMoreData a c
  print d

Ад передачи состояния


Даже без побочных эффектов в мире чистых функций есть сложности. В ряде случаем чрезмерная передача параметров между функциями может стать проблемой.


let
  (a, st1) = getData initalState
  (b, st2) = getMoreData (a, st1)
  (c, st3) = getMoreData (b, st2)
  (d, st4) = getEvenMoreData (a, c, st3)
in print(d)

Встроенное решение: императивные языки


Решить данную проблему можно используя неявное состояние, что позволит функциям общаться между собой без явной передачи всех параметров. К сожалению использование императивной модели существенно усложняет понимание кода. Жизненный цикл и размер состояния обычно не имеет статических границ.


a = getData();
b = getMoreData(a);
c = getMoreData(b);
d = getEvenMoreData(a, c);
print(d)

Монада State


Данная монада позволяет использовать чистое функциональное состояния на которое нет никаких внешних ссылок, что позволяет применять множество полезный операций, например сериализация состояния или реализация таких функций как excursion, что то похожее на то что делаю библиотеки на подобии Redux.


Монада State ограничивает жизненный цикл вычисления с состоянием и гарантируют что программы остаются просты для понимания.


do
  a <- getData
  b <- getMoreData a
  c <- getMoreData b
  d <- getEvenMoreData a c
  print d

Заключение


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

Поделиться с друзьями
-->

Комментарии (21)


  1. nikitasius
    23.05.2017 10:07
    +2

    var a = getData();
    if (a != null) {
      var b = getMoreData(a);
      if (b != null) {
         var c = getMoreData(b);
         if (c != null) {
            var d = getEvenMoreData(a, c)
            if (d != null) {
              print(d);
            }
         }
      }
    }

    А монады-то прикольные штуки! Без них вполне читабельное решение влоб и не такой уж и ад (на haskwell не кодил!) :


    var a,b,c,d;
    if( 
       ((a=getData())!=null)&&
       ((b=getMoreData(a))!=null)&&
       ((c=getMoreData(b))!=null)&&
       ((d=getEvenMoreData(a,c))!=null)
      )
    {
      print(d);
    }


  1. wert_lex
    23.05.2017 11:02
    +2

    Автор немного лукавит)
    Тут дело скорее не в самих монадах, а в do-notation.


    1. dotneter
      23.05.2017 11:10
      +3

      Нельзя сказать что монады тут совсем не при чем, так как do нотация работает по верх них.
      По сути есть две равноправные составляющие, концептуальная и синтаксическая.


  1. Scf
    23.05.2017 11:19
    +6

    При использовании монад всплывают вложенные монады, state-монады с несовместимыми типами, монадические трансформеры....


    И можно готовить следующую статью:


    Избегание ада монад с помощью ???


    1. dotneter
      23.05.2017 11:25
      +5

      Избегание ада монад с помощью ???

      http://okmij.org/ftp/Haskell/extensible/exteff.pdf


      1. Scf
        23.05.2017 11:29

        Насколько я в курсе, системы эффектов пока еще удел теоретиков. До появления чего-то практичного еще далеко.



    1. Envy
      23.05.2017 13:38

      МонадТрасформеров


  1. koldyr
    23.05.2017 16:30
    +1

    [RWST r w s (MaybeT (ErrorT IO)) a]


  1. lynch513
    23.05.2017 19:38

    Какой у Вас резкий переход в примерах проверки на null. От JavaScript, CoffeeScript к Haskell. Например на том же JavaScript можно использовать Maybe как функтор, проверку на null и undefined спрятать в метод map. А если еще упростить

    function mayBe(value, fn) {
        return value === null || value === undefined ? value : fn(value)
    }
    


    или на ES6

    const mayBe = (v, f) => v === null || v === undefined ? v : f(v)
    


    Вроде как корректный функтор, который можно использовать при цепочечных вызовах функции, но такого сахара как do конечно не будет. Сидя в JS к прекрасному тоже можно стремится


    1. dotneter
      23.05.2017 19:41

      Haskell как раз используется для примеров потому что есть сахар, а так конечно, Maybe и прочие абстракции можно использовать в любом языке.


  1. yarric
    23.05.2017 21:01

    проверка на null

    Можно использовать старый добрый try-catch:


    var a = getData()
    try {
      var b = getMoreData(a)
      var c = getMoreData(b)
      var d = getEvenMoreData(a, c)
      print(d)
    }
    catch (NullPointerException ex) { ... }

    for-цикл

    Зачем столько скобочек? Без скобочек всё выглядит также, как и со списковым включением:


    for (var a in getData()) 
      for (var b in getMoreData(a)) 
        for (var c in getMoreData(b)) 
          for (var d in getMoreData(c)) 
            print(d)

    Но тут скорее всего стоит как-то перепроектировать систему и повысить уровень абстракции.


    1. Akirus98
      24.05.2017 15:05
      +1

      А если в getMoreData(a) произойдет настоящий NullPointer мы его тоже будем замалчивать?)


      1. yarric
        24.05.2017 16:31

        Можно либо использовать исключения, либо спроектировать без null: если нет данных просто возвращается пустой список.


        1. raveclassic
          24.05.2017 16:35

          А если это не список?


          1. yarric
            24.05.2017 16:54

            Хорошо: пустой итератор.


    1. zuwr2t
      24.05.2017 15:05

      Исключения же дорогие.


  1. Antervis
    24.05.2017 05:41
    -1

    Ад проверки на null

    да прибудет с вами early return


  1. master65
    24.05.2017 08:57

    Универсальное решение всех проблем!

    do
      a <- getData
      b <- getMoreData a
      c <- getMoreData b
      d <- getEvenMoreData a c
      print d
    


  1. Gryphon88
    24.05.2017 14:57

    А как грамотно обрабатывать общение с устройствами, например, при работе с libusb? Сначала ищем устройство, открываем его, потом claim interface, потом опционально, шлём тестовый пакет. На каждом шаге можем обломиться, и для каждого шага потребуется своё освобождение ресурсов. Стандартный подход — в разной степени замаскированный switch+goto по меткам, с ручкой проверкой каждого шага обращения к устройству


    1. koldyr
      24.05.2017 17:11

      Возможно ResourceT (MaybeT IO) поможет вам.