Мы как программисты иногда попадаем в "программистский ад", место где наши обычные абстракции не справляются с решением ряда повторяющихся проблем.
В данной статье будут рассмотрены такие проблемы, синтаксические конструкции используемые для их решения и наконец как эти проблемы могут быть решены единообразно с помощью монад.
Ад проверки на 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)
Scf
23.05.2017 11:19+6При использовании монад всплывают вложенные монады, state-монады с несовместимыми типами, монадические трансформеры....
И можно готовить следующую статью:
Избегание ада монад с помощью ???
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 к прекрасному тоже можно стремитсяdotneter
23.05.2017 19:41Haskell как раз используется для примеров потому что есть сахар, а так конечно, Maybe и прочие абстракции можно использовать в любом языке.
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)
Но тут скорее всего стоит как-то перепроектировать систему и повысить уровень абстракции.
master65
24.05.2017 08:57Универсальное решение всех проблем!
do a <- getData b <- getMoreData a c <- getMoreData b d <- getEvenMoreData a c print d
Gryphon88
24.05.2017 14:57А как грамотно обрабатывать общение с устройствами, например, при работе с libusb? Сначала ищем устройство, открываем его, потом claim interface, потом опционально, шлём тестовый пакет. На каждом шаге можем обломиться, и для каждого шага потребуется своё освобождение ресурсов. Стандартный подход — в разной степени замаскированный switch+goto по меткам, с ручкой проверкой каждого шага обращения к устройству
nikitasius
А монады-то прикольные штуки! Без них вполне читабельное решение влоб и не такой уж и ад (на haskwell не кодил!) :