As programmers, we sometimes find ourselves in a “programmer hell”, a place where our usual abstractions do not cope with solving a number of recurring problems.
This article will discuss such problems, the syntactic constructs used to solve them, and finally how these problems can be solved uniformly with the help of monads.
This problem occurs when several partial functions (functions that may not return a value) must be executed sequentially.
Such functions usually result in deeply embedded and hard-to-read code with an excessive amount of syntactic noise.
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); } } } }
This is a special (?.) Syntax that helps navigate between calls to partial functions. Unfortunately, it is unnecessarily tied to the object-oriented style of entries and access to methods.
var a = getData(); var b = a?.getMoreData(); var c = b?.getMoreData(); var d = c?.getEvenMoreData(a); print(d);
If our functions explicitly return the type Maybe (sometimes he calls Option), we can chain these functions using do notation (using the fact that Maybe / Option is monadic).
do a <- getData b <- getMoreData a c <- getMoreData b d <- getEvenMoreData ac print d
The problem arises when you need to go through several dependent data sets. Just like when checking for null, the code becomes deeply nested with a lot of syntactic noise.
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); } } } }
A more elegant solution to the problem was found with the introduction of a special syntactic structure called list inclusion, much like SQL ( for example, in C # the similarity reaches a maximum in approx. Transl. ).
[ print(d) for a in getData() for b in getMoreData(a) for c in getMoreData(b) for d in getEvenMoreData(a, c) ]
Having noticed that the lists are monads and using do-notation, you can write the same elegant solution without any additional syntax.
do a <- getData b <- getMoreData a c <- getMoreData b d <- getEvenMoreData ac print d
The most famous and possibly the most painful circle of hell. Here, the inversion of control is necessary for the implementation of asynchrony that is conducted to deeply nested code and syntactic noise, difficulty in tracking error handling and a number of other sores.
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) )
To overcome this complexity, another special syntax was invented - async / await. Typically, this approach delegates error handling using the try / catch syntax, which in itself leads to another hell.
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 is another possible solution (also Futures / Tasks). While the problem with attachments is partially solved, using the result of promises in several places forces us to manually create a lexical script for such values. This will result in one level of investment for each variable used in several places. In addition, using promises directly through the then syntax is not as clean as using async / await
getData().then(a => getMoreData(a) .then(b => getMoreData(b)) .then(c => getEvenMoreData(a, c)) .then(d => print(d) );
It should no longer be a surprise that we can solve this problem using the same approach as for the two previous problems (noting that promises form a monad).
do a <- getData b <- getMoreData a c <- getMoreData b d <- getEvenMoreData ac print d
Even without side effects in the world of pure functions there are difficulties. In some cases, excessive parameter transfer between functions can be a problem.
let (a, st1) = getData initalState (b, st2) = getMoreData (a, st1) (c, st3) = getMoreData (b, st2) (d, st4) = getEvenMoreData (a, c, st3) in print(d)
This problem can be solved by using an implicit state, which allows functions to communicate with each other without explicitly passing all parameters. Unfortunately, the use of an imperative model significantly complicates the understanding of the code. The life cycle and size of the state usually has no static bounds.
a = getData(); b = getMoreData(a); c = getMoreData(b); d = getEvenMoreData(a, c); print(d)
This monad allows you to use pure functional states for which there are no external links, which allows you to use many useful operations, such as state serialization or the implementation of functions such as excursion, something similar to what I do libraries like Redux.
The State Monad limits the life cycle of computing with the state and ensures that the programs remain easy to understand.
do a <- getData b <- getMoreData a c <- getMoreData b d <- getEvenMoreData ac print d
Monads can solve a number of problems in a uniform way. Instead of complicating the design and grammar of the language with additional syntax, we can solve them with the help of a monadic library which in turn can be adapted to solve a number of other problems.
Source: https://habr.com/ru/post/329242/
All Articles