loading...

rethink nested loops in Javascript functional

5422m4n profile image Sven Assmann ・2 min read

I want to start right away with the little problem statement:

const animals = ['ant', 'bison', 'camel', 'duck', 'elephant'];

// c-ish for-i loop
for (let i = 0; i < animals.length; i++) {
    for (let j = i + 1; j < animals.length; j++) {
        const a1 = animals[i];
        const a2 = animals[j];

        console.log(`${a1} and ${a2} are friends`);
    }
}
/* expected output:

ant and bison are friends
ant and camel are friends
ant and duck are friends
ant and elephant are friends
bison and camel are friends
bison and duck are friends
bison and elephant are friends
camel and duck are friends
camel and elephant are friends
duck and elephant are friends

 */

that works and probably there is nothing wrong with it.

But how to do the same thing functional?

Let's give it some tries:

animals.forEach((a1) => {
    animals.forEach((a2) => {
        console.log(`${a1} and ${a2} are friends`);
        // WRONG!
        // > ant and ant are friends
    });
});

Hm, as you can see there is something not as expected as should be.
Now all animals are combined with each other, even ones with themself.

Alright next try to fix that:

animals.forEach((a1, xi) => {
    animals.slice(xi + 1).forEach(a2 => {
        console.log(`${a1} and ${a2} are friends`);
    });
});

Yeah! It works. Let's have a look why is that.

The slice function accepts an argument that is the starting index, from where on an array should be sliced. Here we handover the index + 1 of a1 so that we getting a sub array behind a1.

Alright, as a bonus let's go one more step, to make our code functional reusable.

const combine = (list) => list.map(
    (x, xi) => list.slice(xi + 1).map((y) => [x, y])).reduce(
        (acc, tuple) => acc.concat(tuple), []);

console.log(combine(animals));
/* expected output:

[ [ 'ant', 'bison' ],
  [ 'ant', 'camel' ],
  [ 'ant', 'duck' ],
  [ 'ant', 'elephant' ],
  [ 'bison', 'camel' ],
  [ 'bison', 'duck' ],
  [ 'bison', 'elephant' ],
  [ 'camel', 'duck' ],
  [ 'camel', 'elephant' ],
  [ 'duck', 'elephant' ] ]

 */

now we got a lambda called combine that will yield an array of tuples that we can use as following:

var allTheAnimals = combine(animals).map(
    ([a1, a2]) => `|${a1}| and |${a2}|`).join(' are friends\n');
console.log(`${allTheAnimals} are friends`);
/* expected output:

|ant| and |bison| are friends
|ant| and |camel| are friends
|ant| and |duck| are friends
|ant| and |elephant| are friends
|bison| and |camel| are friends
|bison| and |duck| are friends
|bison| and |elephant| are friends
|camel| and |duck| are friends
|camel| and |elephant| are friends
|duck| and |elephant| are friends

 */

Note that .map(([a1, a2]) will spread the tuple array into the one left and right.

Now you share your approach below in the comments! I'm curious about other solutions.

Thanks for reading!
Cheers Sven

Posted on by:

5422m4n profile

Sven Assmann

@5422m4n

Polyglot software engineer, since 2006 mainly on web technologies. Like to explore new things and toy around with languages, frameworks and problems. 🦀 he/him

Discussion

markdown guide
 

Hey there! Awesome article!

I love separating the concerns. Making a lot of little functions that can either be reused or composed of other little functions. That way, I can easily work on my feature, and even add new features along the way. Here, I took your example, and added two forms of friendships: friends & bffs.

Source-code

"use strict";

/**
 * @param {number} index The index to filter out
 * @return {Function} A function that will be applied to the filter call
 */
function byIndex(index) {
  /**
   * @param {string} current Current iterated item (unused)
   * @param {number} currentIndex Index of the current iterated item
   * @return {boolean} False if the index equals the current item's index (filtered out)
   */
  return function(current, currentIndex) {
    return index !== currentIndex;
  };
}

/**
 * @param {string} message A string containing the message to join the two friends
 * @param {string} friend A string containing the name of the friend to build friendship with
 * @return {Function} A function that will be applied to all friends
 */
function friendshipBuilder(message, friend) {
  /**
   * @param {string} currentFriend The current iterated name of the friend
   * @return {string} The love message describing the new created friendship
   */
  return function(currentFriend) {
    return `${friend} and ${currentFriend} ${message}`;
  };
}

/**
 * @param {string} message Message to link friends together
 * @return {Function} A function that will be applied to all the iterated items
 */
function combineWithMessage(message) {
  /**
   * @param {string} current Name of the current iterated friend
   * @param {number} index Index of the current iterated friend
   * @param {string[]} old An array containing the name of all friends
   * @return {string[]} An array of string containing all the friendships's love messages
   */
  return function(current, index, old) {
    const withoutCurrent = old.filter(byIndex(index));
    const friendships = withoutCurrent.map(friendshipBuilder(message, current));
    const sentences = friendships.join("\n");

    return sentences;
  }
}

const animals = ["ant", "bison", "camel", "duck", "elephant"];

const friendships = animals.map(combineWithMessage("are friends")).join("\n");
const bffs = animals.map(combineWithMessage("are best friends")).join("\n");

console.log(friendships);
/*
ant and bison are friends
ant and camel are friends
ant and duck are friends
ant and elephant are friends
...
*/

console.log(bffs);
/*
elephant and ant are best friends
elephant and bison are best friends
elephant and camel are best friends
elephant and duck are best friends
*/

Available online

Here.

Summary

As you can see, not only we can combine those items altogether, but we can now easily join them with custom messages, making the combination with other strings easier in the future.

Another advantage of using a lot of little functions is that it is easier to test them out. I can write a suite of specifications for my tests, run those specs and see what is working and what is not. I also go back and fix those functions in the future because Maybe in one year, this code won't work anymore because of one change in the JavaScript language about a method. I can easily go back in the code, run the tests, see what test is failing and work my way up to the bug in the source-code easier than if it was inside a big single function. That's what I like the most about functions composition and functional programming.

Thanks for your post, it has given me some inspiration and a pretext to work on my functional game again! Let me know what you think, I would love to know.