Functional programming, where have you been all my life?
This week I was introduced to functional programming, and functional programming specifically using JavaScript. Before this week, I had zero prior knowledge of functional programming, other than the vague impression that it was more abstract than what I'd been doing. But now? Functional programming, where have you been all my life?
All this time, I now realize, I've been coding with an imperative approach, whereas functional programming falls in the realm of declarative (also procedural) programming. It's like realizing the earth is round and not flat. There's another continent on the other side of the world? And the people living there aren't upside down? What?
Since I had such an eye-opening experience learning about functional programming, I want to share a bit of that process with you. A change in paradigm like this one takes a while to sink in (I'm still working on it too!), but every bit of exposure and practice gets us closer.
Getting functional with JavaScript
JavaScript, traditionally an object-oriented language, has been having somewhat of a Renaissance in recent years, with a number of advances in libraries and engines that have brought out heretofore underappreciated potential. Fullstack Academy teaches full-stack JavaScript, and as such, that's what we've been using from the beginning of our lessons, from simple for-loops to this week's foray into functional programming.
To demonstrate what it's like to learn how to program in a functional style, I'll walk us through a well known JavaScript Array method: map(). We'll write our own function that does the same thing, gradually shifting from an imperative to a declarative style. Over the course of a few iterations, I'll show how my understanding of the functional approach evolved. My hope is that other readers might deepen their own understanding as they follow me down the rabbit hole.
Just to make sure we're all on the same page, here's a quick explanation of the built-in Array method map(). If you're already familiar with it, you can skip ahead to the next section.
You call map() on an array. It takes a function as an argument. It applies that function to each item in your original array, and returns a new array with the corresponding results. Let's look at a super simple example of how this works.
var myArray = [1, 2, 3]; // here's our starting array // and here's a simple function function addOne (input) { return input + 1; } var newArray = myArray.map(addOne); console.log(newArray) // [2, 3, 4]
See how smart map() is? You don't even have to pass any arguments to addOne(), because map() takes care of passing each element of myArray to it in order. The function being passed as an argument can be whatever you'd like it to be. Let's look at one more example and then we can move on.
var myDinos = ['Bronto', 'Stego', 'Tyranno']; function addSaurus (input) { return input + "saurus"; } var dinosaurs = myDinos.map(addSaurus); console.log(dinosaurs) // ['Brontosaurus', 'Stegosaurus', 'Tyrannosaurus']
Rewriting map() as our own function
Now that we're clear on how map() works, how would we rewrite it from scratch? Well, the approach we're probably most familiar with would be to write a function that loops over each value in the old array and pushes the results to a new array to be returned.
function ourMap(array, func) { var newArray = []; for (var i=0;i<array.length;i++){ var input = array[i]; var output = func(input); newArray.push(output); } return newArray; }
Looks pretty good. We would use this function differently than the built-in method (it needs to be passed an array and a function), but the result is the same.
var dinos = ourMap(myDinos,addSaurus); console.log(dinos) // ['Brontosaurus', 'Stegosaurus', 'Tyrannosaurus']
So what does it mean to do functional programming? I don't have a simple answer for you, but I will say that the guiding principles are immutability and statelessness.
The first means that functions don't modify data structures, rather they create new ones and return them.
The second principle means that functions aren't affected by things that happen in the world outside of them. If you give a function a certain input, the output should always be the same (e.g., there's no stray global variable coming into play).
These principles result in functions that are reduced to their core essence, like the atomic unit of a function. Each does only one simple thing, but they can be chained together to perform more complex actions. This chaining potential can be very powerful.
Another outcome of these principles is that we'll have to do without those useful looping techniques we've grown so accustomed to. Loops and other control structures are in the domain of the imperative style. They tend to be stateful and often accompanied by mutation, neither of which is desirable. That's certainly not the whole story, but that should be enough to get us started on the walkthrough.
The alternative to looping, we'll see below, is recursion.
How do we rewrite this without loops?
My first attempt at rewriting map() recursively looked something like this:
function ourMap(arr, func){ var newArray = []; function mapHelper(arr,func){ if (arr.length==0){ return newArray; } else { var currentItem = arr.shift(); var resultingItem = func(currentItem); newArray.push(resultingItem); return mapHelper(arr,func); } } return mapHelper(arr,func); }
It worked, and it was recursive, but it didn't meet the standards we set above. For starters, we rely on this mapHelper function to modify a variable that is outside of its scope (not stateless), and our repeated calls to shift() have also modified the input array (mutation, check). And we certainly didn't do much to make the function simpler!
How do we make this better? I started by losing the newArray variable, replacing it with a workingArray parameter in mapHelper(), so that it never had to modify a variable outside its immediate scope. The result was like so:
function mapHelper(arr,func, workingArray){ if (arr.length==0){ return workingArray; } else { var currentItem = arr.shift(); workingArray.push(func(currentItem)); return mapHelper(arr,func,workingArray); } } function map(arr, func){ return mapHelper(arr,func,[]); }
Nice! See how mapHelper() doesn't even need to live inside of ourMap() anymore? As our functions become simpler and more self-contained, we're starting to see some of the beauty of the functional approach.
We still have that pesky shift() modifying our input array though, so let's kill that. We'll use arr[0] to get the value of the first element instead, and while we're at it, we'll lose this currentItem variable. Then, we'll simulate the side-effect of shift() with slice(). Calling .slice(index) on our array will return a new array with all the same elements from the specified index to the end, without modifying the original array.
function mapHelper(arr,func, workingArray){ if (arr.length==0){ return workingArray; } else { workingArray.push(func(arr[0])); return mapHelper(arr.slice(1),func,workingArray); } } function ourMap(arr, func){ return mapHelper(arr,func,[]); }
Looking great! Just one little quibble though: each time we call push() on workingArray, we're modifying it. Just like with using shift() earlier, push() is undesirable because it's mutating an existing data structure, and doing so as a side-effect. For our functions to be clean and "atomic", we should make sure they do one thing, and one thing only: return a freshly made output. We can use the concat() method instead, which will return a new array instead of modifying the array it was called on.
function mapHelper(arr, func, workingArray){ if (arr.length==0){ return workingArray; } return mapHelper(arr.slice(1),func, workingArray.concat([func(arr[0])])); } function ourMap(arr,func) { return mapHelper(arr,func,[]); }
If you're anything like I was prior to this exercise, the thought of replacing simple, straightforward loops with mind-bending recursion could send you running. Give it a chance, though. You might feel differently after a while!
Now that I'm on the other side of it, I have a newfound appreciation for recursion and I feel like I've been liberated from my narrow, loops-first mode of thinking.
This isn't all to say that functional programming is better or worse than imperative styles, or that recursion is better than loops. In fact, the most appropriate approach will probably depend on what language you are using and what your goals are. In JavaScript, loops are often the better way to go, and in fact, built-in "functional-like" methods such as map() are implemented with loops.
Among the important lessons that I took away from this exercise was an awareness of side-effects, statefulness, and mutations. The "rules" of functional programming also helped me learn this process of examining your functions and whittling them down until they are their simplest most naĂŻve selves. Last but not least, I finally see recursion as a useful tool rather than a trivia question. These are concepts and skills that serve us well even in an object-oriented paradigm.
Donât Be Scared Of Functional Programming: Smashing Magazine
Recursion - Where are my for/while loops?: MSDN Blogs
Functional Programming vs. Imperative Programming: msdn.microsoft.com
Functional programming for loop side effect: Programmers Stack Exchange