Skip to content

goodguydaniel.com

Functional bits

Towards a Functional Programming Style

Functional Programming, JavaScript5 min read

Here are some of my top favorite personal utilities to make your programming style more functional. They increase the readability of my code and help me transforming those less pretty pieces of logic into something that I'm proud of and that I can confidently change by merely modifying a line of code (or maybe less).

The bits

I list below a series of utilities together with a short background/motivation followed by an example where I demonstrate both the imperative and functional approaches, where the functional approach makes use of the respective utility. You can also find a rough implementation of the utility by the end of its section.

tap

Inspired by the RxJS tap operator. With tap, you can perform non-intrusive side effects. By non-intrusive, I mean that you can leverage the power of a functional style of coding to perform a task, and simultaneously deliver a side effect (e.g., console.log) within your approach. Let's have a look.

Imperative
1let agesSum = 0;
2const totalNumberOfUsers = users.length;
3
4for (const user of users) {
5 const birthdate = user.birthdate * 1000; // convert to milliseconds
6 console.log(birthdate);
7 const userAge = new Date().getFullYear() - new Date(birthdate).getFullYear();
8 agesSum += userAge;
9}
10
11const meanAge = agesSum / totalNumberOfUsers;
12
13console.log(`Users are in average ${meanAge} years old.`);

Functional with tap
1const sum = (arr) => arr.reduce((s, n) => s + n, 0);
2const mean = (arr) => sum(arr) / arr.length;
3const meanAge = mean(
4 users
5 .map((user) => user.birthdate * 1000) // convert to milliseconds
6 .tap((birthdates) => console.log(birthdates)) // logs array of birthdates
7 .map((birthdate) => new Date().getFullYear() - new Date(birthdate).getFullYear()),
8);
9
10console.log(`Users are in average ${meanAge} years old.`);

I give you that right now, it's far more understandable how you would log something in between your imperative approach. You can inspect each age individually as the code progresses. But as you can see in the functional approach analyzing the birthdates at a particular stage of your data transformations is also possible! Below the implementation of tap. Warning, this approach extends the Array.prototype, use at your own risk.

Implementing tap
1Object.defineProperty(Array.prototype, "tap", {
2 value: function (fn) {
3 fn(this);
4 return this;
5 },
6 writable: true,
7});

and & or

Did it ever happen to you ending up with an if statement that needs to be broken down into several LOC (Lines Of Code) because it is too long, and the linter starts crying about it? There's an elegant solution for that, and it's pure composition, let me share it with you.

Imperative
1const usersEligibleForSurvey = [];
2
3for (const user of users) {
4 const age = getAgeFromUnixTimestamp(user.birthdate);
5
6 if (user.gender === "female" && age < 33 && user.location.country === "denmark") {
7 usersEligibleForSurvey.push(user);
8 }
9}

Functional with and
1const isFemale = (user) => user.gender === "female";
2const isBelowAge = (age) => (user) => getAgeFromUnixTimestamp(user.birthdate) < age;
3const isFromCountry = (country) => (user) => user.location.country === country;
4const isUserEligibleForSurvey = and(isFemale, isBelowAge(33), isFromCountry("denmark"));
5
6const usersEligibleForSurvey = users.filter(isUserEligibleForSurvey);

Instead of a single if statement, you now have a reusable function. More than that, you can easily plugin and out any criteria to exclude users from the survey! Let's say you had a very complex function that, given a specific user would check some rules against the postcode to exclude certain areas of the country. Given you have that function, append it into the and arguments. That's all! isUserEligibleForSurvey is now checking for the postcode as well, you're good to go. and is an excellent example of why we describe this kind of approach as declarative programming, you're expressing the logic without describing its control flow.


Implementing and
1function and(...fns) {
2 const n = fns.length;
3
4 return (...args) => {
5 for (let i = 0; i < n; i++) {
6 const fn = fns[i];
7 const result = fn(...args);
8 if (!result) {
9 return false;
10 }
11 }
12
13 return true;
14 };
15}

More functional, less efficient, since it executes all the predicates.

1const and = (...fns) => (...args) => fns.reduce((prev, fn) => prev && fn(...args));

select & drop

RxJS has pluck, lodash has pick. I find select a more concise and name. Projecting properties from objects is a prevalent task. The fact that JavaScript has destructuring built-in is a live proof of that. You could use destructuring to project properties; it's often more tedious, and it's not suitable for every occasion to use within a chain of operations.

Imperative
1let countries = new Set();
2
3for (const user of users) {
4 countries.add(user.location.country);
5}
6
7console.log(Array.from(countries).join(", "));

Functional with select
1const countries = new Set(users.map(select("location.country")));
2
3console.log(Array.from(countries).join(", "));

In the functional approach, select extracts from each user the country field located within the location object.

Again, with the functional approach, we shift towards a more declarative style. There's also an "opposite" of select, which is drop. In short, instead of picking up the properties of an object, you declare which properties you want to drop.

Implementing select
1// something similar to lodash/get
2function get(o, query, defaultValue = undefined) {
3 if (!query) return defaultValue;
4 const path = query.split(".");
5 let pointer = o;
6
7 for (const k of path) {
8 pointer = pointer[k];
9 if (!pointer) return defaultValue;
10 }
11
12 return pointer;
13}
14
15/**
16 * When there's only a single property in `keys` the value is not wrapped in an object e.g.
17 * > const city = select('location.city')(users[0])
18 * > console.log(city)
19 * > 'staphorst'
20 *
21 * Nested paths are flatten at the top level e.g.
22 * > const cityCountry = select('location.city', 'location.country')(users[0])
23 * > console.log(cityCountry)
24 * > { 'location.city': 'staphorst', 'location.country': 'netherlands' }
25 */
26const select = (...keys) => (o) =>
27 keys.length === 1
28 ? get(o, keys[0])
29 : keys.reduce((acc, k) => {
30 acc[k] = get(o, k);
31 return acc;
32 }, {});

pipe

pipe would be something like lodash/flow where you can take N functions where each performs a unique task and combine them in chain where data flows from left to right. The output of a function within the pipe is the input to the next one (and so on). It's good to use something like pipe when you need to perform a series of data transformations on a given input. Let's look at the following example, where we want to format our users data in a way that is friendly to be consumed by the UI, but first, there are some requirements that need to be met in terms of the shape of each user Object individually. The goal is to render a table with the name (first name + last name), age, and country (with the first character capitalized) so that the Marketing department of the company X can have a look at their users' data nicely formatted.

First let me introduce some shared utilities to do some work on our user Object, we use them in both the imperative and functional approach for ease of comparison.

1// returns number representable of the user age
2function getUserAge(user) {
3 return new Date().getFullYear() - new Date(user.birthdate * 1000).getFullYear();
4}
5// returns the name of the user's country (capitalized)
6function formatCountry(user) {
7 let tmp = Array.from(user.location.country);
8 tmp[0] = tmp[0].toUpperCase();
9 return tmp.join("");
10}
11// puts together first & last name in the same string
12function getFirstAndLastName(user) {
13 return `${user.first_name} ${user.last_name}`;
14}

Now, let's dive in and translate those requirements into code.

Imperative
1const formattedUsers = [];
2
3for (const user of users) {
4 const formattedUser = {
5 name: getFirstAndLastName(user),
6 age: getUserAge(user),
7 country: formatCountry(user),
8 };
9
10 formattedUsers.push(formattedUser);
11}
12
13console.log(formattedUsers); // data ready for the UI!

Functional with pipe
1const formatUser = pipe(
2 (user) => ({ ...user, name: getFirstAndLastName(user) }),
3 (user) => ({ ...user, age: getUserAge(user) }),
4 (user) => ({ ...user, country: formatCountry(user) }),
5 select("name", "age", "country"),
6);
7const formattedUsers = users.map(formatUser);
8
9console.log(formattedUsers); // data ready for the UI!
The only small *trick* here is that I had to feed the initial user down through the pipe, and we incrementally append new data properties to a newly created user Object (original user is not mutated).

As you can see, using pipe, you have a clear separation of concerns in terms of what transformations run against your input, again at any point in time, you can plug in or out a new transformation function from the pipe with minimal effort.

Implementing pipe
1const pipe = (...fns) => (...args) => fns.reduce((prev, fn) => fn(prev), ...args);

Takeaways

Web applications are complex, meaning your code becomes inherently more complicated. Functional constructs do the trick for me when it comes to rearranging my logic into a compact implementation that may read like plain English. But besides a potential big win on readability there are other advantageous things in the package:

  • Functions are more natural to reason - "divide and conquer" one of the most underrated statements that put you on the road to clean code. Your functions will execute one task and one task only, and do it well. You name the smaller functions of your program with intent.
  • Code resilience - you'll notice that your code becomes more bulletproof. Splitting your code into smaller functions and compose them at a higher level of your implementation will make your system more robust, more comfortable to test, achieving the same with less LOC.
  • Composability - your code becomes more composable, allowing you to plug&play functions to promptly tweak your implementation. You'll pull existing functions together to compose them into more intricate ones that will get the job done and still read comfortably.
  • Performance? "Should I stay or Should I Go"? - From the example above, when implementing the and utility, we saw that the functional approach is not able to return earlier as the imperative approach did. There's no way to early break from functional constructs such as .map or .reduce (which is a good thing! no, side-effects allowed!). Don't trick yourself in thinking that such detail dictates overall better performance for an imperative approach. Sometimes the benefits of making it readable and composable through more trivial functions will bring you, your team, and your product far more significant advantages than speeding up the JavaScript execution by a few fractions of a millisecond.

On the "not so good side" of things, the biggest challenge this coding style faces today is debugability. "Oh, but they say also debugging becomes easier!" I don't think so, but let me clarify what I mean by "debugability". One on hand code is more natural to track because there's much structure to it, yes. But on the other hand, diving into specific detail of the implementation becomes hard because you kind of need to perform "reverse engineering" of the compact code to perform a log in the console or other adding a breakpoint (use tap, he's your friend there!).

I think these tools are good ones to spark your interest in a more functional coding style and maybe if you see fit dive into libraries like lodash/fp.

Another tip that I would like to drop is that when adopting a functional architecture it's good to keep in mind some good practices when it comes to design functions. I found Clean Code (by Uncle Bob Martin) to be an awesome resource that helped me laying down some ground rules when it comes to designing function APIs that are clean and scalable.

Do you find these few bits of the functional world beneficial? Give them a try!

If you fell like going through the above examples by executing them to get a better understanding of how they're working, you can use the below dataset as input.

Dataset used for code examples
1const users = [
2 {
3 email: "melany.wijngaard@example.com",
4 gender: "female",
5 phone_number: "(727)-033-9347",
6 birthdate: 608022796,
7 location: {
8 street: "2431 predikherenkerkhof",
9 city: "staphorst",
10 state: "gelderland",
11 postcode: 64265,
12 },
13 username: "bigpeacock217",
14 password: "eagle",
15 first_name: "melany",
16 last_name: "wijngaard",
17 title: "miss",
18 },
19 {
20 email: "nanna.pedersen@example.com",
21 gender: "female",
22 phone_number: "43672992",
23 birthdate: 591428535,
24 location: {
25 street: "2177 fåborgvej",
26 city: "aarhus",
27 state: "syddanmark",
28 postcode: 87547,
29 },
30 username: "purpleduck599",
31 password: "davids",
32 first_name: "nanna",
33 last_name: "pedersen",
34 title: "ms",
35 },
36 {
37 email: "amelia.mercier@example.com",
38 gender: "female",
39 phone_number: "(168)-747-5950",
40 birthdate: 1132298571,
41 location: {
42 street: "7454 rue duquesne",
43 city: "echandens-denges",
44 state: "vaud",
45 postcode: 3811,
46 },
47 username: "whitefrog218",
48 password: "forest",
49 first_name: "amelia",
50 last_name: "mercier",
51 title: "madame",
52 },
53];

If you liked this article, consider sharing (tweeting) it to your followers.