Fundamentals of Functional JavaScript
This article aims to lay down some fundamentals of Functional Programming, while mainly pertaining to their application in JavaScript.
Introduction
Is Functional Programming any style of code that utilizes functions? If only it was that simple! Functions are indeed at the core of Functional Programming, but it's how we use those functions that make our implementation functional.
This article aims to lay down some fundamentals of Functional Programming, while mainly pertaining to their application in JavaScript
, that will help you understand the following :
- What are Functions?
- Functions versus Procedures
- Declarative versus Imperative Programming
- Understanding Function Inputs and Outputs
These foundations will immensely aid you in grasping further concepts of Functional JavaScript, to be covered in future articles, if this one helps.
The next article will cover :
- Function Purity (Pure versus Impure Functions)
- Side Effects
- Extracting and Containing Impurity
- How all of this collectively defines what Functional Programming is, and why it is used
- Is
JavaScript
a Functional Programming Language? - Why consider Functional Programming style for your code?
Stay tuned!
1. What are Functions?
Well, like any introductory programming class will tell you, a function is a reusable piece of code that performs some task upon execution. While this definition is reasonable, it misses out an important perspective that is the core of a function as it applies to Functional Programming.
Let's try to understand Functions more completely, with the example of very basic Mathematics.
You may remember reading about f(x)
in school, or the equation y = f(x)
.
Let's assume the equation f(x) = x2 - 1
. What does that mean? What does it mean to graph that equation? Here's the graph:
It's equivalent to :
function f(x) {
return Math.pow(x,2) - 1;
}
What you can notice is that for any value of x
, say 1
, if you plug it into the equation, you get 0
. What is 0
, though? It's the return value of the f(x)
function, which we earlier said represents a y
value.
In math, a function always takes input(s), and always gives an output. A term you'll often hear around FP is "morphism"; this is a fancy way of describing a set of values that maps to another set of values, like the inputs of a function related to the outputs of that function.
In our code, however, we can define functions with all sorts of input(s) and output(s), even though they'll rarely be interpreted as a visually plotted curve on a graph.
As such, a more complete definition of function would be :
A function is the semantic relationship between input and computed output.
Also note the usage of
function
and function throughout this article. While function is the concept we're discussing,function
is just theJavaScript keyword
.
Essentially, Functional Programming is about embracing using functions
as functions in this mathematical sense.
2. Functions vs Procedures?
The terms Functions and Procedures are often used interchangeably, but they actually mean different things.
A Procedure is an arbitrary collection of functionality. It may have inputs, it may not. It may have an output (as a return
value), it may not.
Whereas, a Function takes input(s) and definitely always has a return
value.
For Functional Programming, we use functions as much as possible, and trying to avoid procedures wherever possible. All your functions should take input(s) and return output(s).
Based upon this knowledge, let's consider the following example :
// Example 1: Function or Procedure?
function addPokémon(team1 = 0, team2 = 0, team3 = 0) {
var total = team1 + team2 + team3;
console.log(total);
}
function countPokémon(currentTeam = 6, ...args) {
return addPokémon(currentTeam, ...args);
}
countPokémon();
// Output : 6
countPokémon(6, 5, 6);
// Output : 17
Try to assess whether function
addPokémon
and countPokémon
are functions or procedures?
Here are some basic observations :
addPokémon
has a defined input, but no output specified byreturn
. It should be a procedure.countPokémon
has a defined input and a definedreturn
, so it should be a function?
We're correct about addPokémon
being a procedure, but countPokémon
is also a procedure, and not a function, since it makes a call to a procedure within itself.
In summary :
Note that a
function
that calls a procedure within itself, is also a procedure. The "impurity" of a procedure, which is a concept to be explained further down, trickles down and "pollutes" all those who directly or indirectly call it.
Now, we would probably like to understand how to convert the last example's procedures into functions?
Based on the more complete definition of a function mentioned in the last section, try to make changes to the last example, before looking ahead for one of many possible solutions. For this example, it should be pretty straight-forward.
// Example 2: Converting Procedures to Functions?
function addPokémon(team1 = 0, team2 = 0, team3 = 0) {
var total = team1 + team2 + team3;
return total;
// Instead of logging a value, we returned it,
// so there's a proper output/return now.
}
function countPokémon(currentTeam = 6, ...args) {
return addPokémon(currentTeam, ...args);
// Now, a call to a function, not a procedure, is returned
}
console.log(countPokémon());
// Output : 6
console.log(countPokémon(6, 5, 6));
// Output : 17
Let's look at one more example for differentiating procedures and functions.
// Example 3. Identifying functions and procedures
function neighbouringPokémonID(x) {
x = Number(x);
return [x - 1, x + 1];
}
function generateNeighboursForTeam(team) {
var teamIDs = Object.keys(team);
teamIDs.forEach(element =>
console.log(neighbouringPokémonID(element)));
}
var myTeam = {
25: "Pikachu",
155: "Cyndaquil"
};
generateNeighboursForTeam(myTeam);
// Output :
// [24, 26]
// [154, 156]
This snippet effectively returns the Pokédex IDs of the immediate neighbours of a Pokémon, given its own ID.
Clearly, neighbouringPokémonID
is a function, as it has an input and return
s an output based upon it.
Also, generateNeighboursForTeam
is a procedure, as it doesn't return
anything.
Once again, we can modify this example such that both are functions.
// Example 4. Converting the procedure to a function
function neighbouringPokémonID(x) {
x = Number(x);
return [x - 1, x + 1];
}
function generateNeighboursForTeam(team) {
var teamIDs = Object.keys(team);
var neighbourIDs = [];
// Use a temporary array to store computation
teamIDs.forEach(element =>
neighbourIDs.push(neighbouringPokémonID(element)));
return neighbourIDs;
}
var myTeam = {
25: "Pikachu",
155: "Cyndaquil"
};
generateNeighboursForTeam(myTeam);
// Output :
// [[24, 26],[154, 156]]
3. Declarative vs Imperative Programming?
Another basic concept to be familiar with is, the distinction between Declarative and Imperative styles of coding, which is honestly a bit relative in its meaning.
There's no style that's absolutely Declarative or absolutely Imperative. It's a spectrum in itself.
That said, let's introduce ourselves to a common, simple definition.
“Imperative programming is like how you do something, and Declarative programming is more like what you do.”
It's a bit ambiguous and open-ended, so let's take a small example.
Suppose, you are trying to help your little brother with the basics of the latest Pokémon game. Specifically, about catching wild Pokémon.
Declarative: Throw a pokéball when the pokémon is weak. (What to do)
Imperative: When the health bar of the pokémon is below 30%, press X to throw a pokéball. (Exactly how to do it)
In general, explicitly listing out all the steps one by one, is Imperative. It's rather robotic to understand, and requires going through it line by line.
And utilizing some level of abstraction and trusted helper functions, to list the steps in a manner in which it only presents the basic idea, is Declarative. It's easier to understand, as we don't need to bother about how something is happening, rather what is happening.
As what and how can be rather subjective, we can't draw a hard boundary around what is declarative or imperative.
For example, for a person programming in machine language, which is super imperative, Java can seem rather declarative. Or for a person who works on a purely functional language, like Haskell or Clojure, even functional implementations in JavaScript can feel rather imperative.
Our concern at the moment, which is to set the foundations of Functional Programming and Functional JavaScript, we need to understand that we should make our code as declarative as possible, by utilizing functions.
Moving on, let's understand a bit more about Function Inputs and Outputs.
4. Function Inputs
This section covers some more aspects of Function Inputs, chiefly :
- Arguments and Parameters
- Defaulting Parameters
- Counting Inputs
- Arrays of Arguments
- Parameter Destructuring
- Benefits of Declarative Style
- Named Arguments
- Unordered Parameters
Let's get started.
a. Arguments and Parameters
There's often a slight confusion about the difference between arguments and parameters.
Put simply, arguments are the values you pass into a function
, and parameters are the named variables inside the function
that receive those values.
Note: In JavaScript, the number of arguments do not need to match the number of parameters. If you pass more arguments than you have declared parameters to receive them, the values pass in just fine.
These extra arguments can be accessed in a few ways, including the
args
object. If you pass fewer arguments, each unmatched parameter is assignedundefined
.
b. Defaulting Parameters
Parameters can declare default values. In the case where the argument for that parameter is not passed, or it's passed the value undefined
, the default assignment expression is substituted.
function f(x = 10) {
console.log(x);
}
f(); // Output : 10
f(undefined); // Output : 10
f(null); // Output : null
f(0); // Output : 0
It's always a good practice to think about any default cases that can aid the usability of your functions.
c. Arity, or Count of Inputs
The number of arguments a function
"expects" is determined by the number of parameters that are declared.
function f(x,y,z,w) {
// something
}
f.length;
// Output :
// 4
f(..)
expects 4
arguments, as it has 4
declared parameters. This count has a special term: Arity, which is the number of parameters in a function
declaration. The arity of f(..)
is 4
.
Furthermore, a function
with arity 1 is also called unary, a function
with arity 2 is also called binary, and a function
with arity 3 or higher is called n-ary.
The length
property of that function
reference returns its arity.
While this may sound simple, the implications are far-reaching.
One reason for determining the arity during execution would be if a piece of code received a function reference from multiple sources, and has to send different values depending on the arity of each.
For example, let's say a fn
function reference could expect one, two, or three arguments, but you always want to just pass a variable x
in the last position:
// `fn` is set to some function reference
// `x` exists with some value
if (fn.length == 1) {
fn(x);
}
else if (fn.length == 2) {
fn(undefined, x);
}
else if (fn.length == 3) {
fn(undefined, undefined, x);
}
Tip: The
length
property of a function is read-only and it's determined at the time you declare the function. It should be thought of as essentially a piece of metadata that describes something about the intended usage of the function.One gotcha to be aware of is that certain kinds of parameter list variations can make the
length
property of the function report something different than you might expect:
function foo(x,y = 2) {
// something
}
function bar(x,...args) {
// something
}
function baz( {a,b} ) {
// something
}
foo.length; // Output : 1
bar.length; // Output : 1
baz.length; // Output : 1
What about counting the number of arguments the current function call received? This was once trivial, but now the situation is slightly more complicated. Each function has an arguments
object (array-like) available that holds a reference to each of the arguments passed in. You can then inspect the length
property of arguments
to figure out how many were actually passed:
function f(x,y,z) {
console.log(arguments.length);
}
f(3, 4);
// Output :
// 2
As of ES5 (and strict mode, specifically), arguments
is considered by some to be sort of deprecated; many avoid using it if possible. However, arguments.length
, and only that, is okay to keep using for those cases where you need to care about the passed number of arguments.
A function signature that accepts an indeterminate amount of arguments is referred to as a variadic function.
Say you do need to access the arguments in a positional array-like way, possibly because you're accessing an argument that doesn't have a formal parameter at that position. How do we do it?
ES6 to the rescue! Let's declare our function with the ...
operator, referred to as "spread", "rest", or "gather":
function f(x,y,z,...args) {
// something
}
The ...args
in the parameter list is an ES6 declarative form that tells the engine to collect all remaining arguments (if any) not assigned to named parameters, and put them in a real array named args
. args
will always be an array, even if it's empty. But it will not include values that are assigned to the x
, y
, and z
parameters, only anything else that's passed in beyond those first three values.
function f(x,y,z,...args) {
console.log(x, y, z, args);
}
f(); // undefined undefined undefined []
f(1, 2, 3); // 1 2 3 []
f(1, 2, 3, 4); // 1 2 3 [ 4 ]
f(1, 2, 3, 4, 5); // 1 2 3 [ 4, 5 ]
So, if you want to design a function that can account for an arbitrary number of arguments, use ...args
.
You can use the ...
operator in the parameter list even if there are no other formal parameters declared.
function (...args) {
// something
}
args
will now be the complete array of arguments, whatever they are, and you can use args.length
to know exactly how many arguments have been passed in.
d. Arrays of Arguments
What if you wanted to pass along an array of values as the arguments to a function call?
function f(...args) {
console.log(args[3]);
}
var arr = [1, 2, 3, 4, 5];
f(...arr);
// Output :
// 4
Our new friend, the ...
operator is used here, but now not just in the parameter list; it's also used in the argument list at the call-site.
It has the opposite behavior in this context.
In a parameter list, we said it gathered arguments together. In an argument list, it spreads them out. So the contents of arr
are actually spread out as individual arguments to the f(..)
call.
Also, multiple values and ...
spreadings can be interweaved, as needed:
var arr = [2];
f(1, ...arr, 3, ...[4,5]);
// Output :
// 4
Think of
...
in this symmetric sense: in a value-list position, it spreads. In an assignment position -- like a parameter list, because arguments get assigned to parameters -- it gathers.
...
makes working with arrays of arguments much easier.
e. Parameter Destructuring
Consider the variadic f(..)
from the previous section:
function f(...args) {
// something
}
f( ...[1,2,3]);
What if we wanted to change that interaction so the caller of our function passes in an array of values instead of individual argument values? Just drop the two ...
usages:
function f(args) {
// something
}
f([1,2,3]);
Simple enough. But what if now we wanted to give a parameter name to each of the first two values in the passed-in array? We aren't declaring individual parameters anymore, so it seems we lost that ability.
Thankfully, ES6 destructuring is the answer. Destructuring is a way to declare a pattern for the kind of structure (object, array, etc.) that you expect to see, and how decomposition (assignment) of its individual parts should be processed.
Consider:
function f([x,y,...args] = []) {
// something
}
f([1,2,3]);
Do you spot the [ .. ]
brackets around the parameter list now? This is called array parameter destructuring.
In this example, destructuring tells the engine that an array is expected in this assignment position (aka parameter). The pattern says to take the first value of that array and assign it to a local parameter variable called x
, the second to y
, and whatever is left is gathered into args
.
f. Benefits of Declarative Style
Considering the destructured f(..)
we just looked at, we could instead have processed the parameters manually:
function f(params) {
var x = params[0];
var y = params[1];
var args = params.slice(2);
// something
}
But here we highlight a principle that declarative code communicates more effectively than imperative code.
Declarative code (for example, the destructuring in the former f(..)
snippet, or the ...
operator usages) focuses on what the outcome of a piece of code should be.
Imperative code (such as the manual assignments in the latter snippet) focuses more on how to get the outcome. The outcome is coded there, but it's not as clear because it's crowded by the details of how we got there.
The earlier f(..)
is regarded as more readable, because the destructuring hides the unnecessary details of how to manage the parameter inputs.
Wherever possible, we should strive for declarative, self-explanatory code.
g. Named Arguments
Just as we can destructure array parameters, we can destructure object parameters:
function f({x,y} = {}) {
console.log(x, y);
}
f({
y: 3
});
// Output :
// undefined 3
We pass in an object as the single argument, and it's destructured into two separate parameter variables x
and y
, which are assigned the values of those corresponding property names from the object passed in. It didn't matter that the x
property wasn't on the object; it just ended up as a variable with undefined
like you'd expect.
With a normal call-site like f(undefined,3)
, position is used to map from argument to parameter; we put the 3
in the second position to get it assigned to a y
parameter.
But at this call-site where parameter destructuring is involved, a simple object-property indicates which parameter (y
) the argument value 3
should be assigned to.
Some languages have an explicit feature for this: named arguments. In other words, at the call-site, labeling an input value to indicate which parameter it maps to. JavaScript doesn't have named arguments, but parameter object destructuring is the next best thing.
h. Unordered Parameters
Another key benefit is that named arguments, by virtue of being specified as object properties, are not fundamentally ordered. That means we can specify inputs in whatever order we want:
function f({x,y} = {}) {
console.log(x, y);
}
f({
y: 3
});
// Output :
// undefined 3
The call-site is no longer cluttered by ordered-placeholders like undefined
to skip a parameter.
Function Outputs
This section covers some more aspects of Function Outputs.
In JavaScript, functions
always return
a value. These three functions all have identical return
behavior:
function foo() {}
function bar() {
return;
}
function baz() {
return undefined;
}
The undefined
value is implicitly returned
if you have no return
or if you just have an empty return;
.
But keeping as much with the spirit of Functional Programming function definition as possible -- using functions and not procedures -- our functions should always have outputs, which means they should explicitly return
a value, and usually not undefined
.
A return
statement can only return a single value. So if your function needs to return multiple values, your only viable option is to collect them into a compound value like an array or an object:
function f() {
var retValue1 = 1;
var retValue2 = 3;
return [retValue1, retValue2];
}
Then, we'll assign x
and y
from two respective items in the array that comes back from f()
:
var [x, y] = f();
console.log(x + y);
// Output : 4
Collecting multiple values into an array (or object) to return, and subsequently destructuring those values back into distinct assignments, is a way to transparently express multiple outputs for a function.
Let's cover some concepts related to Function Outputs, chiefly :
- Early Returns
- Un
return
ed Outputs - Higher-Order Functions (HOFs or Functions of Functions)
a. Early Returns
The return
statement doesn't just return a value from a function
. It's also a flow control structure; it ends the execution of the function
at that point.
A function
with multiple return
statements thus has multiple possible exit points, meaning that it may be harder to read a function to understand its output behavior if there are many paths that could produce that output.
Consider:
function f(x) {
if (x > 10) return x + 1;
var y = x / 2;
if (y > 3) {
if (x % 2 == 0) return x;
}
if (y > 1) return y;
return x;
}
f(2); // Output : 2
f(4); // Output : 2
f(8); // Output : 8
f(12); // Output : 12
First off, f(x)
is highly unreadable and difficult to follow. Dry-running this in your mind is quite tedious. That's because we're using return
not just to return different values, but also as a flow control construct to quit a function's execution early in certain cases.
Consider this version of the code:
function f(x) {
var retValue;
if (retValue == undefined && x > 10) {
retValue = x + 1;
}
var y = x / 2;
if (y > 3) {
if (retValue == undefined && x % 2 == 0) {
retValue = x;
}
}
if (retValue == undefined && y > 1) {
retValue = y;
}
if (retValue == undefined) {
retValue = x;
}
return retValue;
}
This version is unquestionably more verbose. But it is slightly simpler logic to follow, because every branch where retValue
can get set is guarded by the condition that checks if it's already been set.
Rather than return
ing from the function early, we used normal flow control (if
logic) to determine the retValue
's assignment. At the end, we simply return retValue
.
In summary, it's more readable to have just a single return
at the end. Try to figure out the most explicit way to express the logic.
b. Unreturn
ed Outputs
One technique that you've probably used in most code you've written, and maybe didn't even think about it much, is to have a function output some or all of its values by simply changing variables outside itself.
Remember our f(x) = x2 - 1
function from earlier? We could have defined it like this in JS:
var y;
function f(x) {
y = (2 * Math.pow( x, 2 )) + 3;
}
We could just as easily have return
d the value instead of setting it into y
from within the function:
function f(x) {
return (2 * Math.pow( x, 2 )) + 3;
}
Both functions accomplish the same task, so is there any reason we should pick one version over the other?
One way to explain the difference is that the return
in the latter version signals an explicit output, whereas the y
assignment in the former is an implicit output.
But changing a variable in an outer scope, as we did with the y
assignment inside of f(..)
, is just one way of achieving an implicit output. A more subtle example is making changes to non-local values via reference.
Consider:
function sum(list) {
var total = 0;
for (let i = 0; i < list.length; i++) {
if (!list[i]) list[i] = 0;
total = total + list[i];
}
return total;
}
var nums = [ 1, 3, 9, 27, , 84 ];
sum(nums);
// Output :
// 124
The most obvious output from this function is the sum 124
, which we explicitly return
ed. But instead of an undefined
empty slot value in position 4
, now there's a 0
.
The harmless looking list[i] = 0
operation ended up affecting the array value on the outside, even though we operated on a local list
parameter variable.
Why? Because list
holds a reference-copy of the nums
reference, not a value-copy of the [1,3,9,..]
array value. JavaScript uses references and reference-copies for arrays, objects, and functions, so we may create an accidental output from our function all too easily.
This implicit function output has a special name in the FP world: Side Effects. And a function that has no side effects also has a special name: Pure Function. Both these concepts will be covered in the next article.
c. Higher-Order Functions (HOFs or Functions of Functions)
Functions can receive and return values of any type. A function that receives or returns one or more other function values has the special name: higher-order function.
Consider:
function forEach(list,fn) {
for (let v of list) {
fn( v );
}
}
forEach( [1,2,3,4,5], function each(val){
console.log( val );
} );
// Output :
// 1 2 3 4 5
forEach(..)
is a higher-order function because it receives a function as an argument.
A higher-order function can also output another function, like:
function f() {
return function upper(x){
return x.toUpperCase();
};
}
var g = f();
g("Hello!");
// Output :
// HELLO!
return
is not the only way to "output" an inner function:
function f() {
return g(function upper(x){
return x.toUpperCase();
} );
}
function g(func) {
return func("Hello!");
}
f();
// Output :
// HELLO!
Functions that treat other functions as values are higher-order functions by definition. These are very crucial to Functional Programming!
Summary
We covered the following concepts in this article :
- What are Functions?
- Functions versus Procedures
- Declarative versus Imperative Programming
- Function Inputs
- Arguments and Parameters
- Defaulting Parameters
- Counting Inputs
- Arrays of Arguments
- Parameter Destructuring
- Benefits of Declarative Style
- Named Arguments
- Unordered Parameters
- Function Outputs
- Early Returns
- Un
return
ed Outputs - Higher-Order Functions (HOFs or Functions of Functions)
The next article will cover :
- Function Purity (Pure versus Impure Functions)
- Side Effects
- Extracting and Containing Impurity
- How all of this collectively defines what Functional Programming is, and why it is used
- Is
JavaScript
a Functional Programming Language? - Why consider Functional Programming style for your code?
Credits
- Functional-Light JS book by Kyle Simpson, for inspiring this article, as well as,
- FrontEnd Masters : Functional-Light JavaScript
Thanks a lot for reading! ❤️
Dev.to | Twitter | Hashnode | Medium | GitHub | LinkedIn | Buy Me A Coffee