Skip to content

Instantly share code, notes, and snippets.

@peterjwest
Last active June 17, 2020 13:09
Show Gist options
  • Save peterjwest/39c472e24ce126584e55c9eee9bb3a04 to your computer and use it in GitHub Desktop.
Save peterjwest/39c472e24ce126584e55c9eee9bb3a04 to your computer and use it in GitHub Desktop.
Functional function examples

The goal in this example is to count the number of books each author has written, from a list of book objects.

Example input:

const books = [
    { author: "Jane", name: "Foo: a bar" },
    { author: "John", name: "Tale of Zim" },
    { author: "Jan", name: "Legend of Gir"  },
    { author: "Jane", name: "Zig 2: the sequel" },
];

Expected output:

const authorCounts = {
  "Jane": 2,
  "John": 1,
  "Jan": 1,
};

Starting with a non-functional solution:

function countAuthors(books) {
    const authorCounts = {};
    for (const book of books) {
        if (!authorCounts[book.author]) {
            authorCounts[book.author] = 0;
        }
        authorCounts[book.author]++;
    }
    return authorCounts;
}

This uses a loop to build up a new object, pretty standard, but this is pretty much a unit, we couldn't easily decompose this.

For a functional example, I'm using lodash because javascript doesn't have a great amount of functional utilities:

import { mapValues, groupBy } from 'lodash';

function countAuthors(books) {
    return mapValues(groupBy(books, 'author'), (books) => books.length);
}

Here we just do 2 transformations, first grouping the list of books into a dictionary keyed by author name, and then taking the length of the inner array of books.

This is now a simple pipeline, and much shorter than the original.

The goal in this example is to take a set of absolute file paths, and create a nested set of objects to represent all the folders and files.

Example input:

const input = [
    '/foo/bar/zim/gir.txt',
    '/foo/bar/zim/zig.pdf',
    '/bar/zim/bop.js',
];

Expected output:

const output = {
    foo: { bar: { zim: { 'gir.txt': true, 'zig.pdf': true } } },
    bar: { zim: { 'bop.js': true } },
};

So folders become nested objects, and files become true (just to keep this example relatively simple).

First I'll give a not-very-functional example:

function fileTree(files) {
    const tree = {}
    for (const file of files) {
        let position = tree;
        const paths = file.slice(1).split("/");
        for (let i = 0; i < paths.length; i++) {
            const path = paths[i];
            if (!position[path]) {
                if (i < paths.length - 1) {
                    position[path] = {}
                } else {
                    position[path] = true;
                }
            }
            position = position[path];
        }
    }
    return tree;
}

This uses a couple of loops to build up a tree object, using a position variable to remember where we are in the tree structure.

Splitting this function up would be possible, but probably wouldn't make it any clearer, because an inner function would have to mutate values for the outer.

Moving on to a more functional solution:

function fileTree(files) {
    const filePaths = files.map((file) => file.slice(1).split("/"));
    return filePaths.reduce(addPath, {});
}

function addPath(tree, path) {
    if (path.length === 0) {
        return tree;
    }
    const branch = tree[path[0]] || (path.length === 1 ? true : {});
    return { ...tree, [path[0]]: addPaths(branch, path.slice(1)) };
}

This doesn't modify the original input, as well as none of the functions having any side effects or in fact any mutations.

It uses map and reduce which are both very standard functional utilities, and immediately break up the algorithm into two separable phases.

The advantage of no mutations is it makes the testing very simple, addPath takes a tree and the path to a file, and returns a new tree with that file added into it.

addPath has turned out to be a function that makes sense outside of the context of fileTree, so it's more likely to be reusable down the line.

The whole thing is also much more concise, taking only half the number of lines.


A fun (but silly) thing we can also do is also reduce both functions to one line, as they're effectively just single expressions:

function fileTree(files) {
    return files.map((file) => file.slice(1).split("/")).reduce(addPaths, {});
}

function addPaths(tree, paths) {
    return paths.length === 0 ? tree : { ...tree, [paths[0]]: addPaths(tree[paths[0]] || (paths.length === 1 ? true : {}), paths.slice(1)) };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment