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