Skip to Content
Course content

114: Practice Exercise: Building a Pipe/Compose Utility

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When I first started digging into functional programming in JavaScript, I hit a wall with the concept of "piping." I kept thinking that a pipe utility was just a fancy wrapper for a for loop. I thought, "Why do I need a complex utility when I can just iterate over an array of functions and update a variable?"

Thinking a Pipe is just a loop

Here is the mistake I made. I wrote a "pipe" that looked something like this:

function runPipe(fns, value) {
  let result = value;
  for (const fn of fns) {
    result = fn(result);
  }
  return result;
}

// Usage:
const trim = s => s.trim();
const uppercase = s => s.toUpperCase();
runPipe([trim, uppercase], "  hello world  "); // "HELLO WORLD"

Technically, this works. It gets the job done. But it's not actually a pipe utility in the software engineering sense—it's just a runner. The problem here is that the data (the string) and the logic (the functions) are tied together immediately. I can't reuse that specific pipeline elsewhere in my app without passing the data in every single time. In a real production environment, you often want to define the transformation logic once and then apply it to a stream of data later.

Creating a Function Factory

The "aha!" moment for me was realizing that pipe should be a higher-order function. It shouldn't execute the logic immediately; it should return a new function that embodies the pipeline. This allows you to define a specific process (like a data sanitizer) and pass that process around as a first-class citizen.

The cleanest way to implement this in modern JavaScript is using reduce. Instead of managing a let variable in a loop, we use the accumulator to pass the result of one function into the next.

const pipe = (...fns) => (initialValue) => 
  fns.reduce((acc, fn) => fn(acc), initialValue);

// Now, look at the difference in usage:
const trim = s => s.trim();
const uppercase = s => s.toUpperCase();
const exclaim = s => `${s}!`;

// We create a specialized function here. No data has been processed yet.
const prepareGreeting = pipe(trim, uppercase, exclaim);

// Now we can use that "pipeline" whenever we want
console.log(prepareGreeting("  hello  ")); // "HELLO!"
console.log(prepareGreeting("  good morning  ")); // "GOOD MORNING!"

I personally love this pattern because it reads like a recipe. You define the steps from top to bottom (or left to right), and the logic remains decoupled from the data it eventually processes.

The Mirror Image: Compose

You'll often hear compose mentioned alongside pipe. They are mathematically the same, but the execution order is flipped. While pipe goes left-to-right, compose goes right-to-left. This is a holdover from mathematical notation f(g(x)), where the innermost function executes first.

To build a compose utility, you only need to change one thing: the order of the functions. Using reduceRight instead of reduce flips the execution flow perfectly.

const compose = (...fns) => (initialValue) => 
  fns.reduceRight((acc, fn) => fn(acc), initialValue);

// With compose, the LAST function listed is the FIRST one to run.
const processText = compose(exclaim, uppercase, trim); 
// trim -> uppercase -> exclaim

In my experience, pipe is much more intuitive for most developers because we read from left to right, but you'll see compose everywhere if you start using libraries like Redux. Now that you've seen how the factory pattern works, let's put it into practice.




📋 Practical Task

Exercise: Building a User Profile Sanitizer Pipeline

You are building a user registration system. Before the user data is saved to the database, it needs to go through a series of cleaning steps. Your goal is to create a compose utility and use it to build a sanitization pipeline.

Requirements:

  • Implement a compose function that takes any number of functions as arguments and returns a new function.
  • Create three small helper functions:
    1. trimWhitespace: Removes leading and trailing spaces from a string.
    2. toLowerCase: Converts a string to lowercase.
    3. escapeHtml: Replaces the < character with &lt; and the > character with &gt;.
  • Use your compose utility to create a function called sanitizeInput. Crucially, because you are using compose, you must list the functions in the order that they should be executed last to first.
  • Test your sanitizeInput function with the string: " <script>alert('hi')</script> ". The result should be trimmed, lowercased, and have the HTML characters escaped.
// Your code here:

Rating
0 0

There are no comments for now.

to be the first to leave a comment.