Closures, Curried Functions, and Cool Abstractions in JavaScript

JavaScript is a language full of intriguing concepts and syntax that give it a lot of flexibility and expressiveness. Two of the most powerful features JavaScript offers are closures and function currying. When wielded properly, they allow us to create some pretty cool abstractions and write code in a more functional, reusable style.

In this post, we‘ll dive deep into closures and curried functions, examining how they work under the hood and exploring some practical, real-world examples of the awesome things they let us do. By the end, you‘ll have a solid grasp of these concepts and a few new tools in your JS utility belt to help you write cleaner, more modular, and more expressive code. Let‘s get started!

Understanding Closures

At their core, closures are a natural consequence of how scoping works in JavaScript. When you create a function, it retains access to the environment in which it was created – specifically, any variables that were in scope at the time the function was defined. This combination of a function and its surrounding state is called a closure.

Here‘s a simple example:

function outerFunc() {
  let outerVar = ‘I am outside!‘;

  function innerFunc() {
    console.log(outerVar); // => logs "I am outside!"
  }

  return innerFunc;
}

const myInnerFunc = outerFunc();
myInnerFunc(); // => logs "I am outside!"

In this code, innerFunc is defined inside outerFunc. This means it has access to any variables in the scope of outerFunc, even after outerFunc has returned. When we call outerFunc, it returns innerFunc, which maintains a reference to outerVar. We can then call myInnerFunc later on, and it still has access to outerVar.

This ability of a function to "remember" the environment it was created in allows for some powerful patterns. One common use case is creating private state:

function counter() {
  let count = 0;

  return function() {
    count++;
    console.log(count);
  };
}

const myCounter = counter();
myCounter(); // => logs 1
myCounter(); // => logs 2

Here, the count variable is essentially private to the returned inner function. There‘s no way to access or modify count from outside the closure – the inner function has exclusive access to it. Every time we call myCounter, it increments and logs its private count variable, which persists across invocations.

Demystifying Curried Functions

Function currying is the process of transforming a function that takes multiple arguments into a sequence of functions, each taking a single argument. Curried functions are constructed by chaining together a series of one-argument functions, with each function returning the next one in the chain until all arguments have been supplied.

Here‘s how we might curry a simple function that multiplies two numbers:

function multiply(a, b) {
  return a * b;
}

function curryMultiply(a) {
  return function(b) {
    return a * b;
  };
}

multiply(3, 4); // => 12

const multiplyBy3 = curryMultiply(3);
multiplyBy3(4); // => 12

The curried version, curryMultiply, takes one argument a and returns a function that takes another argument b, which when called will return the product of a and b. We can create specialized multiplier functions by partially applying curryMultiply with a value for a.

The key idea behind currying is that if you don‘t provide all of a function‘s arguments up front, it returns a new function that takes the remaining arguments. This is where closures come into play, allowing each function in the curried chain to access the arguments passed into the previous functions.

With the ES6 arrow syntax, we can make curried functions more concise:

const curryMultiply = a => b => a * b;

Currying is a key technique in functional programming that enables powerful patterns like partial application and function composition. By partially applying a curried function, we can create specialized versions of the function with some of its arguments "baked in".

const curriedGreet = greeting => name => `${greeting}, ${name}!`;

const greetItalian = curriedGreet(‘Ciao‘);
greetItalian(‘Stefano‘); // => ‘Ciao, Stefano!‘

const greetTex = curriedGreet(‘Howdy‘);
greetTex(‘Billy Bob‘); // => ‘Howdy, Billy Bob!‘

Real-World Abstractions

Let‘s look at some more complex, real-world examples of the cool things we can do by combining closures and curried functions.

Custom Event Emitter

Event emitters are a common pattern for managing and triggering callbacks. We can implement a simple event emitter using closures to store the registered event callbacks in a private scope:

function createEmitter() {
  const listeners = {};

  function on(event, callback) {
    listeners[event] = listeners[event] || [];
    listeners[event].push(callback);
  }

  function emit(event, ...args) {
    if (listeners[event]) {
      listeners[event].forEach(callback => callback(...args));
    }
  }

  return { on, emit };
}

const myEmitter = createEmitter();

function handleUserLogin(username) {
  console.log(`User ${username} logged in!`);
}

myEmitter.on(‘login‘, handleUserLogin);

myEmitter.emit(‘login‘, ‘johndoe‘); // => logs "User johndoe logged in!"

The createEmitter function returns an object with on and emit methods that have closure access to the private listeners object storing the registered callbacks. This ensures our event emitter‘s internal state can‘t be modified from the outside.

Fluent API for DOM Manipulation

Chaining together a sequence of curried functions is a great way to create expressive, fluent interfaces. For example, we could create a small library for DOM manipulation:

const $ = selector => document.querySelector(selector);

const addClass = className => element => {
  element.classList.add(className);
  return element;
};

const setHTML = html => element => {
  element.innerHTML = html;
  return element;
};

const onClick = handleClick => element => {
  element.addEventListener(‘click‘, handleClick);
  return element;
};

$(‘#myButton‘)
  |> addClass(‘btn-primary‘)
  |> setHTML(‘Click me!‘)
  |> onClick(() => alert(‘Clicked!‘));

Here, we have a sequence of curried functions that each take a DOM element, perform some operation on it, and return the element to be chained to the next function. The |> operator is the proposed pipeline operator which passes the result of the left expression into the function on the right.

By using curried functions, we can omit the element parameter when chaining, resulting in a fluent, readable API for our DOM operations. Closures allow each chained method to access the original element clicked.

Lightweight State Management

Finally, let‘s see how we can leverage closures and currying to create a simple state management system, similar to libraries like Redux:

function createStore(reducer, initialState) {
  let state = initialState;
  const listeners = [];

  function getState() {
    return state;
  }

  function dispatch(action) {
    state = reducer(state, action);
    listeners.forEach(listener => listener());
  }

  function subscribe(listener) {
    listeners.push(listener);
    return function unsubscribe() {
      const index = listeners.indexOf(listener);
      listeners.splice(index, 1);
    };
  }

  return { getState, dispatch, subscribe };
}

function reducer(state = { count: 0 }, action) {
  switch (action.type) {
    case ‘INCREMENT‘:
      return { count: state.count + 1 };
    case ‘DECREMENT‘:
      return { count: state.count - 1 };
    default:
      return state;
  }
}

const store = createStore(reducer);

const unsubscribe = store.subscribe(() => {
  console.log(‘State changed:‘, store.getState());
});

store.dispatch({ type: ‘INCREMENT‘ }); // => logs "State changed: { count: 1 }"
store.dispatch({ type: ‘INCREMENT‘ }); // => logs "State changed: { count: 2 }"
store.dispatch({ type: ‘DECREMENT‘ }); // => logs "State changed: { count: 1 }"

unsubscribe(); // Remove listener

Our createStore function takes a reducer function for updating the state based on dispatched actions, and an initialState value. It returns an object with getState, dispatch, and subscribe methods which all have closure access to the private state and listeners variables.

The subscribe method takes a listener callback and returns an unsubscribe function, leveraging closures to give it access to the listener for removal.

By dispatching actions, we trigger the reducer to update the state, and notify all subscribed listener callbacks. This pattern allows us to manage a global state object while keeping all the state-updating logic encapsulated within the reducer closure.

Conclusion

Closures and curried functions are two of JavaScript‘s most powerful features. As we‘ve seen, they enable patterns like data privacy, partial application, function composition, fluent APIs, and more. By understanding these concepts deeply, you can start to see opportunities to wield them to create your own clever abstractions.

Of course, as with any abstraction, it‘s important to strike a balance and not overuse these techniques. Highly curried code can become difficult to read and maintain if not structured carefully. Similarly, it‘s possible to create convoluted, deeply nested closures that are hard to reason about. Always strive for a healthy mix of expressiveness and clarity in your code.

I hope diving into these examples has deepened your understanding of closures and currying, and given you some inspiration for new ways to combine them for useful, expressive abstractions in your own projects. Happy coding!

Similar Posts