A Pure Function is a function (a block of code) that always returns the
same result if the same arguments are passed. It does not depend on any
state or data change during a program’s execution. Rather, it only depends
on its input arguments.It does not produces any side effects.
Function which are not pure are called impure functions.
Basically a pure function must pass two tests to be considered “pure”:
1) Same inputs always return same outputs
2) No side-effects
Code:
function calculateGST(productPrice) {
return productPrice * 0.05;
}
console.log(calculateGST(15))
Explanation:
The above function will always return the same result if we pass the
same product price. In other words, its output doesn’t get affected
by any other values/state changes. So we can call the “calculate GST”
function a Pure Function.
Lets see some more examples of pure function:
Code:
// max of arguments
Math.max(1, 2, 3)
// nearest lowest integer
Math.floor(1.23)
// The multiplication of two numbers
function multiply(a, b) {
return a * b
}
// Summarizing the array items
function sumOfArray(array) {
return array.reduce((sum, item) => sum + item)
}
// Returning a constant value
function answer() {
return 42
}
// Function that returns nothing (noop)
function noop() {
// nothing
}
Now let's look at the second requirement of a pure function: do not produce a
side effect.
A side effect is change to external state or environment outside of the
function scope.
Examples of side effects are:
1) changing variables and objects defined outside the function scope
2) logging to console
3) changing document title
4) DOM manipulations
5) making HTTP requests
6) modification of function parameter
7) accessing external state
Logging to console:
Code:
function sumSideEffect(a, b) {
const s = a + b
console.log(s) // Side effect!
return s
}
Explanation:
If the sumSideEffect() function logs to the console, then the function
is not pure because it produces a side effect.let value = 0
Changing variables and objects defined outside the function scope:
Code:
let value = 0
function add(increase) {
value += increase // Side-effect
return value
}
console.log(add(2)) // logs 2
console.log(add(2)) // logs 4
Output:
2
4
Explanation:
add() function is impure because it produces a side effect: modifies value
variable accessed from the outer scope. The function also returns different
values for the same arguments.
Function Parameter modification:
Code:
function addProperty(object) {
// Mutates the parameter object (side effect)
Object.assign(object, { b: 1 })
}
Explanation:
A JavaScript function that modifies the value of its parameter is not
considered a pure function.
More concise example:
Code:
// Function with side effect (modifying parameter)
function addToParameter(obj, value) {
obj.prop += value;
}
let myObj = { prop: 5 };
addToParameter(myObj, 3);
console.log(myObj.prop); // Output: 8 (modified value)
addToParameter(myObj, 3);
console.log(myObj.prop); // Output: 11 (further modification)
Output:
8
11
Explanation:
Here we are calling addToParameter(myObj, 3) twice but each time result
is also different.
DOM manipulations:
Code:
function deleteById(id) {
// Modifies DOM (side effect)
document.getElementById(id).remove()
}
Explanation:
This function is not pure as it makes DOM manipulation
Making HTTP requests:
Code:
async function fetchEmployees() {
// Accesses the networks (external state)
const response = await fetch('https://example.com/employees/')
return response.json()
}
Explanation:
This function is not pure as it is making http request.
Accessing External State:
Code:
function screenSmallerThan(pixels) {
// Accesses the browser page (external state)
const { matches } = window.matchMedia(`(max-width: ${pixels})px`)
return matches
}
Explanation:
This function is impure because it accesses external state
(window.matchMedia) which can change over time and is not controlled
within the function itself.
Pure functions do not rely on or modify external state; they only depend on
their input parameters and produce a deterministic output based solely on
those parameters.
what is immutable merge operation ?
An immutable merge operation refers to combining two or more data structures
(such as objects or arrays) without modifying the original data structures.
This is commonly used in functional programming and immutable data structures
to ensure data integrity and avoid side effects.
Transforming Impure Function to Pure Function:
Some impure functions can be transformed into pure by refactoring mutable
operations to immutable.
Here is an impure function.
Code:
function addDefaultsImpure(original, defaults) {
return Object.assign(original, defaults)
}
const original = { a: 1 }
const result = addDefaultsImpure(original, { b: 2 })
console.log(original) // logs { a: 1, b: 2 }
console.log(result) // logs { a: 1, b: 2 }
Explantion:
The function is impure because the parameter original is mutated.
Let's make the function pure by using an immutable merge operation:
Code:
function addDefaultsPure(original, defaults) {
return Object.assign({}, original, defaults)
}
const original = { a: 1 }
const result = addDefaultsPure(original, { b: 2 })
console.log(original) // logs { a: 1 }
console.log(result) // logs { a: 1, b: 2 }
Explanation:
Object.assign({}, object, defaults) doesn't alter neither original nor
defaults objects. It just creates a new object.addDefaultsPure() is
now pure and has no side effects.
Immutable Merge for Objects:
Code:
// Using the spread syntax (ES6)
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
// Merge obj1 and obj2 immutably
const mergedObj = { ...obj1, ...obj2 };
console.log(mergedObj); // Output: { a: 1, b: 3, c: 4 }
// Using Object.assign() (pre-ES6)
const mergedObjOld = Object.assign({}, obj1, obj2);
console.log(mergedObjOld); // Output: { a: 1, b: 3, c: 4 }
Immutable Merge for Arrays:
// Using concat() method
const arr1 = [1, 2, 3];
const arr2 = [4, 5];
const mergedArr = arr1.concat(arr2); // Merge arr1 and arr2 immutably
console.log(mergedArr); // Output: [1, 2, 3, 4, 5]
// Using the spread syntax (ES6)
const mergedArrES6 = [...arr1, ...arr2];
console.log(mergedArrES6); // Output: [1, 2, 3, 4, 5]
In last two examples (objects and arrays), we're creating a new merged data
structure (mergedObj or mergedArr) without modifying the original data
(obj1, obj2, arr1, arr2). This ensures immutability and prevents unintended
side effects that can occur with mutable operations.
Benifits of Pure function:
Pure functions offer several benefits that contribute to writing robust,
predictable, and maintainable code.
1) Deterministic Behavior: Pure functions always produce the same output
for the same input. This determinism makes your code easier to reason
about and test since you don't have to worry about unexpected side
effects or external state changes affecting function behavior.
2) No Side Effects: Pure functions do not cause side effects such as
modifying global variables, mutating data structures, or performing I/O
operations. This property helps in isolating and controlling the effects
of your code, reducing bugs related to unintended state changes.
3) Easy Testing: Because pure functions rely only on their input
parameters and produce predictable outputs, they are straightforward to
test. Unit testing pure functions involves providing inputs and asserting
the expected outputs, making it easier to write comprehensive test suites.
4) Concurrency and Parallelism: Pure functions are inherently thread-safe
and can be executed concurrently without concerns about shared mutable
state. This property is crucial for leveraging parallel computing and
improving performance in multi-threaded or distributed systems.
5) Referential Transparency: Pure functions exhibit referential
transparency, meaning you can replace a function call with its result
without changing the program's behavior. This property aids in code
optimization, caching, and reasoning about program transformations.
6) Modularity and Composition: Pure functions encourage modular code
design and composition. You can compose pure functions together to
create more complex behavior while maintaining clarity and predictability.
This composability enhances code reusability and makes codebases easier
to extend and refactor.
7) Debugging and Refactoring: Pure functions simplify debugging and
refactoring tasks. Since they have no hidden dependencies or side
effects, you can refactor them confidently without worrying about
unintended consequences. Debugging is also more straightforward
since pure functions isolate behavior based solely on inputs.
8) Functional Programming Benefits: Pure functions align with functional
programming principles, promoting functional purity, immutability, and
declarative programming styles. Embracing pure functions can lead
to cleaner, more concise code that is easier to maintain and reason
about.
By leveraging pure functions appropriately in your codebase, you can
improve code quality, enhance reliability, and facilitate better
collaboration among developers. These benefits contribute to building
scalable and maintainable software systems, especially in complex and
critical applications.
Benifits of Pure function by example:
Predictability:
Given the same arguments it always returns the same value.
The pure function is also easy to test. The test just has to supply
the right arguments and verify the output:
describe('sum()', () => {
it('should return the sum of two numbers', () => {
expect(sum(1, 2)).toBe(3)
})
})
Because the pure function doesn't create side effects,
the test doesn't have to arrange and clean up the side effect.
Memoization:
The pure function that makes computationally expensive calculations can
be memoized. Because the single source of truth of a pure function is
its arguments they can be used as cache keys during memoization.
factorial() function is a pure function. Because factorial computation
is expensive, you can improve the performance of the function by wrapping
factorial into a memoize() wrapper
Code:
var memoize = require("lodash").memoize;
function factorial(n) {
if (n === 0) {
return 1;
}
return n * factorial(n - 1);
}
const memoizedFactorial = memoize(factorial);
console.log(memoizedFactorial(5)); // logs 120
console.log(memoizedFactorial(5)); // logs 120
Explanation:
Here, memoizedFactorial(5) is called twice. The first call calculates
the factorial of 5 (5! = 120) and caches the result. The second call
with the same argument immediately returns the cached result without
recomputing, resulting in 120 being logged both times.
By memoizing the factorial function, redundant calculations for the
same input values are avoided, leading to improved performance for
repeated function calls with identical arguments.
Composition of function:
Composition of function refers to combining multiple functions together
to create new functions.
Pure functions lend themselves well to composition due to several
reasons:
Code:
// Pure functions
function double(x) {
return x * 2;
}
function addOne(x) {
return x + 1;
}
// Composing pure functions
const composedFunction = (x) => addOne(double(x));
console.log(composedFunction(3)); // Output: 7 (double(3) + 1)
Explanation:
In this example, double and addOne are pure functions, and we compose
them together using arrow function syntax to create a new function
(composedFunction) that doubles a number and then adds one. The
composition is straightforward and relies on the deterministic
behavior of pure functions.
Referential transparency:
Referential transparency is a concept from functional programming that
refers to the property of an expression or function where it can be
replaced with its corresponding value without changing the program's
behavior.
Here's a simple example to illustrate referential transparency.
Code:
// Pure function
function add(a, b) {
return a + b;
}
// Referentially transparent expressions
const result1 = add(2, 3); // Result: 5
const result2 = add(2, 3); // Result: 5
console.log(result1 === result2);
Explanation:
In this example, the add function is referentially transparent
because it always returns the same result for the same inputs
(2 and 3). The result1 and result2 variables are equal
because the expression add(2, 3) can be replaced with its result
5 without changing the program's behavior.
Here are key points related to referential transparency:
1) Predictability: Referentially transparent expressions are predictable
because their behavior is solely determined by their inputs. Given the
same inputs, a referentially transparent expression will always produce
the same output.
2) Substitution Principle: Referential transparency allows you to
substitute an expression with its value anywhere it appears in the code
without altering the program's semantics. This makes code
easier to reason about and facilitates optimization and refactoring.
3) Pure Functions: Pure functions are an example of referentially
transparent functions. They have no side effects and always return the
same output for the same inputs. Because of this property,
pure functions can be freely composed and replaced in code without
causing unexpected behavior.
4) Immutable Data: Immutable data structures also contribute to
referential transparency. When data is immutable (unchangeable),
operations on that data are referentially transparent because
they don't modify the original data but create new copies or versions
instead.
5) Functional Programming: Referential transparency is a fundamental
concept in functional programming paradigms. It promotes declarative
programming styles and encourages writing code that focuses on
computations rather than mutable state or side effects.
6) Benefits: The benefits of referential transparency include easier
testing, code optimization, reasoning about program behavior, and
facilitating parallelism and concurrency in concurrent systems.
Impure functions:
A function that can return different values given the same arguments
or makes side effects is named impure function.
In practice, a function becomes impure when it reads or modifies an
external state.
A good example of an impure function is the built-in JavaScript
random generator Math.random():
Code:
console.log(Math.random()) // logs 0.8891108266488603
console.log(Math.random()) // logs 0.9590062769956789
Explanation:
Math.random(), given the same arguments (in this case no arguments
at all), returns different numbers smaller than 1. This makes the
function impure.
Impact of impure functions on codebase:
Impure functions have a higher complexity compared to pure functions.
Complexity is added by accessing external states or by side effects.
Because of their higher comlexity impure functions are harder to test.
You have to mock the external state or the side effect to understand
if the function works correctly.
Dealing with impure functions:
Here are some approaches and best practices for dealing with impure
functions:
Isolate Impure Logic: Whenever possible, isolate impure logic into
separate functions or modules. This helps contain the effects of impure
functions and makes it easier to reason about and test the rest of your
codebase, especially if the impure logic involves I/O operations, side
effects, or mutable state.
Encapsulate Side Effects: If you must work with impure functions,
encapsulate their side effects or interactions with external state in a
controlled manner. For example, use design patterns like the repository
pattern to encapsulate database interactions, or use services to handle
I/O operations, isolating these concerns from the core logic of your
application.
Minimize Global State: Minimize the use of global variables or mutable
state within your application. Impure functions that rely heavily on
global state can introduce hidden dependencies and make it challenging
to reason about program behavior. Consider using dependency injection
or functional programming techniques to pass dependencies explicitly to
functions, reducing reliance on global state.
Use Functional Composition: Embrace functional composition to combine
pure functions and isolate impure functions' effects. By composing
functions together, you can create higher-level abstractions while
keeping impure logic contained and manageable. Functional composition
also promotes code reusability and maintainability.
Mocking and Testing: For impure functions that interact with external
systems or dependencies, use mocking frameworks or techniques to simulate
those interactions during testing. Mocking helps isolate the behavior
of impure functions and ensures that tests focus on the function's logic
rather than external dependencies.
Documentation and Conventions: Clearly document impure functions and
their side effects. Use naming conventions or annotations to indicate
impure functions in your codebase, making it easier for developers to
identify and understand where side effects or external state changes
occur.
Refactor Over Time: Consider refactoring impure functions into pure or
more controlled versions over time. Refactoring can help reduce the
complexity and risk associated with impure functions, improving code
maintainability and reliability.
Code Reviews and Peer Feedback: Conduct thorough code reviews and seek
feedback from peers to identify potential issues or areas where impure
functions can be refactored or improved. Collaborative efforts can lead
to better-designed code and reduce the impact of impure functions on
your application's overall architecture.
By following these approaches and best practices, you can effectively
manage and mitigate the challenges posed by impure functions, leading
to a more maintainable, testable, and reliable codebase.
There's nothing wrong with the impure functions. They are a necessary evil
for the application to communicate with the external world.
No comments:
Post a Comment