📜 ⬆️ ⬇️

Avoiding Hell with Monads

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.


Null check hell


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); } } } } 

Embedded Solution: Elvis Operator


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); 

Monad Maybe


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 

Cycle hell


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); } } } } 

Embedded solution: List inclusion


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) ] 

List Monad


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 

Hell kolbekov


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) ) 

Embedded solution: async / await


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) } 

Embedded solution: Promises


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) ); 

Monad Continuation


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 

Transfer status hell


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) 

Embedded solution: imperative languages


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) 

Monad State


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 

Conclusion


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