Functional Programming - Part 2 - Entering the Way

· 16 min read Đọc bài này bằng tiếng Việt

Style note: This series uses martial-arts/cultivation metaphors to tell the story of programming. This is an intentional choice by the author to make dry concepts more vivid.

So, Functional Programming is the art of programming in which we:

  • use functions to control workflow
  • adhere to the two principles of immutability and purity

In other words, fellow cultivators who wish to practice Functional Programming must keep their dao-mind pure, their will firm, daily contemplating, meditating on, perceiving function — and their cultivation will continuously advance.

But how do we perceive “function intent”? How should we observe, think about, and meditate on functions? Here are the foundational techniques for entering the way.

Higher-order function

Higher-order function is a concept from mathematics. Any function that accepts a function as a parameter, or returns a function as a result, is considered a higher-order function.

Below is an example: the getItem function takes a function by describing a condition, and returns another function. It more than qualifies as a higher-order function.

const getItem = (by) => (arr) => by;

// or the verbose version
const getItem = (by) => {
  return (arr) => {
    return by(arr);
  };
};

Programming in the Functional Programming style is dancing with functions.

In Functional Programming, nearly every function is a higher-order function, because they can all receive and return functions.

But what’s the benefit? It simply provides us with a different way to reason and deduce. For instance, the getItem function above lets you transform it in countless ways depending on how you manipulate by.

When you write getItem, you don’t need to know what condition will be checked later, nor do you care about what input you’ll receive.

You can create a function to find the maximum number in an array of numbers like this:

// create an engine function to get max number from array
const maxNumber = (arr) => {
  return Math.max(...arr);
};

// then pass it to getItem to get the needed function
const getMaxNumber = getItem(maxNumber);

// let's try it
getMaxNumber([4, 6, 2, 3, 1, 8, 7, 5]);
// => 8

Why not just pass the number array directly to maxNumber? Because in this design, we’re treating maxNumber as a plugin. There are many other plugins. We don’t call plugins directly — we call them through a more general interface.

Now suppose we have data for a group of people:

const members = [
  { name: 'Alice', height: 165 },
  { name: 'Bob', height: 152 },
  { name: 'Celina', height: 178 },
  { name: 'Dan', height: 194 },
  { name: 'Eric', height: 187 },
];

Want to find the tallest person? Just add another plugin:

const maxHeight = (people) => {
  return people.reduce((prev, current) => {
    return prev.height > current.height ? prev : current;
  });
};

const getTallestPerson = getItem(maxHeight);

getTallestPerson(members);
// => { name: 'Dan', height: 194 }

The example above is simple, but it can be a good hint for using higher-order functions to design flexible, extensible programs.

Function Composition

This is a mathematical concept. In Functional Programming, everything has mathematical origins.

Function Composition is the combination, the linking of multiple functions together into one larger function with more capabilities.

There are two fundamental techniques in Function Composition: compose and pipe.

Compose

Remember, in the Functional Programming space, there exist countless small, simple pure functions. True to the UNIX philosophy of “do one thing and do it well.”

Since each function does only one thing, when we want to perform multiple actions on the same input, we just combine the needed functions.

Let’s pause our cultivation, temporarily forget our cultivation level, descend into the mundane, and observe human life.

This time, you become the fourth son in a family of villagers living at the foot of Tản Viên Mountain, making a living selling meat…

One day, you score a large log from your uncle who works as both a forest ranger and timber smuggler.

Large log — raw material for the woodworking problem

From this log, you want to make a cutting board for home use.

As a newly initiated Functional Programming cultivator, though lacking cultivation power, you can still envision the pure functions you’ll need:

  • saw(): takes a log, returns round slices
  • dry(): takes fresh wood slices, returns dried slices
  • plane(): takes wood slices, returns flattened slices
  • drill(): takes wood slices, returns slices with 2 holes (for hanging)
  • sand(): takes wood slices, returns smooth slices (using sandpaper)
  • hook(): takes a hookless board, returns a hooked board

Each function does exactly one thing. No more. No less. After going through all these steps, we get the desired product.

Woodworking process: each function does exactly one thing

Of course, since we want the log modified, we’ll temporarily set aside immutability.

Here’s the simulation version:

const saw = (x) => `${x} sawn`;
const dry = (x) => `${x} dried`;
const plane = (x) => `${x} planed`;
const drill = (x) => `${x} drilled`;
const sand = (x) => `${x} sanded`;
const hook = (x) => `${x} hooked`;

To make a cutting board, in ancient times, primitive coders used to write:

var board = saw('log');
board = dry(board);
board = plane(board);
board = drill(board);
board = sand(board);
board = hook(board);

console.log(board);
// => log sawn dried planed drilled sanded hooked

50,000 years later, when mathematics had emerged, the Giao Chỉ tribespeople of the Hồng Bàng era preferred writing:

var board = hook(sand(drill(plane(dry(saw('log'))))));
console.log(board);
// => log sawn dried planed drilled sanded hooked

This is basic mathematics. With y = f(g(x)), we compute g(x) first, then pass the result to f(). The computation goes from the innermost parentheses outward — reading right to left, from g to f.

Another 5,000 years pass. Now we have ES6. Some FP masters created the compose function:

const compose = (...fns) => {
  return fns.reduce((f, g) => (x) => f(g(x)));
};

The idea of compose is to stack functions together, left to right, creating a new function that, when executed, calls the passed-in functions in reverse order — right to left.

That is, if y = compose(f, g), then y(x) = f(g(x)); It computes g(x) first, then passes the result to f; If g(x) = z, then y(x) = f(z);

If you still find this foggy, just treat it as the Dao. It can only be perceived, not explained in words!

Back to the cutting board. The compose function is, of course, a higher-order function. Let’s see it in action:

const gimme_a_board = compose(hook, sand, drill, plane, dry, saw);
console.log(gimme_a_board.toString());
// => guess what it logs?

Now we have a function called gimme_a_board, the result of composing all the pure functions above.

We know compose calls right to left, so the step that happens first goes on the right.

Let’s try it:

const board = gimme_a_board('log');
console.log(board);
// => log sawn dried planed drilled sanded hooked

All steps complete — the log has become a fine cutting board.

But it doesn’t stop there. When you hang this board at home, many friends visiting see its beauty and want to buy one. So many that you decide to go into the cutting board business.

Selling boards requires labels, so you create a new pure function and use compose to make a commercial-grade board mold.

Easy — no impact on the home-use board product line.

const label = (x) => `${x} labeled`;

const make_board_for_sale = compose(label, hook, sand, drill, plane, dry, saw);

Or reuse the old mold:

const make_board_for_sale = compose(label, gimme_a_board);

Let’s try:

const sale_board = make_board_for_sale('log');
console.log(sale_board);
// => log sawn dried planed drilled sanded hooked labeled

To expand market share into the budget segment, you create a mid-range product line — using MediaTek chips — skipping drying and sanding to reduce cost. Very simple:

const make_budget_board = compose(label, hook, drill, plane, saw);

Let’s try:

const budget_board = make_budget_board('log');
console.log(budget_board);
// => log sawn planed drilled hooked labeled

Programming like this is incredibly elegant and courteous! Sometimes I feel the Functional Programming style carries a noble serenity — both down-to-earth and academic, beautiful to the point of being hard to understand!

If we’d used OOP, we might still be tangled in a mess of SawMachine, PlaneMachine, DrillMachine classes… Or one giant BoardMakingMachine class with all the saw, plane, drill methods… Plus a heap of properties to decide which should be public and private. Then creating instances, inheriting back and forth multiple rounds before maybe getting a board. Adding new product lines would be even harder — creating BoardForHome class, extending to BoardForSale, BoardForSaleBudget… unbelievably tedious!

With Functional Programming, we just need a few simple, independent functions, assembled with compose like linking a production line, to manufacture all kinds of boards.

Function Composition is like a modern factory — each component is processed by a specialized robot, scientifically combined to produce the finished product.

Pipe

A variant of compose is pipe, which operates in the opposite direction. We can implement it by swapping f and g:

const pipe = (...fns) => {
  return fns.reduce((f, g) => (x) => g(f(x)));
};

Or keep compose’s code but use reduceRight instead of reduce:

const pipe = (...fns) => {
  return fns.reduceRight((f, g) => (x) => f(g(x)));
};

Since pipe composes functions in the opposite direction from compose, we write:

const make_cheap_board = pipe(saw, plane, label);

Let’s try:

const cheap_board = make_cheap_board('log');
console.log(cheap_board);
// => log sawn planed labeled

Using pipe feels more natural. The order of steps — saw, plane… — looks quite intuitive. If you’re comfortable with mathematical reasoning, you’ll like compose. If you want visual clarity, use pipe.

Compose and pipe are easy-to-learn, easy-to-use foundational techniques, but don’t underestimate their power — every FP library includes them. In Ramda.js, besides compose and pipe, the authors also provide pipeK, pipeP, composeK, composeP.

Once proficient, you can create compose your own way. For example, composeBinary linking functions from the middle outward instead of end-to-end, composeRandom linking functions in non-fixed order… That’s a creative space that belongs to you.

Currying function

The term currying and its variants (curry, curried) in computer science were coined by Christopher Strachey in 1967 to honor Haskell Brooks Curry, an American mathematician and logician.

Currying a function means turning a function into a “curried function.”

The original function is a bit dumb — it needs you to pass all N parameters to compute, and if even one parameter is missing, it won’t run.

For example, this sum function:

const sum = (a, b, c) => {
  return a + b + c;
};

sum needs 3 parameters to add up. If any are missing, it can’t compute.

// good to go
sum(5, 3, 2); // => 10
sum(4, 4, 2); // => 10
sum(4, 3, 3); // => 10
sum(3, 5, 2); // => 10

// but
sum(4, 5); // => NaN

That’s like missing forwards — the whole team refuses to take the field! But life isn’t always smooth with everything in place. Even if all the forwards are injured, suspended, or skipping practice, the remaining players still have a responsibility to show up!

Currying is precisely the technique of turning that dumb sum function into something more marvelous: if you call it with 1 argument, it returns a temporary function holding that argument, waiting until all 3 arguments are present before performing the computation.

Imagine organizing a party, inviting 3 friends. Two have arrived, one is late. You decide not to wait. The party starts now — when the last person arrives, we continue from there.

Here’s one way to implement a curry function:

const curry = (fn) => {
  let totalArguments = fn.length;
  let next = (argumentLength, rest) => {
    if (argumentLength > 0) {
      return (...args) => {
        return next(argumentLength - args.length, [...rest, ...args]);
      };
    }
    return fn(...rest);
  };
  return next(totalArguments, []);
};

And of course, curry is also a higher-order function.

Let’s try it with sum:

const curriedSum = curry(sum);

curriedSum is now the curried version of the original sum.

curriedSum(4, 4, 2); // => 10
curriedSum(4, 3, 3); // => 10
curriedSum(3, 5, 2); // => 10

// and
curriedSum(5, 3); // => [Function]

curriedSum(5, 3) is a function. It’s waiting for the final argument to appear. If we now call it with one argument, the result is computed:

curriedSum(5, 3)(2); // => 10

What if we pass more arguments than needed? With the implementation above, extra arguments are ignored. The curry implementations in Ramda.js and Lodash FP behave the same way.

curriedSum(5, 3)(2, 4, 8); // => 10

Another important point: you can split the original function into 1 to N parts, where N is the number of parameters of the original function. For example, if the original has 3 parameters, you can split it into 1, 2, or 3 parts. These calls are equivalent:

curriedSum(3, 5, 2);
curriedSum(3, 5)(2);
curriedSum(3)(5, 2);
curriedSum(3)(5)(2);

Curry, like compose and pipe, is a fundamental technique everyone must learn and know. Every language designed with Functional Programming in mind — Haskell, Scala, Elm… — provides these functions. They’re elegant and used everywhere.

Master just these three techniques, and you’ll be considered a genuine Functional Programming disciple.

📎 Source: kipalog.com

Comments

  1. Loading comments…