Main Concepts
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
example: if data is mutated some bugs which are hard to find can be hidden
version 1
const stat = [
{name: "John", score: 1.003},
{name: "Lora", score: 2},
{name: "Max", score: 3.76},
];
// expecting to create a new array, but contents in array got modified instead
// this is because inside the stat, item got modified
const statScoreInt = stat.map((el) => {
el.score = Math.floor(el.score);
el.name = el.name;
return el;
});
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 }]version 2
const stat = [
{name: "John", score: 1.003},
{name: "Lora", score: 2},
{name: "Max", score: 3.76},
];
// new copied array got created as expected
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 }]
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
- The variable which cannot be redeclared is created by using
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)
- You can make a value truly immutable by
example
- There are several libraries in JavaScript which try to follow this principle, for example, Immutable.js
const a = Object.freeze({
greeting: "Hello",
subject: "student",
mark: "!",
});
a.greeting = "Goodbye";
// Error: Cannot assign to read only property 'foo' of object Object
Side Effects
- it is a side effect
- if state changes are observable outside the called function
- they are not returned value of the function
- 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
- Modifying any external variable or object property
- In functional programming side effects are mostly avoided
- It makes a program much easier to understand, and much easier to test
- 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
- If the code does not write to or read from a database, does not make any requests, does not change UI, etc.
- to isolate side effects from the rest of your software
- by keeping side effects separately from the rest of the software
- the application will be much easier to extend, refactor, debug, test, and maintain
- by keeping side effects separately from the rest of the software
- 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
- they are loosely coupled modules
- ReactJS and Redux are examples of that
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
JS arrays methods such 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
example 1: Pure function: no side effect
- there are no side effects because price comes as an argument
const doubledPrice = (price) => price * 2;
doubledPrice(2);example 2: not pure function: have side effect
- there is a side effect because
- the price is changed inside the function, but price is declared outside the doubledPrice scope
let price = 2;
const doubledPrice = () => (price = price * 2);
doubledPrice();- there is a side effect because
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
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
example
- Functions
main()
andminor()
do something and wants to log anarr
- Function
logGrocery()
logs elements into console- However, it removes elements from the array while logging them
logGrocery()
breaksminor()
and that is why there is an undefined
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)- Functions
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 stateexample
- Functions
main()
andminor()
do something and wants to log anarr
- Function
logGrocery()
logs elements into console- The code creates a new variable
localArray
, a copy ofarr
- So the
localArray
is modified, and it is a new declaration on each call
- The code creates a new variable
- Avoiding mutations by updating non-destructively
const arr = ["bread", "milk", "wine"];
function logGrocery(arr) {
const localArr = [...arr]; // important
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- Functions
example: 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
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']- We can prevent mutations of shared data by making that data immutable
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
- in order to achieve the desired result, small functions have to be combined together
- 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
Composition over inheritance
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
example: combines the power of objects and functional programming
const dog = (name) => {
const self = {
name,
};
return self;
};
const buddy = dog("Buddy");example 1: using composition
const canSayHi = (self) => ({
sayHi: () => console.log(`Hi! I'm ${self.name}`),
});
const canEat = () => ({
eat: (food) => console.log(`Eating ${food}...`),
});const behaviors = (self) => Object.assign({}, canSayHi(self), canEat());
const dog = (name) => {
const self = {
name,
};
const dogBehaviors = (self) => ({
bark: () => console.log("Ruff!"),
});
return Object.assign(self, behaviors(self), dogBehaviors(self));
};
const buddy = dog("Buddy");
buddy.sayHi(); // Hi! I'm Buddy
buddy.eat("Petfood"); // Eating Petfood...
buddy.bark(); // Ruff!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!example 2: using composition to create a statistic board with the possibility to sort, find all occurrences, and filter by prop
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 to filter and sort in one part of the application and filter and find 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
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 }]