Functional JavaScript
1. Declarative vs Imperative Approach
- Imperative Approach describes explicit steps for the computer to perform. Specifies the sequence of operations and state modifications.
function sumOfSquaresOfEvens(arr) {
let result = 0;
for (let i = 0; i < arr.length; i++) {
if (arr[i] % 2 === 0) {
result += arr[i] * arr[i];
}
}
return result;
}
- Declarative Approach expresses what the program should accomplish, without detailing the steps. Describes the logic and relationships between entities.
function sumOfSquaresOfEvens(arr) {
return arr
.filter(num => num % 2 === 0)
.map(num => num * num)
.reduce((acc, val) => acc + val, 0);
}
In functional programming, the declarative approach is often preferred for its readability, conciseness, and focus on expressing the logic rather than the step-by-step execution.
2. Functions and Procedures
- Function is the semantic relationship between input and computed output, takes parameters and return a value.
function add(a, b) {
return a + b
}
const result = add(3, 4)
console.log(result) // Output: 7
- Procedure processes the action flow.
function greet(name) {
console.log("Hello, " + name + "!")
}
greet("John") // Output: Hello, John!
3. Side Effects
“Side effect” refers to the modification of state or behavior outside the scope of a function’s return value. Functions with side effects can alter the program’s state, interact with external systems, or produce observable changes beyond their primary purpose of returning a value. Try to avoid them where possible, or extract them into a separate entity to make it more obvious.
- Modifying External State or Global Variables
let counter = 0
function incrementCounter() {
counter++
}
incrementCounter()
console.log(counter) // Output: 1
- Console Logging
function logMessage(message) {
console.log(message)
}
logMessage("This is a log message")
- DOM Manipulation
function changeColor() {
document.body.style.backgroundColor = "lightblue"
}
changeColor()
- Network Requests
Functions can make asynchronous requests to external servers, databases, or APIs, introducing side effects due to the asynchronous nature of the operation.
function fetchData() {
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error))
}
fetchData()
- Random Numbers and Timestamps
function getRandomNumber() {
return Math.random()
}
function getCurrentTimestamp() {
return new Date().getTime()
}
console.log(getRandomNumber()) // Output: A random number between 0 and 1
console.log(getCurrentTimestamp()) // Output: Current timestamp in ms
- High CPU Usage
function timeConsumingOperation() {
for (let i = 0; i < 1000000000; i++) {
// Some time-consuming operation
}
}
timeConsumingOperation()
4. Pure Function is a concept in functional programming that describes a function with two key properties:
- Deterministic: Produces the same output for the same inputs, without randomness.
- No Side Effects: Doesn’t modify external state or interact with the outside world (e.g., global variables).
Benefits include predictability, easier testing, and debugging.
// pure function
function add(a, b) {
return a + b
}
// non-pure function
let total = 10
function addToTotal(value) {
total += value
return total
}
5. Arguments and Parameters
- Arguments: Actual values or expressions passed to a function when it is called, corresponding to the parameters.
const result = add(3, 4)
// 3 and 4 are arguments passed to the 'add' function
- Parameters: Variables listed in the function declaration, acting as placeholders for values.
function add(a, b) {
// 'a' and 'b' are parameters
return a + b
}
5.1. the arguments
object is not an array, but it behaves somewhat like an array. The arguments
object is an array-like object that holds all the parameters passed to a function, regardless of the number of declared parameters in the function's signature. arguments
has a length property, it does not have typical array methods.
function exampleFunction() {
console.log(arguments.length) // Number of arguments
console.log(arguments[0]) // Accessing the first argument
}
exampleFunction(1, "hello", true)
Convert to Array:
function exampleFunction(...args) {
console.log(args.join(', '))
console.log(args[0])
}
exampleFunction(1, "hello", true)
6. Higher-order Function is a function that can take other functions as arguments or return functions as results.
// Higher-order function example
function operate(operation, a, b) {
return operation(a, b)
}
// Functions to pass as arguments
function add(x, y) {
return x + y
}
function multiply(x, y) {
return x * y
}
// Using the higher-order function
const result1 = operate(add, 3, 4) // Output: 7 (3 + 4)
const result2 = operate(multiply, 3, 4) // Output: 12 (3 * 4)
// Higher-order function
function not(fn) {
return function negated(...args) {
return !fn(...args)
}
}
function isOdd(n) {
return n % 2 == 1
}
const isEven = not(isOdd)
isEven(4) // Output: true
function when(fn) {
// predicate is function that returns true or false
return function (predicate) {
return function (...args) {
if (predicate(...args)) {
return fn(...args)
}
}
}
}
7. Composition / Point Free Style
Point-free style encourages the use of function composition to build higher-level abstractions without explicitly naming the variables or arguments involved.
function mod(y) {
return function forX(x) {
return x % y
}
}
function eq(y) {
return function forX(x) {
return x === y
}
}
// isOdd
const mod2 = mod(2)
const eq1 = eq(1)
function isOdd(x) {
return eq1(mod2(x))
}
// Composition
function compose(fn2, fn1) {
return function composed(v) {
return fn2(fn1(v))
}
}
// isOdd: Composition
const isOdd = compose(eq1, mod2)
// or
const isOdd = compose(eq(1), mod(2))
8. Closure is when a function ‘remembers’ the variables around it even when that function is executed elsewhere.
function outerFunction() {
let outerVariable = "I am from the outer function"
function innerFunction() {
console.log(outerVariable)
}
return innerFunction
}
const closureExample = outerFunction()
closureExample() // Output: I am from the outer function
8.1. Lazy and Eager Execution
Lazy and eager execution are two different approaches to when code is executed in a program.
- In Eager Execution, expressions are evaluated or code is executed as soon as it is encountered. The function
eagerExample
is executed immediately when it's called, and the result is stored ineagerResult
.
// Eager execution example
function eagerExample() {
console.log("Executing eagerly")
return 42
}
const eagerResult = eagerExample() // Output: Executing eagerly
console.log(eagerResult) // Output: 42
- In Lazy Execution, code is deferred until it is actually needed. This is often achieved using functions or constructs that delay the execution. The function
lazyExample
returns another function. The inner function is executed separately when it's invoked. This allows for the delay of execution until it's explicitly requested.
// Lazy execution example
function lazyExample() {
console.log("Not executing immediately")
return function() {
console.log("Executing lazily")
return 42
};
}
const lazyResultFunction = lazyExample()
console.log("Some other code") // Output: Not executing immediately
const lazyResult = lazyResultFunction() // Output: Executing lazily
console.log(lazyResult) // Output: 42
8.2. Memoization
Memoization is an optimization technique used to speed up the execution of functions by caching the results of expensive function calls and returning the cached result when the same inputs occur again. This can be particularly useful in recursive or repetitive computations where the same inputs might be encountered multiple times. You can implement memoization by storing the results of function calls in a cache (usually an object) and checking the cache before executing the function. If the function has been called with the same arguments before, you return the cached result instead of recomputing it.
// Without memoization
function fibonacciWithoutMemoization(n) {
if (n <= 1) {
return n
} else {
return fibonacciWithoutMemoization(n - 1) + fibonacciWithoutMemoization(n - 2)
}
}
console.time("Without Memoization")
console.log(fibonacciWithoutMemoization(35))
console.timeEnd("Without Memoization") // Output: 86.40283203125 ms
// With memoization
function fibonacciWithMemoization() {
const cache = {}
return function memoize(n) {
if (n <= 1) {
return n
} else if (cache[n]) {
return cache[n]
} else {
cache[n] = memoize(n - 1) + memoize(n - 2)
return cache[n]
}
};
}
const memoizedFibonacci = fibonacciWithMemoization()
console.time("With Memoization")
console.log(memoizedFibonacci(35))
console.timeEnd("With Memoization") // Output: 0.05908203125 ms
8.3. Referential Transparency
Referential transparency is a property of a function in functional programming paradigms. A function is considered referentially transparent if its output is solely determined by its input parameters, and it doesn’t have any side effects. In other words, if you call a referentially transparent function with the same arguments multiple times, it will always produce the same result.
// Referentially transparent function
function add(a, b) {
return a + b
}
const result1 = add(3, 4) // result1 is 7
const result2 = add(3, 4) // result2 is also 7
// Non-referentially transparent function
let globalVariable = 10
function impureAdd(x) {
globalVariable += x
return globalVariable
}
const impureResult1 = impureAdd(5) // impureResult1 is 15
const impureResult2 = impureAdd(5) // impureResult2 is 20, not guaranteed to be the same
(!) A pure function is a specific type of function that is both referentially transparent and free of side effects. All pure functions are referentially transparent, but not all referentially transparent functions are necessarily pure if they have side effects.
8.4. Currying and Partial Application
Currying is a technique in functional programming where a function is transformed into a sequence of functions, each taking a single argument. The curried function returns a new function with each argument until all arguments are provided, and the final result is produced.
// Non-curried function
function add(x, y, z) {
return x + y + z
}
console.log(add(1, 2, 3)) // Output: 6
// Curried version of the same function
function curriedAdd(x) {
return function(y) {
return function(z) {
return x + y + z
}
}
}
// Curring
console.log(curriedAdd(1)(2)(3)) // Output: 6
// Partial Application
console.log(curriedAdd(1)(2))
console.log(curriedAdd(3))
// Example of Strict Curring (1)(2)
// curry(1)(2)(3)
// Example of Loose Curring (1, 2)
// curry(1, 2)(3)
9. Composition (right-to-left call) vs Piping/Chaining (left-to-right)
- Composition involves combining multiple functions to create new functions, arguments are specified from right to left.
const minus = (x) => x - 2
const multiply = (x) => x * 2
const increment = (x) => x + 1
const shippingRate = x => minus(multiply(increment(x)))
const totalCost = basePrice + shippingRate(4) // Output: basePrice + 8
// Composition
function compose(fn3, fn2, fn1) {
return function composed(v) {
return fn3(fn2(fn1(v)))
}
}
const shippingRate = compose(minus, multiply, increment) // right-lo-left call
const totalCost = basePrice + shippingRate(4) // Output: basePrice + 8
- Piping composing functions by passing the result of one function as an argument to another function, arguments are specified from left to right. Example: reduce method.
function pipe(...fns) {
return function piped(v) {
for (let fn of fns) {
v = fn(v)
}
return v
}
}
const shippingRate = pipe(minus, multiply, increment) // left-lo-right call
- Chaining (particular case of piping) refers to the practice of calling multiple methods on an object in a single expression. This technique is often used to perform a series of operations on an object in a concise and readable way.
// Simple object with methods
const calculator = {
result: 0,
add: function (x) {
this.result += x
return this // Returning the object for chaining
},
multiplyBy2: function () {
this.result *= 2
return this
},
};
// Using method chaining (piping)
const result = calculator.add(3).add(4).multiplyBy2().result
console.log(result) // Output: (3 + 4) * 2 = 14
9.1. Composition with Currying
Allows to reshape (binary to unary) functions as parameters.
const sum = (x, y) => x + y
const triple = (x) => x * 3
const divBy = (y, x) => x / y
divBy(2, triple(sum(3, 5))) // Output: 12
// Currying function
const curry = (fn) => {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args)
} else {
return function (...moreArgs) {
return curried(...args, ...moreArgs);
}
}
}
}
// Composition
function compose(fn3, fn2, fn1) {
return function composed(v) {
return fn3(fn2(fn1(v)))
}
}
const sumCurry = curry(sum)
const divByCurry = curry(divBy)
compose(
divByCurry(2),
triple,
sumCurry(3)
)(5) // Output: 12
10. Immutability
Immutability is a concept in programming that refers to the idea that once an object (such as a variable, array, or object) is created, its state cannot be changed. Benefits of Immutability are predictability, concurrency, implementing undo/redo functionality, debugging. Don’t mutate, copy!
// Mutable approach
let mutableArray = [1, 2, 3]
mutableArray.push(4)
console.log(mutableArray) // Output: [1, 2, 3, 4]
// Immutable approach
const immutableArray = [1, 2, 3]
const newArray = [...immutableArray, 4]
console.log(immutableArray) // Output: [1, 2, 3]
console.log(newArray) // Output: [1, 2, 3, 4]
10.1. Object.freeze()
method in JavaScript is used to freeze an object, making it immutable.
Object.freeze(obj)
freezes the obj
so that its properties cannot be modified or extended.
const obj = {
property1: 42,
property2: "Hello",
}
// Freeze the object
Object.freeze(obj)
// Attempting to modify a property will result in an error in strict mode
obj.property1 = 100 // This will not work
// Attempting to add a new property will also not work
obj.property3 = "New Property" // This will not work
// The object remains unchanged
console.log(obj) // Output: { property1: 42, property2: 'Hello' }
11. Recursion
Recursion is a programming concept where a function calls itself. Every recursive call must have a base condition to avoid overloading.
// Recursive function to calculate factorial
function factorial(n) {
// Base case: factorial of 0 or 1 is 1
if (n === 0 || n === 1) {
return 1
}
// Recursive case: n! = n * (n-1)!
return n * factorial(n - 1)
}
// Example usage
const result = factorial(5)
console.log(result) // Output: 120 (5 * 4 * 3 * 2 * 1)
// Short example of factorial
const factorial = n => n ? n * factorial(n - 1) : 1
// Recursive function to calculate if the string is palindrome (example: 'abcba' is true)
function isPalindrome(str) {
if (str.length <= 1) {
return true
}
const first = str[0]
const last = str[str.length - 1]
if (firsr === last) {
return isPalindrome(str.substring(1, str.length - 1))
}
return false
}
(!) JavaScript engines have a limit on the maximum stack size to prevent stack overflow errors that may occur without considering edge/base cases when using recursion. Typical stack sizes in browsers ranged from around 5,000 to 10,000 frames, but these values can vary between browsers.
11.1. Proper Tail Calls (PTC)
Proper Tail Calls (PTC) refer to a specific optimization feature in JavaScript engines that allows functions to perform tail calls without consuming additional stack space. A tail call is a function call that appears as the last operation in another function. With Proper Tail Calls optimization, JavaScript engines can reuse the current function’s stack frame for the next function call, preventing unnecessary stack growth.
This optimization is particularly relevant for recursive functions, as it helps prevent stack overflow errors that may occur when the call stack becomes too deep due to recursive calls.
In order to be optimized as a proper tail call, a tail call must be in a tail position (the last operation in the function), and the result of the tail call must be the result of the enclosing function.
// Non-optimized tail call
function nonOptimizedTailCall(n) {
if (n === 0) {
return 1
} else {
// Not a proper tail call, as it involves additional stack space
return n * nonOptimizedTailCall(n - 1)
}
}
// Proper tail call (optimized)
function properTailCall(n, accumulator = 1) {
if (n === 0) {
return accumulator
} else {
// Proper tail call, as it reuses the current stack frame
return properTailCall(n - 1, n * accumulator)
}
}
11.2. Continuation Passing Style (CPS)
Continuation Passing Style, and it’s a programming style used in functional programming to handle asynchronous or non-blocking operations. In the example, addCPS
takes three parameters: x
, y
, and cont
. Instead of returning x + y
, it invokes cont(x + y)
. This allows for more flexible control flow and is particularly useful when dealing with asynchronous operations.
// Normal function
function add(x, y) {
return x + y
}
// Same function in CPS
function addCPS(x, y, cont) {
cont(x + y)
}
// Usage of the CPS function
addCPS(2, 3, function(result) {
console.log(result) // Output: 5
})
11.3. Trampolines
A trampoline is a programming technique used to handle recursion in a way that avoids stack overflow errors. In functional programming, where recursion is often favored over iterative loops, trampolines can be particularly useful when dealing with tail-recursive functions.
The factorial
function returns a function that represents the next step of the computation. The trampoline
function repeatedly calls functions until it gets a final result, avoiding deep recursion and potential stack overflow.
function factorial(n) {
if (n <= 1) {
return 1
} else {
return n * factorial(n - 1)
}
}
console.log(factorial(5)) // Potential stack overflow for large n
// Trampoline Technique
function trampoline(fn) {
while (typeof fn === 'function') {
fn = fn()
}
return fn
}
function factorial(n, acc = 1) {
if (n <= 1) {
return acc
} else {
return () => factorial(n - 1, n * acc)
}
}
console.log(trampoline(() => factorial(5))) // No stack overflow, the function is always located in one memory slot
12. List Operations
List Operations can be represented using arrays. Arrays are flexible data structures that allow you to store and manipulate collections of elements.
const myList = [1, 2, 3, 4, 5]
// Accessing Elements
const firstElement = myList[0] // 1
const thirdElement = myList[2] // 3
// Adding Elements
myList.push(6) // myList is now [1, 2, 3, 4, 5, 6]
myList.unshift(0) // myList is now [0, 1, 2, 3, 4, 5, 6]
// Add elements at a specific position .splice(ind, deleteCount, value)
myList.splice(2, 0, 2.5) // myList is now [0, 1, 2, 2.5, 3, 4, 5, 6]
// Removing Elements
myList.pop() // myList is now [0, 1, 2, 2.5, 3, 4, 5]
myList.shift() // myList is now [1, 2, 2.5, 3, 4, 5]
// Remove elements from a specific position .splice(ind, deleteCount, value)
myList.splice(2, 2) // myList is now [1, 2, 4, 5]
// Creates a new list:
// Concatenation
const anotherList = [6, 7, 8]
const combinedList = myList.concat(anotherList) // combinedList is [1, 2, 4, 5, 6, 7, 8]
// Slicing
const slicedList = myList.slice(1, 4) // slicedList is [2, 4, 5]
// Mapping
const squaredList = myList.map(num => num ** 2) // squaredList is [1, 4, 16, 25]
// Filtering
const evenNumbers = myList.filter(num => num % 2 === 0) // evenNumbers is [2, 4]
// Reducing
const sum = myList.reduce((acc, num) => acc + num, 0) // sum is 17
12.1. Immutable Methods: map
, filter
, concat
, slice
, spread operator ...
, reduce
, every
, and some
.
// map
const originalArray = [1, 2, 3]
const newArray = originalArray.map(num => num * 2)
// originalArray is still [1, 2, 3], newArray is [2, 4, 6]
// spread operator
const array1 = [1, 2]
const array2 = [3, 4]
const newArray = [...array1, ...array2]
// array1 and array2 remain [1, 2] and [3, 4], newArray is [1, 2, 3, 4]
// slice
const originalArray = [1, 2, 3, 4, 5]
const newArray = originalArray.slice(1, 4)
// originalArray is still [1, 2, 3, 4, 5], newArray is [2, 3, 4]
12.2. reduce
examples:
// arr.reduce(callbackFn, initialVal)
// callbackFn(accumulator = initialVal || arr[0], curVal, curInd, arr) => return accumulator = callbackFn(...)
// reduce: Summing Array Elements
const originalArray = [1, 2, 3, 4, 5]
const sum = originalArray.reduce((acc, num) => acc + num, 0)
// originalArray is still [1, 2, 3, 4, 5], sum is 15
console.log(sum) // Output: 15
// reduce: Flattening an Array
const nestedArray = [[1, 2], [3, 4], [5, 6]]
const flattenedArray = nestedArray.reduce((accumulator, currentArray) => {
return accumulator.concat(currentArray)
}, []) // [] is initial value
console.log(flattenedArray) // Output: [1, 2, 3, 4, 5, 6]
// reduce: Finding Maximum Value
const values = [10, 5, 8, 3, 17]
const max = values.reduce((accumulator, currentValue) => {
return Math.max(accumulator, currentValue)
}, values[0])
console.log(max) // Output: 17
// reduceRight() applies from right-to-left
// reduce() from left-to-right
12.3. Composition with reduceRight
// Example functions
const addTwo = x => x + 2
const multiplyByThree = x => x * 3
const subtractFive = x => x - 5
// Function composition using reduceRight
const compose = (...funs) => value => funs.reduceRight((acc, fn) => fn(acc), value)
// Compose the functions in reverse order
const composedFunction = compose(subtractFive, multiplyByThree, addTwo)
// Test the composed function
const result = composedFunction(10)
console.log(result) // Output: 31 ((10 + 2) * 3 - 5)
12.4. Fusion
Combination of the compose
function with the map
method to create a new function that applies a composition of functions to each element of an array.
// Example functions
const addTwo = x => x + 2
const multiplyByThree = x => x * 3
const subtractFive = x => x - 5
// Function composition using compose
const compose = (...funs) => value => funs.reduceRight((acc, fn) => fn(acc), value)
// Compose the functions
const composedFuns = compose(subtractFive, multiplyByThree, addTwo)
// Array of values
const numbers = [1, 2, 3, 4, 5]
// Use map and the composed function
const transformedNumbers = numbers.map(composedFuns)
// instead of
// numbers
// .map(subtractFive)
// .map(multiplyByThree)
// .map(addTwo)
console.log(transformedNumbers)
// Output: [4, 7, 10, 13, 16]
13. Transduction
Transduction is composition of reducers. Transducers provide a way to separate the concerns of data transformation and data reduction, allowing for more modular and reusable code.
// Transducer function for mapping (curryied)
const map = mappingFn => reducer => (accumulator, value) =>
reducer(accumulator, mappingFn(value))
// Transducer function for filtering (curryied)
const filter = predicateFn => reducer => (accumulator, value) =>
predicateFn(value) ? reducer(accumulator, value) : accumulator
// Transducer composition function
const compose = (...transducers) =>
transducers.reduce((acc, transducer) => transducer(acc), x => x)
// Example: Doubling even numbers and summing
const data = [1, 2, 3, 4, 5]
const transducer = compose(
map(x => x * 2), // doubling each element
filter(x => x % 2 === 0) // keeping only even numbers
)
const sumReducer = (acc, value) => acc + value
const result = data.reduce(transducer(sumReducer), 0)
console.log(result) // Output: 15
14. Data Structure Traversing
// map
function mapObj(mapperFn, obj) {
const newObj = {}
const keys = Object.keys(obj)
for (let key of keys) {
newObj[key] = mapperFn(obj[key])
}
return newObj
}
// filter
function filterObj(predicateFn, obj) {
const newObj = {}
const keys = Object.keys(obj)
for (let key of keys) {
if (predicateFn(obj[key])) {
newObj[key] = obj[key]
}
}
return newObj
}
// reducer
function filterObj(reducerFn, initialVal, obj) {
let result = initialVal
const keys = Object.keys(obj)
for (let key of keys) {
result = reducerFn(result, obj[key])
}
return result
}
14.1. Monad
A Monad is a design pattern used in functional programming to handle computations that involve side effects, asynchronous operations, or other types of context.
- The
Maybe
monad is often used to represent computations that might fail or have an absent value. It is a generic representation that can either beJust
(with a value) orNothing
(no value).
class Maybe {
constructor(value) {
this.value = value
}
static just(value) {
return new Maybe(value)
}
static nothing() {
return new Maybe(null)
}
flatMap(func) {
return this.value !== null && this.value !== undefined
? func(this.value)
: Maybe.nothing()
}
}
// Example usage
const resultMaybe = Maybe.just(5)
.flatMap(value => Maybe.just(value * 2))
.flatMap(value => Maybe.just(value + 3))
console.log(resultMaybe.value) // Output: 13
- The
Just
monad is a specific instance of theMaybe
monad representing a successful computation with a definite value. This can be achieved by using theMaybe
monad and always providing it with a value.
15. Async/ Observables: Generator Function
A generator function is a special type of function that allows you to control the flow of iteration in a more fine-grained manner. It produces an iterator, which can be used to generate a sequence of values lazily, on-demand.
The function*
syntax is used to define a generator function. Inside the generator function, the yield
keyword is used to produce a value.
Iterator Object: When you invoke a generator function, it returns an iterator object. The iterator has a next
method, which, when called, resumes the execution of the generator until the next yield
statement is encountered.
function* myGenerator() {
yield 1
yield 2
yield 3
}
const generator = myGenerator()
console.log(generator.next()) // { value: 1, done: false }
console.log(generator.next()) // { value: 2, done: false }
console.log(generator.next()) // { value: 3, done: false }
console.log(generator.next()) // { value: undefined, done: true }
Infinite Sequences: Generators are well-suited for representing infinite sequences since they allow you to produce values one at a time, on-demand.
function* infiniteSequence() {
let i = 0
while (true) {
yield i++
}
}
const numbers = infiniteSequence()
console.log(numbers.next().value) // 0
console.log(numbers.next().value) // 1
console.log(numbers.next().value) // 2
// ...
(!) Libraries like Lazy.js
or RxJS
that provide utilities for lazy evaluation and asynchronous operations. These libraries can be useful for building more complex lazy evaluation pipelines in JavaScript. Another useful FP Libraries: Lodash, Ramda.