3. Main concepts
3.1 Immutability​
The main rule of an immutable object is it cannot be modified after creation. Conversely, a mutable object is each object which can be modified after creation.
The data flow in the program is lossy if the immutability principle is not followed, that is why it is the main concept of functional programming. For example, Listing 3.1. In case the data is mutated in the program some bugs which are hard to find can hide there.
const stat = [
{ name: "John", score: 1.003 },
{ name: "Lora", score: 2 },
{ name: "Max", score: 3.76 },
];
const statScoreInt = stat.map((el) => { // (1)
el.score = Math.floor(el.score); // (2)
el.name = el.name; // (3)
return el; // (4)
});
console.log(stat); // [{ name: "John", score: 1 }, { name: "Lora", score: 2 }, { name: "Max", score: 3 }]
console.log(statScoreInt); // [{ name: "John", score: 1 }, { name: "Lora", score: 2 }, { name: "Max", score: 3 }]
In the Listing 3.1
the stat
array includes actual data from a database. It has player name
and player score
that shows win percentage. The task is to display the score
to the user as a rounded down integer. In lines from (1) to (4) it goes through array and modifies score
in a needed way and the result of this operation is statScoreInt
array. The last step is to console two arrays stat
and statScoreInt
and the result is unexpectable. Both arrays are equal. That is because inside the map
the stat
item was modified. These kinds of bugs are hard to find and can lead to strange actions. Immutability helps to avoid such behavior. For example, Listing 3.2. The tasks is the same.
const stat = [
{ name: "John", score: 1.003 },
{ name: "Lora", score: 2 },
{ name: "Max", score: 3.76 },
];
const statScoreInt = stat.map((el) => {
return { score: Math.floor(el.score), ...el };
});
console.log(stat); // [{ name: "John", score: 1.003 }, { name: "Lora", score: 2 }, { name: "Max", score: 3.76 }]
console.log(statScoreInt); // [{ name: "John", score: 1 }, { name: "Lora", score: 2 }, { name: "Max", score: 3 }]
There are two arrays stat
and statScoreInt
in Listing 3.2. The difference is in the map
function, instead of modifying stat
item it creates a new element with copied data from stat
item and modified score
value. As a result, there are two arrays with different values.
In JavaScript, it might be easy to confuse const
with immutability. The variable which cannot be redeclared is created by using const
but immutable objects are not created by const
. You can't change the object that the binding refers to, but you can still change the properties of the object, which means that bindings created with const
are mutable.
Immutable objects can't be changed at all. You can make a value truly immutable by deep-freezing the object. JavaScript has a method that freezes an object one-level deep (in order to freeze an object deeply, recursion could be used to freeze each property and nested objects):
const a = Object.freeze({
greeting: "Hello",
subject: "student",
mark: "!",
});
a.greeting = "Goodbye";
// Error: Cannot assign to read only property 'foo' of object Object
There are several libraries in JavaScript which try to follow this principle, for example, Immutable.js.
3.1.1 Side effects​
If state changes are observable outside the called function and they are not returned value of the function it is a side effect.
Side effects include:
- Modifying any external variable or object property (e.g., a global variable, or a variable in the parent function scope chain)
- Logging to the console
- Alert
- Writing to the screen, in other words, replacing the content of a specific tag (querySelector(), getElementById(), etc.)
- Writing to a file
- The HTTP request might have side effects - therefore the function that triggers the request transitively have side effects
- Triggering any external process
- Calling any other functions with side effects
In functional programming side effects are mostly avoided. It makes a program much easier to understand, and much easier to test.
That is important to understand that a program without side effects does nothing. If the code does not write to or read from a database, does not make any requests, does not change UI, etc., it does not bring any value. So we cannot completely avoid side effects.
What we can do is isolate side effects from the rest of your software. In case of keeping side effects separately from the rest of the software, the application will be much easier to extend, refactor, debug, test, and maintain.
That is why a lot of front-end frameworks suggest using state management tools along with the library. Because it separates components rendering from state management, and they are loosely coupled modules. ReactJS and Redux are examples of that.
3.1.2 Pure functions​
A function is called pure if it has the following properties:
- Given the same input, always returns the same output
- Function without side effects
A pure function also can be called a deterministic function.
Such JS arrays methods as: map
, filter
, reduce
etc., are examples of pure function. A pure function does not depend on any state, it only depends on input parameters.
Let's look on the example:
const doubledPrice = (price) => price * 2;
doubledPrice(2);
In this case, there are no side effects because price
comes as an argument. Also, the result will always be 4 if the price
is 2.
Just to compare let's check another example:
let price = 2;
const doubledPrice = () => (price = price * 2);
doubledPrice();
I believe, you already noticed the difference, there is a side effect in this case. The price
is changed inside the function, but price
is declared outside the doubledPrice
scope.
3.2 No shared state​
Shared state is a memory space (could be an object or simple variable) that is reachable from all program parts. In other words, it is global and exists in shared scope. It also could be passed as a property between scopes. If two or more application parts change the same data, then the data is a shared state.
3.2.1 Problems with shared state​
If the state is changing from more than one place in the application, there is a risk of one modification preventing another part of the application to work with the actual data. So it might lead to strange hard to track bugs.
const arr = ["bread", "milk", "wine"];
function logGrocery(arr) {
for (let i = 0; i <= arr.length + 1; i++) {
console.log(arr.shift());
}
}
function main() {
// some code
logGrocery(arr);
}
function minor() {
// some code
logGrocery(arr);
}
main();
minor();
// bread
// milk
// wine
// undefined (1)
In this case, there are three independent parties:
- Functions
main()
andminor()
do something and wants to log anarr
. - Function
logElements()
logs elements intoconsole
. However, it removes elements from the array while logging them.logElements()
breaksminor()
and that is why there is anundefined
in a line (1).
3.2.2 How to avoid it​
- We can avoid shared state by copying data
Until we are reading from a shared state without any modification we are safe. Before doing some modifications we need to "un-share" our state.
Let's try to fix the previous example:
const arr = ["bread", "milk", "wine"];
function logGrocery(arr) {
const localArr = [...arr];
for (let i = 0; i <= localArr.length + 1; i++) {
console.log(localArr.shift());
}
}
function main() {
// some code
logGrocery(arr);
}
function minor() {
// some code
logGrocery(arr);
}
main();
minor();
// bread
// milk
// wine
// bread
// milk
// wine
In this case, there are three independent parties:
Functions
main()
andminor()
do something and wants to log anarr
.Function
logElements()
logs elements intoconsole
. The code creates a new variablelocalArray
, a copy ofarr
. So thelocalArray
is modified, and it is a new declaration on each call. So everything works as expected.Avoiding mutations by updating non-destructively
Let's imagine that we have to add some fruit to our shopping list.
const shoppingList = ["bread", "milk", "wine"];
function addToShoppingList(arr, item) {
return [...arr, item];
}
function main(item) {
// some code
return addToShoppingList(arr, item);
}
const withFruit = main("fruit");
console.log(withFruit); // ['bread', 'milk', 'wine', 'fruit']
console.log(shoppingList); // ['bread', 'milk', 'wine']
- Preventing mutations by making data immutable
We can prevent mutations of shared data by making that data immutable. If data is immutable, it can be shared without any risks. In particular, there is no need to copy defensively.
3.3 Composition​
Function composition is a combination of two or more functions. The single function does a small piece which is not valuable for an application, so in order to achieve the desired result, small functions have to be combined together. You can imagine composing functions as pipes of functions that data has to go through, so that outcome is reached. In functional programming, it is preferable to use composition over inheritance.
3.3.1 Composition over inheritance​
Let's check the example with object composition in JavaScript. This approach combines the power of objects and functional programming. For example, let's create an animal that can talk and eat. Previously, using inheritance we would have abstract class Animal
and a child class TalkingAnimal
. Imagine we had to add more and more animals. In this case, the hierarchy could become messy, since abilities are shared between animals.
Composition helps to solve the problem:
const dog = (name) => {
// (1)
const self = {
name,
};
return self;
};
const buddy = dog("Buddy");
The first step in Listing 3.9 (1) is to create a function that creates an animal (e.g. dog).
The internal variable self
represents the prototype using classes or prototypes.
The next step (2) (Listing 3.10 (2)) is defining behaviors, it will be created as functions receiving the self
. Because they are functions it is easy to combine them. And finally (3), all of these functions have to be merged. Object.assign
or the spread operator ({...a, ...b})
can be used for this purpose.
const canSayHi = (self) => ({
// (1)
sayHi: () => console.log(`Hi! I'm ${self.name}`),
});
const canEat = () => ({
eat: (food) => console.log(`Eating ${food}...`),
});
const behaviors = (self) => Object.assign({}, canSayHi(self), canEat()); // (2)
const dog = (name) => {
const self = {
name,
};
const dogBehaviors = (self) => ({
bark: () => console.log("Ruff!"),
});
return Object.assign(self, behaviors(self), dogBehaviors(self)); // (3)
};
const buddy = dog("Buddy");
buddy.sayHi(); // Hi! I'm Buddy
buddy.eat("Petfood"); // Eating Petfood...
buddy.bark(); // Ruff!
The different behaviors were defined by using the prefix can
. Also, some behavior was combined Listing 3.10, 2 by composition.
Let's create another animal, for example, a cat. The cat can talk and, it can eat as all animals do:
const cat = (name) => {
const self = {
name,
};
const catBehaviors = (self) => ({
meow: () => console.log("Meow!"),
haveLunch: (food) => {
self.eat(food);
},
});
return Object.assign(self, catBehaviors(self), canEat());
};
const kitty = cat("Kitty");
kitty.haveLunch("fish"); // Eating fish...
kitty.meow(); // Meow!
Keep in mind that all functionality was added in the same self
reference, that is a reason why self.eat
can be called within haveLunch
. That allows us to create catBehaviors
on top of other behaviors
.
So composition is easier in maintenance and for reusability purposes. It is easy to refactor the code if needed. Composition is a simple mental model, so there is no need to think in advance of hierarchy, and we can combine all small pieces in the way that we need them to be. For example, Listing 3.12. The task is to create a statistic board with the possibility to sort, find all occurrences, and filter by prop.
const stat = [
{ name: "Lora", score: 1.003 },
{ name: "Lora", score: 1.003 },
{ name: "Lora", score: 2 },
{ name: "Max", score: 3.76 },
];
const sort = (arr) => {
return arr.sort((a, b) => b.score - a.score);
};
const filter = (params) => {
return (arr) => arr.filter((item) => item.name === params);
};
const findAll = (params) => {
return (arr) => arr.filter((item) => item.score === params);
};
const compose = (...funcs) => {
return (arr) => {
return funcs.reverse().reduce((acc, func) => func(acc), arr);
};
};
console.log(compose(filter("Lora"))(stat)); // [{ name: "Lora", score: 1.003 }, { name: "Lora", score: 1.003 }, { name: "Lora", score: 2 }]
console.log(compose(findAll(1.003), filter("Lora"))(stat)); // [{ name: "Lora", score: 1.003 }, { name: "Lora", score: 1.003 }]
console.log(compose(sort, filter("Lora"))(stat)); // [{ name: "Lora",score: 2 }, { name: "Lora",score: 1.003 }, { name: "Lora",score: 1.003 }]
sort
function sorts,findAll
function finds allscore
occurrences,- and
filter
filters byname
, compose
function is a self-invoking* function that can take any number of parameters and execute right-to-left, in other words, performs right-to-left function composition. So, you can compose functions the way you need. There is a possibility tofilter
andsort
in one part of the application andfilter
andfind
in another without any duplication, by composing small reusable parts.
* self-invoking function is a nameless (anonymous) function that is invoked immediately after its definition.