15/16 lessons94%

Practical Use Cases of Closures and Currying

In this lesson, we will explore real-world scenarios where closures and currying are extremely useful in functional programming. Understanding these practical examples will help you apply these concepts effectively in your own projects.

Using Closures for Data Privacy

One of the most common use cases of closures is creating private variables. JavaScript does not have built-in private fields (until recent updates with class syntax), but closures allow you to emulate private data.

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
          function createCounter() {
  let count = 0; // private variable
  return {
    increment: function () {
      count++;
      return count;
    },
    decrement: function () {
      count--;
      return count;
    },
    getCount: function () {
      return count;
    },
  };
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
console.log(counter.decrement()); // 1
        

In this example, count is encapsulated inside the closure, and it's only accessible via the methods defined inside the returned object.

Memoization with Closures

Memoization is a technique to optimize performance by storing the results of expensive function calls and reusing them when the same inputs occur again. Closures are useful to store this cache.

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
          function memoize(fn) {
  const cache = {}; // closure scope
  return function (arg) {
    if (cache[arg]) {
      console.log("Fetching from cache");
      return cache[arg];
    } else {
      console.log("Calculating result");
      const result = fn(arg);
      cache[arg] = result;
      return result;
    }
  };
}

const expensiveComputation = (n) => n * n;

const memoizedComputation = memoize(expensiveComputation);

console.log(memoizedComputation(5)); // Calculating result, 25
console.log(memoizedComputation(5)); // Fetching from cache, 25
console.log(memoizedComputation(6)); // Calculating result, 36
        

Here, the memoize function caches the results of expensiveComputation and retrieves them directly from the closure for previously computed values.

Function Currying for Dynamic Behavior

Currying allows you to create reusable functions that can be pre-configured with specific arguments. It’s especially helpful in situations where you need functions tailored for different scenarios, but with the same underlying logic.

Example: Customizable Logging Function

javascript
1
2
3
4
5
6
7
8
9
10
11
          function logger(level) {
  return function (message) {
    console.log(`[${level}] - ${message}`);
  };
}

const infoLogger = logger("INFO");
const errorLogger = logger("ERROR");

infoLogger("This is an informational message.");
errorLogger("This is an error message.");
        

In this case, the logger function is curried to allow the level to be set once, and then the function can be reused for different messages.

Partial Application and Currying in UI Event Handling

Currying and partial application are especially helpful in UI development, where you often need to create event handlers that require certain parameters. These functions can be curried or partially applied to create more flexible event handlers.

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
          function createEventHandler(element, eventType) {
  return function (callback) {
    element.addEventListener(eventType, callback);
  };
}

const button = document.createElement("button");
document.body.appendChild(button);
button.textContent = "Click Me!";

const clickHandler = createEventHandler(button, "click");

clickHandler(() => {
  console.log("Button clicked!");
});
        

Here, the event handler is created using currying, allowing us to pre-configure the button and event type, and later provide the callback when the event happens.

Using Currying for Configurable Functions

You can also use currying to create functions that are adaptable to different scenarios, such as a function that generates customized messages based on a template.

javascript
1
2
3
4
5
6
7
8
9
10
11
          function messageGenerator(greeting) {
  return function (name) {
    return `${greeting}, ${name}!`;
  };
}

const helloMessage = messageGenerator("Hello");
const goodbyeMessage = messageGenerator("Goodbye");

console.log(helloMessage("John")); // Hello, John!
console.log(goodbyeMessage("Jane")); // Goodbye, Jane!
        

By currying, we create flexible functions that are easy to configure and reuse in various contexts.

Chaining Operations with Currying

Currying also facilitates chaining multiple operations. Each curried function can return a new function, which can be chained with others.

javascript
1
2
3
4
5
          const applyDiscount = (price) => (discount) => price - (price * discount);
const applyTax = (price) => (taxRate) => price + (price * taxRate);

const finalPrice = applyTax(applyDiscount(100)(0.1))(0.2);
console.log(finalPrice); // 108
        

In this case, we’re chaining two functions, each returning a curried function, to calculate a final price with a discount and tax.

Conclusion

  • Closures help encapsulate private data and are great for data privacy and memoization.
  • Currying allows for function reusability and creates specialized functions tailored to different needs.
  • Practical use cases like event handling, logging, and configurable functions demonstrate how these concepts can be applied to real-world scenarios.