Мы как программисты иногда попадаем в "программистский ад", место где наши обычные абстракции не справляются с решением ряда повторяющихся проблем.
В данной статье будут рассмотрены такие проблемы, синтаксические конструкции используемые для их решения и наконец как эти проблемы могут быть решены единообразно с помощью монад.
Ад проверки на 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
Заключение
Монады позволяют решить ряд проблем единообразным способом. Вместо того что бы усложнять дизайн и грамматику языка дополнительными синтаксисом, мы можем решить их с помощью монадической библиотеки которая в свою очередь может быть адаптирована под решение ряда других задач.