Functional Programming - What Is It and Why Does It Matter?
While the concept has been around for quite some time, functional programming finally came from out of obscurity to the forefront of the JavaScript community a few years ago. Developers of large applications now regularly incorporate its principles into their codebase. If you're new to functional programming, this guide will get you up to speed.
What is functional programming?
Functional programming (FP) is a programming paradigm for developing software using functions. Following the FP philosophy entails foregoing things like shared states, mutable data and side effects. Functional programming is a declarative paradigm because it relies on expressions and declarations rather than statements. Unlike procedures that depend on a local or global state, value outputs in FP depend only on the arguments passed to the function. Taking outside effects, or state changes that don't depend on function inputs, makes software behave more predictably, which is a major selling point for many FP coders. Code written with the principles of FP in mind is called functional code.
Another programming paradigm that is more familiar to casual coders is object-oriented programming, or OOP, which takes advantage of application states and methods. These days, OOP is much more widespread than FP.
However, functional code is often more condensed and easier to test; although at first glance it may look unintelligible to newcomers. FP has a steep learning curve, so don't expect to learn everything you need to know about it from one tutorial. Nonetheless, if you're relatively experienced with JavaScript, you've likely incorporated some functional programming concepts into your code, perhaps even without knowing it.
Functional programming terminology
Before you can become an FP expert, there is some jargon you need to understand:
Pure functions
Pure functions always return the same results when given the same inputs. Consequently, they have no side effects. Pure functions allow referential transparency, which means you can replace a function call with its resulting value.
Function composition
Function composition means combining functions to make a new one or to perform a computation. A single dot is used to combine functions, so the composition a . b
means the same as a(b(x))
in JavaScript.
Shared states
Shared states are frequently used in object oriented programming to add properties to objects. For instance, if a computer game has a master game object, characters and items could be stored as properties of that object. Shared states can be useful in some case, but they also present challenges for functions, which is why they are avoided in FP.
Avoiding shared states ensures that the timing and order of function calls do not impact the results of calculations. Therefore, function calls remain completely independent of one another, which makes refactoring and other alterations less of a headache. While the order of operations still applies, anything that happens to variables outside of a function doesn't affect their value in FP. Otherwise, you'd have to know every variable's history.
Immutability
Since FP doesn't depend on shared states, all data in functional code must be immutable, or incapable of changing. Do not confuse immutability with JavaScript's const declaration, which is used for variable binding. To make a value immutable, you can deep freeze it. JavaScript offers a method for freezing objects one-level deep, but that still doesn't make them completely unalterable.
Functional programming languages take advantage of something called trie data structures. Pronounced like "tree," trie data structures are essentially deep frozen so that properties cannot be changed at any level in the object hierarchy. Through the process of structural sharing, they consume less memory and therefore perform better than traditional data structures. GitHub has JavaScript libraries for a few different trie data structures including Immutable.js and Mori.
Side effects
Side effects are any state changes that occur outside of a called function aside from the returned value. A major goal of functional programming is to minimize side effects, which is accomplished by isolating them from the rest of the software code. Separating side effects from the rest of your logic can make a program easier to maintain, test, debug, extend and refactor. Functional languages like Haskell isolate side effects using structures called monads.
The basics of functional programming
Pure functions operate only on their input parameters. Take the following JavaScript code:
var z = 15;
function add(x, y) {
return x + y;
}
Since the z
variable isn't included in the add function, the function only reads and writes to its inputs, x
and y
. Therefore, the function is pure. Including z
would make it impure.
To be useful, pure functions must take parameters and return something. You can make use of the above pure function as follows:
function add(x, y) {
return x + y;
}
console.log(add(1, 2)); // 3
Pure functions always return the same output when given the same input. Functions like writeFile(fileName)
, updateDatabaseTable(sqlCmd)
and sendAjaxRequest(ajaxRequest)
are not pure because they affect external variables and therefore have side effects that can't be predicted.
Of course, there is only so much you can do with pure functions, and functional languages can't entirely eliminate side effects; they just confine them. Parts of your code have to be impure, but the goal of functional programming is to limit impure code and keep it separate from everything else.
Variables that don't vary
To illustrate the difference between imperative programming and functional programming, let's look at a small code sample:
var x = 1;
x = x + 1;
In imperative programming, this means take the current value of x
, add 1
and put the result back into x
. In functional programming, however, x = x + 1
is illegal. That's because there are technically no variables in functional programming. While we still call stored values variables in FP, those values are always the same, so they don't actually vary.
How can you accomplish anything without variables? Using immutable data structures, you can make single or multi-valued changes by copying the variables and calculating new values, which is discussed more in the next section.
Looping in functional programming
Functional programming doesn't use loop constructs such as repeat
, for
and while
. Instead, it relies on recursion for looping. Take the following JavaScript examples:
var acc = 0;
for (var i = 1; i <= 10; ++i)
acc += i;
console.log(acc); // 55
The simple loop construct can be made functional like this:
function sumRange(start, end, acc) {
if (start > end)
return acc;
return sumRange(start + 1, end, acc + start)
}
console.log(sumRange(1, 10, 0)); // 55
These two very different approaches give you the same value, 55. In the latter example, recursion performs the same task as the loop by calling itself with a new start and a new accumulator. The original values are not modified; rather, new values are calculated using the old ones.
While functional programming is possible in JavaScript, it may make more sense to use a language specifically designed for it. Here's how the example above would look in the Elm language:
sumRange start end acc =
if start > end then
acc
else
sumRange (start + 1) end (acc + start) // 55
The code is a bit more streamlined, and you still get a value of 55. While using loops may look less intimidating to you right now, using recursion allows you to keep things immutable. One of the biggest benefits of immutability is that since you only have read access to values, you don't have to worry about anyone accidentally changing them and causing problems.
Reusability and higher order functions
Another perk of functional programming is that it facilitates reuse of functional utilities to more efficiently process data. Unlike OOP, which colocates methods and data in objects so that they can only operate on designated data types, all data types are fair game in functional programming.
Fortunately, JavaScript offers first-class functions, which means that they can be treated like data, assigned to variables and passed to other functions. For example, you could use the higher order function map()
to map over numbers, objects, and strings. Higher order functions take other functions as arguments or return other functions. They are helpful for creating utilities that can act on many different data types, partially applying functions to its arguments or creating curried functions for reuse.
map()
Speaking of which, the higher order functions map()
and reduce()
offer a better alternative to iterating over lists. If you have a function and a collection of items, map()
can run the function on each item and insert the return values into the new collection. For example, this simple map written in Python 2 takes a list of names and returns the character lengths of those names:
name_lengths = map(len, ["Bob", "Rob", "Bobby"])
print name_lengths
// [3, 3, 5]
Here is another map that squares each number in a collection:
squares = map(lambda x: x * x, [0, 1, 2, 3, 4])
print squares
// [0, 1, 4, 9, 16]
As you can see, there isn't a named function in the above example. Instead, the map takes an inlined, anonymous function defined with lambda. Functional programming is based on lambda calculus, so anything you learn about that subject will help your FP coding. In this example, lambda's parameters are defined on the left side of the colon, and the function body is defined on the right side.
Now, let's make some non-functional code functional. The following code randomly assigns a list of individuals to one of two teams:
import random
names = ['Seth', 'Ann', 'Morganna']
team_names = ['A Team', 'B Team']
for i in range(len(names)):
names[i] = random.choice(team_names)
print names
// ['A Team', 'B Team', 'B Team']
Using functional principles, we can rewrite the code as a map:
import random
names = ['Seth', 'Ann', 'Morganna']
team_names = map(lambda x: random.choice(['A Team',
'B Team']),
names)
reduce()
reduce()
is another higher order function for performing iterations. It takes functions and collections of items, and then it returns the value of combining the items. Here is a simple example that outputs the sum of a collection of numbers:
sum = reduce(lambda a, x: a + x, [0, 1, 2, 3, 4])
print sum
// 10
The x
variable represents the item currently being iterated over, and the a
variable is the accumulator, or the value returned after executing lambda on the previous item. In the first iteration, a
takes the value of the first item, so iteration doesn't begin until the second item. Therefore, the first x
is actually the second item. As the reduce()
function iterates, it runs lambda on the current a
and x
variables, and the result becomes the a
of the next iteration.
It may be difficult for you to see the advantages of using map()
and reduce()
for iterations at first, but using them can provide several benefits for a program's overall code. There are also other higher order functions like filter()
and fold()
that you should familiarize yourself with if you want to become an FP pro.
Declarative vs imperative
Because functional programming is a declarative paradigm, program logic is expressed without explicit instructions for flow control. While imperative programs dedicate many lines of code to flow control, declarative programs abstract the process.
For example, the following imperative mapping takes an array of numbers and returns another array with each number multiplied by 3:
const tripleMap = numbers => {
const triple = [];
for (let i = 0; i < numbers.length; i++) {
triple.push(numbers[i] * 3);
}
return triple;
};
console.log(tripleMap([5, 6, 7])); // [15, 18, 21]
The same thing can be accomplished with declarative mapping much more succinctly thanks to the functional Array.prototype.map()
utility:
const tripleMap = numbers => numbers.map(n => n * 3);
console.log(tripleMap([5, 6, 7])); // [15, 18, 21]
Imperative code makes frequent use of statements like for
, if
and switch
. Declarative code, on the other hand, relies more heavily on expressions. Expressions are pieces of code that evaluate and return a value, so they are composed of things like function calls, values, and operators. In FP, expressions can be assigned to identifiers, returned from functions or passed into a function.
Summary
Working in the web development industry requires adaptability. The more you know, the more opportunities you'll have for advancing your career. If you don't keep expanding your repertoire of skills, you could find yourself unable to compete in the job market.
If you're looking for a way to write code more concisely, concurrently and even want to improve the performance of your code, learning how to use functional programming will be beneficial.