- Published on
Understand JavaScript Composition Once and for All
- Authors
- Name
Photo by Ricardo Gomez Angel on Unsplash
When we embark on the coding journey with JavaScript, we encounter numerous concepts, and Function Composition is one such fascinating concept.
Imagine creating a recipe — you blend different ingredients to create a dish. Function Composition is somewhat similar, blending different functions to achieve a desired result. Let’s break it down and make it as palatable as a well-cooked meal!
What is Function Composition?
Function Composition is a technique in which you combine two or more functions to produce a new function. The idea is to take the output of one function and use it as the input for another.
Mathematically, given two functions f
and g
, their composition is represented as f(g(x))
. Here, g(x)
is computed first, and its result is passed to f
.
const f = (x) => x + 2
const g = (x) => x * 3
// Composing f and g
const composedFunction = (x) => f(g(x)) // f(g(x)) = f(3x) = 3x + 2
console.log(composedFunction(2)) // Outputs 8
Simple Analogy: Making a Sandwich
Let’s relate function composition to making a sandwich, a real-world example most of us are familiar with.
- Bread Slicing Function: You start by slicing the bread.
- Spreading Function: Next, you spread butter, mayo, or any spread of your choice.
- Filling Function: Lastly, you add the fillings — lettuce, tomato, cheese, and so forth.
Each step in making a sandwich can be represented as a function. When you combine these functions, you get a composed function — the process of making a sandwich!
Breaking it Down with JavaScript
Let’s turn our sandwich analogy into JavaScript functions!
// Bread Slicing Function
const sliceBread = (bread) => `${bread} is sliced`
// Spreading Function
const spreadButter = (bread) => `Butter spread on ${bread}`
// Filling Function
const addFilling = (bread) => `Filling added to ${bread}`
// Composing Functions to make a Sandwich
const makeSandwich = (bread) => addFilling(spreadButter(sliceBread(bread)))
console.log(makeSandwich('Whole Wheat'))
// Outputs: "Filling added to Butter spread on Whole Wheat is sliced"
Reusability and Maintainability
One of the key benefits of function composition is reusability. Each function created can be used independently or in conjunction with others. If you wish to make a toast, you can reuse the sliceBread
and spreadButter
functions without the addFilling
function.
const makeToast = (bread) => spreadButter(sliceBread(bread))
console.log(makeToast('White Bread'))
// Outputs: "Butter spread on White Bread is sliced"
Maintainability is another advantage. If you want to upgrade your sandwich with a new spread, you only need to modify the spreadButter
function without touching the other functions.
Higher Order Functions and Composition
In JavaScript, functions that accept other functions as arguments or return functions are called Higher Order Functions. They are the backbone of function composition.
// Composing two functions
const compose = (f, g) => (x) => f(g(x))
const makeSandwich = compose(addFilling, compose(spreadButter, sliceBread))
console.log(makeSandwich('Rye Bread'))
// Outputs: "Filling added to Butter spread on Rye Bread is sliced"
Closure: Composing Functions Smoothly
Closure in JavaScript allows a function to access variables from an enclosing scope, even after the outer function has finished executing. This concept is particularly useful in function composition to maintain the state between different function calls.
const addTopping = (topping) => (bread) => `${topping} added to ${bread}`
const addLettuce = addTopping('Lettuce')
console.log(addLettuce('Butter spread on Whole Wheat is sliced'))
// Outputs: "Lettuce added to Butter spread on Whole Wheat is sliced"
Here is what I wrote in 2017 when I understood currying in a practical way. Maybe it can help you as well.
Now that we have gone through some fundamental topics, let's dive a bit deeper into what else we have around composition. Here are some more advanced topics you can take further:
Professional Composition Libraries
While understanding and implementing basic function composition is crucial, in a professional setting, developers often leverage libraries like Ramda or Lodash/fp. These libraries offer a suite of utility functions that make functional programming and function composition in JavaScript more accessible and manageable.
Ramda Example:
import R from 'ramda'
const sliceBread = (bread) => `${bread} is sliced`
const spreadButter = (bread) => `Butter spread on ${bread}`
const addFilling = (bread) => `Filling added to ${bread}`
// Using Ramda's compose function
const makeSandwich = R.compose(addFilling, spreadButter, sliceBread)
console.log(makeSandwich('Sourdough'))
// Outputs: "Filling added to Butter spread on Sourdough is sliced"
TypeScript and Function Composition
TypeScript, a superset of JavaScript, brings static types to the table. When working with function composition in TypeScript, types add an extra layer of safety, ensuring that each function in the composition chain adheres to a specific contract.
Here’s how you can define a compose
function in TypeScript:
function compose<A, B, C>(f: (b: B) => C, g: (a: A) => B): (a: A) => C {
return (x: A) => f(g(x))
}
const sliceBread = (bread: string) => `${bread} is sliced`
const spreadButter = (bread: string) => `Butter spread on ${bread}`
const addFilling = (bread: string) => `Filling added to ${bread}`
const makeSandwich = compose(addFilling, compose(spreadButter, sliceBread))
console.log(makeSandwich('Multigrain'))
// Outputs: "Filling added to Butter spread on Multigrain is sliced"
I know that typing looks funny, and in fact, there are many other ways to accomplish this. The idea here was more to illustrate a way we can type our compose function using generics. There are more elegant (but more verbose) solutions using reduce to map all arguments, instead of chaining compose
functions, one inside the other.
Monads and Functor Composition
As you deepen your understanding of function composition, you may encounter concepts like Monads and Functors. They are advanced structures that allow you to handle program-wide concerns, such as state or I/O, in a pure functional way.
Monads and Functors can be composed just like regular functions, and they form the basis for advanced functional programming patterns like the Maybe Monad and the Either Monad. Exploring these patterns can provide you with powerful tools to handle errors gracefully and write more robust and maintainable code.
As this may go super deep, I will save this for another post. You have my word.
Point-Free Style
Point-free style or tacit programming emphasizes defining functions without explicitly mentioning their arguments. In JavaScript, higher-order functions (functions that take other functions as arguments or return them) play a significant role in achieving this.
Before Point-Free:
Here’s a simple example without using point-free style:
const double = (x) => x * 2
const increment = (x) => x + 1
const transform = (x) => increment(double(x))
After Point-Free:
Now, adopting a point-free style in pure JavaScript:
const double = (x) => x * 2
const increment = (x) => x + 1
// This is a basic compose function
const compose = (f, g) => (x) => f(g(x))
const transform = compose(increment, double)
Here, the compose
function is the main player, allowing us to chain increment
and double
without explicitly referencing their arguments. When you invoke transform
, it processes the input through both double
and increment
without explicitly detailing how the data flows between these functions.
Why Use Point-Free?
- Conciseness: It reduces verbosity, focusing on operations rather than data.
- Readability: The code emphasizes the transformation process, making it more declarative.
- Reusability: Abstracting away specific arguments leads to more general and reusable functions.
The key takeaway is that point-free style is all about creating functions where data flow is implicit. While libraries like Ramda make this style more accessible, it’s very much possible to employ it using vanilla JavaScript.
And What About Inheritance?
When diving into JavaScript and exploring function composition, it’s common to encounter discussions about another fundamental concept — Inheritance. While both are cornerstones of JavaScript, it’s crucial to distinguish them and understand how they fit into the bigger picture of code design.
Inheritance is a principle of Object-Oriented Programming (OOP) that allows one class to inherit properties and methods from another class. It promotes code reusability and establishes a relationship between the base (parent) class and the derived (child) class.
class Sandwich {
constructor(bread) {
this.bread = bread
}
make() {
return `${this.bread} sandwich is made`
}
}
class GrilledCheeseSandwich extends Sandwich {
make() {
return `Grilled Cheese ${super.make()}`
}
}
const grilledCheese = new GrilledCheeseSandwich('White Bread')
console.log(grilledCheese.make()) // Outputs: "Grilled Cheese White Bread sandwich is made"
In the above example, GrilledCheeseSandwich
is a child class inheriting from the parent class Sandwich
. It overrides the make
method to customize the sandwich-making process.
Composition vs. Inheritance
The debate between composition and inheritance is an ongoing one. While inheritance is about extending properties and behaviors from a base entity, function composition is about combining simple functions to create more complex ones, promoting modularity and reusability.
- Flexibility: Composition offers more flexibility, as it allows you to easily plug in and plug out functionalities. Inheritance can lead to a rigid structure and can be challenging to modify as the application evolves.
- Relationships: Inheritance establishes an “is-a” relationship (GrilledCheeseSandwich is a Sandwich), whereas function composition implies a “has-a” or “uses-a” relationship, indicating how components are related or utilized.
- Reusability: While both concepts promote reusability, function composition does so without tying the code to a specific object structure, making it more adaptable and easier to reason about.
Blending the Concepts
In real-world applications, you might find scenarios where combining inheritance with function composition provides the optimal solution. By carefully assessing the requirements and understanding the strengths and weaknesses of both paradigms, you can leverage them harmoniously to build robust, maintainable, and efficient applications.
While function composition and inheritance may seem like two sides of the same coin, they serve different purposes and are used in varying contexts. Understanding when and how to use each will empower you to write more versatile and effective code.
Final Bite
Function composition is a powerful concept in JavaScript, enabling you to create modular, reusable, and maintainable code. By understanding this technique, you’re not just making sandwiches, but you’re building a versatile kitchen that can create a variety of dishes!
Remember, practice is key. The more you experiment with combining different functions, the more comfortable you’ll become with function composition. So, go ahead, experiment with your recipes, and create some coding delicacies!
Want to connect? Find me at:
- Medium: https://medium.com/@yuribett
- Linkedin: https://www.linkedin.com/in/yuribett/