An Introduction to Abstraction and Functional Composition in Your Javascript

It’s important to be self-aware, both as a developer and as a human being. It helps you to better understand yourself so that you can effectively change and grow. And in an industry that doesn’t stay still for long, it's vital to continue growing alongside the restless beast as it perpetually evolves.

I’ve noticed in my own code a shift in style that incorporates a more functional style of programming that is leaps and bounds more maintainable and readable than my early imperative style of coding. While the imperative style that is characteristic of my early Javascript days did help me to understand the language, it really served as a sort of training wheels that I would ultimately ditch once as I was able to code with confidence and self-awareness.

As I think and look back to those earlier days, I’ve also noticed a lack of abstraction in my imperative code. It was just something that I wasn’t familiar with at the time when I wrote my code as a sequence of instructions.

But abstraction is an incredibly important technique in computer science, and it can even be argued that we wouldn’t be at this point in technological progress, nor would we be able to advance further, without abstraction.

So what follows are demonstrations of how abstraction can be applied to your own Javascript, as well as how functional composition can be used, which I’ve come to understand as the product of combining abstractions to build more complicated abstractions.

Abstraction

I understand abstraction in computer science as taking complexity and generalizing it, thereby simplifying it. Abstraction outside of computer science is knowing what beer is without having to know what beer really is: which is a beverage brewed from malted barley, some hops, and some yeast, among other ingredients for individuality and character. Beer serves as an abstraction for this beverage, so we can better interact with and understand beer without having to really interact with and understand all the complexity that is beer.

It is through combining abstractions that we can understand composition. Abstractions can be composed into further abstractions, so that we can understand what a six pack is without also really having to concentrate on what a six pack really is: six glass bottles that each hold 12 ounces of a beverage brewed from malted barley, hops, yeast, and other ingredients, which are held in the six pockets of a cardboard holder. And this is a pretty shallow analysis of the abstraction levels of a six pack. It can be taken deeper into the cardboard holder, the glass bottles, and etcetera. But what I’d like to reiterate is that composition is combining abstractions and layering them to create further abstractions, which make life, and coding, more manageable.

In programming, functions, in essence, are abstractions. In Javascript, you declare a function with a name and define it with instructions to perform. After defining the function, you really don't ever engage with the instructions and procedures. Unless you maybe need to revise it. But the instructions defined within the function, that's the complexity underneath the surface; that's the complexity that gets generalized and abstracted into the name of the function.

You declare and define a function:

function addNumbers(a, b) {  
    if (typeof a !== "number" || typeof b !== "number") return;
    return a + b;
} 

And you never have to really worry about the complexity underneath the surface after that. So long as the abstraction is appropriately name, as misleading function names could cause you to dig around to figure out what it does.

Although this is a very simple function, the point still remains: the complexity, albeit it a little, of adding two arguments, if and only if they are both numbers, is abstracted away into the name of the function; so, all you really have to do is engage with the abstraction, the function, by feeding it only numbers.

let sum = addNumbers(1, 2)  
console.log(sum) // this will log the number 3  

That's abstraction at work. And combining these abstractions through functional composition ultimately can help in creating more maintainable and readable code. But before digging into functional composition, let's take a look at a bit more of an imperative style of programming that alsodoesn't take advantage of the benefits of abstraction.

Imperative Abstraction-less Code

This sort of style of programming that reads from top to bottom as a list of instructions and doesn't take advantage of the benefits of abstraction is characteristic of the code I wrote when I first started programming. That's because as a budding programmer, I was still trying to figure out and understand all of the fundamentals. So, it could be overwhelming to be conscious of the code I was writing, and how best to optimize it. Not to mention, I was ill-equipped to do so, as I simply lacked the experience.

If I needed to figure out how many beers I had in my fridge at any given time, I would have written a simple program like this:

let sixPacksInFridge = [  
   {
        sixPack: "Banquet Burr",
        brewedBy: "Coors",
        beers: [false, false, false, true, true, true]
    },
    {
        sixPack: "Fat Tire",
        brewedBy: "New Belgium Brewing",
        beers: [false, false, false, false, false, false]
    },
    {
        sixPack: "Anchor Steam",
        brewedBy: "Anchor Brewing Company",
        beers: [false, false, true, true, true, true]
    },
    {
        sixPack: "Sculpin",
        brewedBy: "Ballast Point Brewing Company",
        beers: [false, false, false, false, false, true]
    },
    {
        sixPack: "A six pack of good ol' Buds",
        brewedBy: "Anheuser-Busch",
        beers: [false, false, false, false, false, false]
    }
];

let sixPacksWithBeers = [];  
let totalBeersLeft = 0;

for (let i = 0; i < sixPacksInFridge.length; i++) {  
    for (let x = 0; x < sixPacksInFridge[i].beers.length; x++) {
        if (sixPacksInFridge[i].beers[x]) {
            sixPacksWithBeers.push(sixPacksInFridge[i]);
            break;
        }
    }
}

for (let i = 0; i < sixPacksWithBeers.length; i++) {  
    for (let x = 0; x < sixPacksWithBeers[i].beers.length; x++) {
        if (sixPacksWithBeers[i].beers[x]) {
            totalBeersLeft++
        }
    }
}

console.log("Six Packs with Unopened Beers: ", sixPacksWithBeers);  
console.log("Total Unopened Beers Left: ", totalBeersLeft);  

In the first procedure, a nested for loop goes through the six packs in my fridge, and pushes them into an array if there is at least one unopened beer.

In the following procedure, another nested for loop goes through the array containing six packs with at least one unopened beer and counts each individual beer in the six pack.

It gets the job done. I know how many unopened beers I have after. But, hey, it isn't that great. And it is kind of a pain to figure out what's going on just by reading the code.

One of my main concerns with this program is that the nested for loops can be difficult to read. The variable names aren’t confusing or misleading, so they do provide some context as to what is happening, but all the code used to construct the nested for loops really muddles the context. Some might also argue that the longer, more descriptive variable name sort of lose their clarity in the midst of the for loop syntax.

The other main concern that I have is that this abstraction-less code suffers from no reusability. In its current state, this code exists solely for this small program that counts the beers in my fridge. Being devoid of any useful abstractions, there isn't anything to export and import into another program that, say, might count thirty packs, forties, tall cans, and mini-kegs in my fridge, or someone else's fridge. And so, this abstraction-less code is quite limiting.

Lastly, with no reusuability, what will likely follow is an unnecessary repetition of code. It isn't quite obvious in this program, as much isn't happening, but when you write code that isn't reusuable, you'll often find the same pieces of code scattered around and repeated when packaging and abstracting it would help minimize that.

So, while this program will get the job done, the abstraction-less style it is written in ultimately detracts heavily from the quality of the code.

What we can do, though, is refractor it in a more functional style, keeping in mind the technique of abstraction.

Abstracting into Functions

With the help of some native Javascript methods, a function can be created to filter out the six packs in my fridge that contain at least one unopened beer to the same effect as the first procedure in my earlier example.

function filterIrrelevantSixPacks (allTheSixPacks) {  
    if (!Array.isArray(allTheSixPacks)) return;
    return allTheSixPacks.filter(sixPack => sixPack.beers.includes(true));
 }

The filterIrrelevantSixPacks function expects an array as an argument. If one is provided, the filter method is called off of it. Filter, by the way, is an array method that returns a new array with any elements from the original array that pass the test implemented by the callback. In this case, if a six pack contains an unopened beer, the six pack will pass the test and it will be filtered into the new array.

This function is pretty similar to what is happening in the first procedure from the earlier example. In both cases, there is an outer loop over the six pack array, and there is also an inner loop on the beers within each six pack. In both cases, the object is filtered into a new array if a beer is found to be unopened in the six pack. The obvious difference, though, is that the filterIrrelevantSixPacks function is much, much more readable because the complex processes that is happening behind the scenes are abstracted into the method names.

Here's the second procedure abstracted into a function:

function countRemainingBeers (filteredSixPacks) {  
    if (!Array.isArray(filteredSixPacks)) return;
    return filteredSixPacks.reduce((previousPack, sixPack) => {
        return sixPack.beers.filter(beer => beer).length + previousPack;
    }, 0)
}

The countRemainingBeers function also expects an array as an argument. If one is provided, the reduce method is then called off of it. The reduce method reduces an array of values into a single value, and this implementation will ultimately add up the number of unopened beers in each six pack, returning that total.

This function also behaves like the second procedure in the earlier example. There is an outer for loop from the reduce method and an inner for loop from the filter method. But like the first function, this one is much more readable and it is also reusable because of the use of abstraction. And reusability and readability produces better quality of code cleanliness. Which translates to code that is easier to understand and maneuver in.

But these abstractions can be taken further. They can be used as building blocks to create a more complex abstraction through the technique of functional composition.

Functional Composition

Functional composition is the technique of building a function with functions. This serves the purpose of further abstracting away from the complexity below, which if done well, can add even more cleanliness to your code.

Functional composition was actually used to create the filterIrrelevantSixPacks and countRemainingBeers functions through the use of the native filter, includes, and reduce methods. But I also constructed those functions so that they can be used to create a single function that packages both procedures together.

function countBeersInFridge(sixPacksInFridge) {  
    if (!Array.isArray(sixPacksInFridge)) return;
    return countRemainingBeers(filterIrrelevantSixPacks(sixPacksInFridge));
}

The countBeersInFridge function accepts an array of six packs as an argument. All of the six packs are then fed to the filterIrrelevantSixPacks function. The output it returns is fed to the countRemainingBeers function. And lastly, the output of countRemainingBeers, which is the total number of unopened beers, is finally returned by the enclosing countBeersInFridge function.

So what's there to gain from abstraction and functional composition?

Well, besides readability and reusability, there's simplicity to gain from only having to interact with the topmost abstraction. This post began at the bottom with two nested for loops, one for each procedure, and the convoluted syntax involved with them. That messiness was then hidden by abstracting those procedures into two functions with the help of some abstractions in the form of native Javascript methods. Finally, the abstraction is stacked even higher through additional functional composition, resulting in a single countBeersInFridge function. At this topmost level, only countBeersInFridge needs to be interacted with. I feed it all of the six packs in my fridge, and that value passes down through each abstraction layer. From the surface, it doesn't look like it does much. But it does. Abstractions run deep in programming.

comments powered by Disqus