Functional JavaScript

TheTechnoCult
16 min readDec 3, 2023

--

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 in eagerResult.
// 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 be Just (with a value) or Nothing (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 the Maybe monad representing a successful computation with a definite value. This can be achieved by using the Maybe 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.

--

--

No responses yet