
Ramda-style thinking: declarative programming
- Transfer
- Tutorial
1. First steps
2. Combining functions
3. Partial use (currying)
4. Declarative programming
5. Shadowless notation
6. Immutable and objects
7. Immutable and arrays
8. Lenses
9. Conclusion
This post is the fourth part of the series on functional programming called "Ramda style thinking."
In the third part, we talked about combining functions that can take more than one argument, using partial application and currying techniques.
When we begin to write small functional building blocks and combine them, we find that we need to write many functions that will wrap JavaScript operators such as arithmetic, comparison, logic, and flow control. It may seem tedious, but we are behind Ramda.
But first, a small introduction.
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 versus declarative.
Without diving deeper into it, imperative programming is a programming style in which programmers tell the computer what to do, explaining how to do it. Imperative programming gives many constructs that we use every day: Flow Control (
Declarative programming is a programming style in which programmers tell the computer what to do, explaining to them what they want. The computer further must 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 Prolog's set of inference rules uses facts and rules to answer your question.
Functional programming is seen as a subset of declarative programming. In a functional program, we declare functions and then 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 we need to work with. But we need to find ways to express these constructions in a declarative style.
Since we program in JavaScript, an imperative language, it’s 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 with the created code structure.
Let's look at some of the basic building blocks that Ramda provides in order to help us get out of this unpleasant situation.
In the second part, we implemented a series of arithmetic transformations to demonstrate the assembly line:
Notice how we write the functions for all the basic building blocks we want to use.
Ramda provides add , subtract , multiply, and divide functions for use in place of standard arithmetic operations. So we can use the ramd
We may use
So we can simplify our pipeline a little more:
Also in the second part, we wrote several functions to determine whether a person is eligible to vote. The final version of that code was as follows:
Please note that some of our functions use standard comparison operators (
Let's convert our code to using equals instead
Ramda also provides gt for
Note that these functions seem to take their arguments in the normal order (is the first argument larger than the second?). This makes sense when we use them in isolation, but can be confusing when combining functions. These functions violate the “data comes last” principle, so we need to be careful when we use them in our assembly lines and similar situations. And that is where flip and placeholder ( __ ) can benefit.
In addition to
There are a number of cases of main applications for
In the second part (and a little higher), we used the functions
These combined functions work great when functions combine an operation on the same value. Written above
But sometimes we need to apply
It is mainly
This is a common idiom, and most often working, but relying on JavaScript logic for determining "falsity." What if
We can use the function
Controlling the flow of execution is less important in functional programming, but sometimes it is necessary. The collection of iterative functions that we talked about in the first part takes care of most situations with loops, but the conditions are still pretty important.
Let's write a function,
Note that our condition (
Now we are in a position where we can use the ifElse function from Ramda, which is the equivalent of a structure
As we mentioned above, comparison functions do not work like union functions, so here we need to start using placeholder (
In this case, we should read it as "21 is less or equal
Constant functions are very useful in situations like this. As you can imagine, Ramda provides us with a shortcut. In this case, the abbreviation is called always .
Ramda also provides T and F as further abbreviations for
Let's try to write another function
The second branch of comparison (
As you can expect, Ramda provides us with an identity function :
An expression
If, as in our case, the second branch is an identity, we can use when instead of
If the first branch of the condition is an identity, we can use unless . If we turn our condition of use over
Ramda also provides a cond function that can replace an expression
I did not need to use
We looked at the set of functions that Ramda provides us with for transforming our imperative code into declarative functional code.
You may have noticed that the last few functions that we wrote (
This is a common pattern, and again Ramda provides us with tools to bring it all to a cleaner look. The next post, " Pointless Notation, " looks at ways to simplify functions that follow a similar pattern.
2. Combining functions
3. Partial use (currying)
4. Declarative programming
5. Shadowless notation
6. Immutable and objects
7. Immutable and arrays
8. Lenses
9. Conclusion
This post is the fourth part of the series on functional programming called "Ramda style thinking."
In the third part, we talked about combining functions that can take more than one argument, using partial application and currying techniques.
When we begin to write small functional building blocks and combine them, we find that we need to write many functions that will wrap JavaScript operators such as arithmetic, comparison, logic, and flow control. It may seem tedious, but we are behind Ramda.
But first, a small 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 versus declarative.
Without diving deeper into it, imperative programming is a programming style in which programmers tell the computer what to do, explaining how to do it. Imperative programming gives many constructs that we use every day: Flow Control (
if
- then
- else
syntax and cycles), arithmetic operators ( +
, -
, *
, /
), comparison operators ( ===
,>
, <
Etc.), and logical operators ( &&
, ||
, !
). Declarative programming is a programming style in which programmers tell the computer what to do, explaining to them what they want. The computer further must 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 Prolog's set of inference rules uses facts and rules to answer your question.
Functional programming is seen as a subset of declarative programming. In a functional program, we declare functions and then 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 we need to work with. 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’s 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 with the created code structure.
Let's look at some of the basic building blocks that Ramda provides in order to help us get out of this unpleasant situation.
Arithmetic
In the second part, we implemented a series of arithmetic transformations to demonstrate the assembly line:
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 the functions for all the basic building blocks we want to use.
Ramda provides add , subtract , multiply, and divide functions for use in place of standard arithmetic operations. So we can use the ramd
multiply
function where we used the self-written function, we can take the advantage of the curried function add
to replace ours addOne
, and we can also write square
with 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 modifies a variable so that it causes a mutation. As we learned from the first part , immutability is the main principle of functional programming, so we don’t want to use ++
its cousin either --
. We may use
add(1)
and subtract(1)
to increase or decrease, but since these two operations are 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 a negate function to accomplish 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 was as follows:
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 now, Ramda also provides substitutes for all of this. Let's convert our code to using equals instead
===
and gte instead >=
.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 larger than the second?). This makes sense when we use them in isolation, but can be confusing when combining functions. These functions violate the “data comes last” principle, so we need to be careful when we use them in our assembly lines and similar situations. And that is where flip and placeholder ( __ ) can benefit.
In addition to
equals
there is still identical to determine whether two values are references to the same space in memory.There are a number of cases of main applications for
===
: checking that a string or array is empty ( str === ''
or arr.length === 0
) and checking whether a variable is equal to null
or undefined
. Ramda provides convenient functions for both cases: isEmpty and isNil .Logics
In the second part (and a little higher), we used the functions
both
and either
instead of the &&
and operators ||
. We also talked about complement
for places with !
. These combined functions work great when functions combine an operation on the same value. Written above
wasBornInCountry
, wasNaturalized
and isOver18
all applied to the object of the person. But sometimes we need to apply
&&
, ||
and !
to different meanings. For special cases, Ramda provides us with the functions and , or and not . I think as follows: and
, or
and not
are working with values, while both
, either
andcomplement
work with functions. It is mainly
||
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 logic for determining "falsity." What if
0
is a valid parameter? Since it 0
is a false value, we get the line value equal to 80. We can use the function
isNil
that we 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 to isNil
. If the check fails, it will return the received value, otherwise it will return the first argument passed to it.Conditions
Controlling the flow of execution is less important in functional programming, but sometimes it is necessary. The collection of iterative functions that we talked about in the first part takes care of most situations with loops, but the conditions are still pretty important.
ifElse
Let's write a function,
forever21
which gets the year and returns the next one. But, as her name indicates to us, starting at age 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 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 a position where we can use the ifElse function from Ramda, which is the equivalent of a structure
if...then..else
or its shorter cousin, the ternary operator ( ?:
).const forever21 = age => ifElse(gte(__, 21), () => 21, inc)(age)
As we mentioned above, comparison functions do not work like union functions, so here we need to start using placeholder (
__
). We can also apply lte
instead:const forever21 = age => ifElse(lte(21), () => 21, inc)(age)
In this case, we should read it as "21 is less or equal
age
." I am going to stick with the alternate version for the remainder of the post, as I find this more readable and less confusing.Constants
Constant functions are very useful in situations like this. As you can imagine, Ramda provides us with a shortcut. In this case, the abbreviation is called always .
const forever21 = age => ifElse(gte(__, 21), always(21), inc)(age)
Ramda also provides T and F as further abbreviations for
always(true)
andalways(false)
Identity
Let's try to write another function
alwaysDrivingAge
. This function accepts 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 is old enough to drive a car, 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. Trans. ). That is, it is a function that simply returns the argument that it received. As you can expect, Ramda provides us with an identity function :
const alwaysDrivingAge = age => ifElse(lt(__, 16), always(16), identity)(age)
identity
can take more than one argument, but always return only the first. If we want to return something other than the first argument, there is a more general nthArg function for this . This is a much less common situation than use identity
."When" and "unless"
An expression
ifElse
in which one of the logical branches is identity is also a typical pattern, so Ramda provides us with more abbreviation 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 an identity, we can use unless . If we turn our condition of use over
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 an expression
switch
or a chain of expressions if...then...else
.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 did not need to use
cond
Ramda in my code, but I wrote similar code in Lisp many years ago, so it cond
feels like an old friend.Conclusion
We looked at the set of functions that Ramda provides us with for transforming our imperative code into declarative functional code.
Further
You may have noticed that the last few functions that we wrote (
forever21
, drivingAge
and water
) all take parameters, create a new function and then apply this function to the parameter. This is a common pattern, and again Ramda provides us with tools to bring it all to a cleaner look. The next post, " Pointless Notation, " looks at ways to simplify functions that follow a similar pattern.