- PVSM.RU - https://www.pvsm.ru -
Мы как программисты иногда попадаем в "программистский ад", место где наши обычные абстракции не справляются с решением ряда повторяющихся проблем.
В данной статье будут рассмотрены такие проблемы, синтаксические конструкции используемые для их решения и наконец как эти проблемы могут быть решены единообразно с помощью монад.
Данная проблема возникает когда несколько частичных функций (функции которые могут не вернуть значение) нужно выполнить последовательно.
Такие функции обычно приводят в глубоко вложенному и сложно читаемому коду с чрезмерным количеством синтаксического шума.
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 (иногда он называет Option), мы можем соединить эти функции в цепочку с помощью do нотации (используя тот факт что Maybe/Option монадические).
do
a <- getData
b <- getMoreData a
c <- getMoreData b
d <- getEvenMoreData a c
print d
Проблема возникает когда нужно пройтись по нескольким зависимым наборам данных. Так же как и при проверке на 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)
]
Подметив что списки это моданы и использую 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. Обычно данный подход делегирует обработку ошибок с помощью 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 – это еще одно возможное решение (так же Futures/Tasks). В то время как проблема с вложениями частично решена, использовать результат промисов в нескольким местах заставляет нас в ручную создавать лексический скоуп для таких значений. Это приводить в одному уровню вложения для каждой переменной используемой в нескольких местах. К тому же, использование промисов напрямую через then синтаксис выглядит не так чисто как использование async/await
getData().then(a => getMoreData(a)
.then(b => getMoreData(b))
.then(c => getEvenMoreData(a, c))
.then(d => print(d)
);
Уже не должно быть сюрпризом что мы можем решить данную проблемы с помощью такого же подхода как и для двух предыдущих проблем (отметив что промисы образую монаду).
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)
Данная монада позволяет использовать чистое функциональное состояния на которое нет никаких внешних ссылок, что позволяет применять множество полезный операций, например сериализация состояния или реализация таких функций как excursion, что то похожее на то что делаю библиотеки на подобии Redux.
Монада State ограничивает жизненный цикл вычисления с состоянием и гарантируют что программы остаются просты для понимания.
do
a <- getData
b <- getMoreData a
c <- getMoreData b
d <- getEvenMoreData a c
print d
Монады позволяют решить ряд проблем единообразным способом. Вместо того что бы усложнять дизайн и грамматику языка дополнительными синтаксисом, мы можем решить их с помощью монадической библиотеки которая в свою очередь может быть адаптирована под решение ряда других задач.
Автор: dotneter
Источник [1]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/255950
Ссылки в тексте:
[1] Источник: https://habrahabr.ru/post/329242/
Нажмите здесь для печати.