📜 ⬆️ ⬇️

Ramda Thinking: Declarative Programming

1. First steps
2. We combine functions
3. Partial application (currying)
4. Declarative programming
5. Ruleless Notation
6. Immutability and objects
7. Immutability and arrays
8. Lenses
9. Conclusion

This post is the fourth part of the series on functional programming called "Thinking in the style of Ramda".

In the third part, we talked about combining functions that can take more than one argument, using techniques of partial application and currying.
')
When we start writing small functional building blocks and combining them, we find that we need to write a lot of functions that will wrap JavaScript statements, such as arithmetic, comparison, logic, and flow control. This may seem tedious, but we are behind Ramda.

But first, a little introduction.

Imperative vs Declarative


There are many different ways to separate programming languages ​​and writing styles. These are static typing versus dynamic typing, interpreted languages ​​and compiled languages, high-level and low-level, and so on.

Another similar division is imperative programming against declarative.

Without immersion into it, imperative programming is a programming style in which programmers tell a computer what to do, explaining to it how to do it. Imperative programming provides many constructs that we use every day: flow control ( if - then - else syntax and cycles), arithmetic operators ( + , - , * , / ), comparison operators ( === , > , < , and m .d.), and logical operators ( && , || ,!).

Declarative programming is a programming style in which programmers tell a computer what to do by explaining what they want. The computer must then determine how to obtain the desired result.

One of the classic declarative languages ​​is Prolog. In Prolog, a program consists of a set of facts and a set of inference rules. You start the program by asking a question, and the set of rules for Prolog's output uses facts and rules to answer your question.

Functional programming is considered as a subset of declarative programming. In the functional program, we declare the functions and further explain to the computer what we want to do by combining these functions.

Even in declarative programs, it is necessary to perform similar tasks that we perform in imperative programs. Flow control, arithmetic, comparisons and logic are still the basic building blocks with which we must work. But we need to find ways to express these constructions in a declarative style.

Declarative substitutes


Since we program in JavaScript, an imperative language, it is normal to use standard imperative constructs when writing "normal" JavaScript code.

But when we write functional transformations using pipelines and similar constructions, imperative constructions cease to fit in with the created code structure.

Look at some of the basic building blocks that Ramda provides to help us get out of this unpleasant situation.

Arithmetic


In the second part, we implemented a series of arithmetic transformations to demonstrate the conveyor:

 const multiply = (a, b) => a * b const addOne = x => x + 1 const square = x => x * x const operate = pipe( multiply, addOne, square ) operate(3, 4) // => ((3 * 4) + 1)^2 => (12 + 1)^2 => 13^2 => 169 

Notice how we write functions for all the basic building blocks that we wish to use.

Ramda provides the add , subtract , multiply, and divide functions for use in the field of standard arithmetic operations. So we can use the ramd multiply where we used the samopisnaya function, we can take advantage of the addOne add function to replace our addOne , and we can also write square using multiply .

 const square = x => multiply(x, x) const operate = pipe( multiply, add(1), square ) 

add(1) very similar to the increment operator ( ++ ), but the increment operator changes the variable so that it causes a mutation. As we learned from the first part , immobility is the basic principle of functional programming, so we don’t want to use ++ or its cousin -- .

We can use add(1) and subtract(1) to increase and decrease, but since these two operations are so common, Ramda provides inc and dec instead.

So we can simplify our pipeline a little more:

 const square = x => multiply(x, x) const operate = pipe( multiply, inc, square ) 

subtract is a replacement for the binary operator - , but we still have a unary operator - to negate the value. We can also use multiply(-1) , but Ramda provides the negate function to perform this task.

Comparison


Also in the second part, we wrote several functions to determine whether a person is eligible to vote. The final version of that code looked like this:

 const wasBornInCountry = person => person.birthCountry === OUR_COUNTRY const wasNaturalized = person => Boolean(person.naturalizationDate) const isOver18 = person => person.age >= 18 const isCitizen = either(wasBornInCountry, wasNaturalized) const isEligibleToVote = both(isOver18, isCitizen) 

Please note that some of our functions use standard comparison operators ( === and >= in this case). As you can imagine now, Ramda also provides substitutes for all of this.

Let's convert our code to use equals instead of === and gte instead of >= .

 const wasBornInCountry = person => equals(person.birthCountry, OUR_COUNTRY) const wasNaturalized = person => Boolean(person.naturalizationDate) const isOver18 = person => gte(person.age, 18) const isCitizen = either(wasBornInCountry, wasNaturalized) const isEligibleToVote = both(isOver18, isCitizen) 

Ramda also provides gt for > , lt for < and lte for <=

Note that these functions seem to take their arguments in the normal order (is the first argument greater than the second?). This makes sense when we use them in isolation, but can be confusing when combining functions. These functions violate the “last-minute data” principle, so we need to be careful when we use them in our pipelines and similar situations. And this is the place where the flip and placeholder ( __ ) become useful.

In addition to equals there are still identical ones to determine if two values ​​are references to the same space in memory.

There is a set of cases for the main uses for === : checking that a string or array is empty ( str === '' or arr.length === 0 ) and checking whether a variable is null or undefined . Ramda provides convenient functions for both isEmpty and isNil .

Logics


In the second part (and slightly higher), we used the both and both functions in the places of the operators && and || . We also talked about complement for places with ! .

These combined functions work fine when functions combine operation on the same value. The above wasBornInCountry , wasNaturalized and isOver18 all applied to the person object.

But sometimes we need to apply && , || and ! to different values. For sub-cases, the Ramda provides us with the functions and , or and not . I think as follows: and , or and not work with values, while both , both and complement work with functions.

Mostly || used to get default values. For example, we can write something like this:

 const lineWidth = settings.lineWidth || 80 

This is a common idiom, and most often working, but relying on JavaScript, the logic of determining "falsity." What if 0 is a valid parameter? Since 0 is a false value, we get a line value of 80.

We can use the isNil function, which we have just learned about above, but Ramda again has a more logical option for us: defaultTo .

 const lineWidth = defaultTo(80, settings.lineWidth) 

defaultTo checks the second argument on isNil . If the check fails, it returns the resulting value, otherwise it returns the first argument passed to it.

Conditions


Flow control is less important in functional programming, but sometimes it turns out to be necessary. A collection of iterative functions, which we talked about in the first part , take care of most loop situations, but the conditions are still quite important.

ifElse


Let's write a function, forever21 , which gets the year and returns the next. But, as we are told by her name, from the age of 21, he will remain in that meaning.

 const forever21 = age => age >= 21 ? 21 : age + 1 

Note that our condition ( age >= 21 ) and the second branch ( age + 1 ) can both be written as functions of age . We can rewrite the first branch ( 21 ) as a constant function ( () => 21 ). Now we will have three functions that accept (or ignore) age .

Now we are in position when we can use the ifElse function from Ramda, which is equivalent to the if...then..else or its shorter cousin, the ternary operator ( ?: .

 const forever21 = age => ifElse(gte(__, 21), () => 21, inc)(age) 

As we mentioned above, the comparison functions do not work like the union functions, so here we need to start using the placeholder ( __ ). We can also use lte instead:

 const forever21 = age => ifElse(lte(21), () => 21, inc)(age) 

In this case, we should read this as “21 is less than or equal to age ”. I'm going to stick with the placeholder version for the rest of the post, since I find it more readable and less confusing.

Constants


Constant functions are very useful in situations like this. As you can imagine, Ramda provides us with an abbreviation. In this case, the abbreviation is always called.

 const forever21 = age => ifElse(gte(__, 21), always(21), inc)(age) 

Ramda also provides T and F as further abbreviations for always(true) and always(false)

Identity


Let's try to write another function, alwaysDrivingAge . This function takes age and returns it if its value is gte 16. If it is less than 16, then it will return 16. This allows anyone to pretend that he controls age, even if it is not:

 const alwaysDrivingAge = age => ifElse(lt(__, 16), always(16), a => a)(age) 

The second branch of comparison ( a => a ) is another typical pattern in functional programming. This is known as “identity” ( I don’t know the exact translation of the term “identity function”, just choose this one - approx. Lane ). That is, it is a function that simply returns the argument that it received.

As you can already expect, Ramda provides us with the identity function:

 const alwaysDrivingAge = age => ifElse(lt(__, 16), always(16), identity)(age) 

identity can take more than one argument, but always returns only the first one. If we want to return something else that is different from the first argument, for this, there is a more general function nthArg . This is a much less common situation than using identity .

"When" and "unless"


The ifElse expression, in which one of the logical branches is the identity, is also a typical paternum, so Ramda provides us with more reduction methods.

If, as in our case, the second branch is an identity, we can use when instead of ifElse :

 const alwaysDrivingAge = age => when(lt(__, 16), always(16))(age) 

If the first branch of the condition is identity, we can use unless . If we reverse our use of gte(__, 16) , we can use unless .

 const alwaysDrivingAge = age => unless(gte(__, 16), always(16))(age) 

Cond


Ramda also provides a cond function that can replace a switch or a chain of if...then...else expressions.

 const water = temperature => cond([ [equals(0), always('water freezes at 0°C')], [equals(100), always('water boils at 100°C')], [T, temp => `nothing special happens at ${temp}°C`] ])(temperature) 

I didn't need to use cond in my code with Ramda, but I wrote similar Lisp code many years ago, so cond feels like an old friend.

Conclusion


We looked at the set of functions that Ramda provides us to turn our imperative code into a declarative function code.

Further


You may have noticed that the last few functions that we wrote ( forever21 , drivingAge and water ) all accept the parameters, create a new function and then apply this function to the parameter.

This is a common pattern, and once again, Ramda provides us with the tools to bring it all to a cleaner look. The next post, " Pointless Notation ", discusses ways to simplify functions that follow a pattern like this.

Source: https://habr.com/ru/post/349674/


All Articles