Functional programming has been a current in software development since the earliest days, but has taken on new importance in the modern era. This article looks at the concepts behind functional programming and offers a practical understanding with examples in JavaScript and Java.
Functional programming defined
Functions are fundamental to code organization; they exist in all higher order programming languages. Generally, functional programming means using functions to the best effect for creating clean and maintainable software. More specifically, functional programming is a set of approaches to coding, usually described as a programming paradigm.
Functional programming is sometimes defined in opposition to object-oriented programming (OOP) and procedural programming. That is misleading as these approaches are not mutually exclusive and most systems tend to use all three.
Functional programming offers clear benefits in certain cases, it’s used heavily in many languages and frameworks, and it’s prominent in current software trends. It is a useful and powerful tool that should be part of the conceptual and syntactic toolkit of every developer.
Pure functions
The ideal in functional programming is what is known as pure functions. A pure function is one whose results are dependent only upon the input parameters, and whose operation initiates no side effect, that is, makes no external impact besides the return value.
The beauty of a pure function is in its architectural simplicity. Because a pure function is reduced to only the arguments and return value (that is, its API), it can be seen as a complexity dead end: Its only interaction with the external system in which it operates is via the defined API.
This is in contrast to OOP where object methods are designed to interact with the state of the object (the object members) and in contrast to procedural style code where external state is often manipulated from within the function.
However, in actual practice, functions often end up needing to interact with the broader context, as evidenced by React's useEffect
hook.
Immutability
Another tenet of functional programming philosophy is not to modify data outside the function. In practice, this means to avoid modifying the input arguments to a function. Instead, the return value of the function should reflect the work done. This is a way of avoiding side effects. It makes it easier to reason about the effects of the function as it operates within the larger system.
First class functions
Beyond the pure function ideal, in actual coding practice functional programming hinges on first class functions. A first class function is a function that is treated as a “thing in itself,” capable of standing alone and being treated independently. Functional programming seeks to take advantage of language support in using functions as variables, arguments, and return values to create elegant code.
Because first class functions are so flexible and useful, even strongly OOP languages like Java and C# have moved to incorporate first class function support. That is the impetus behind Java 8’s support for Lambda expressions.
Another way to describe first class functions is functions as data. That is to say, a first class function can be assigned to a variable like any other data. When you write let myFunc = function(){}
you are using a function as data.
Higher-order functions
A function that accepts a function as an argument, or returns a function, is known as a higher-order function — a function that operates upon a function.
Both JavaScipt and Java have added improved function syntax in recent years. Java added the arrow operator and the double colon operator. JavaScript added the arrow operator. These operators are designed to make it easier to define and use functions, especially inline as anonymous functions. An anonymous function is one that is defined and used without being given a reference variable.
Functional programming example: Collections
Perhaps the most prominent example of where functional programming shines is in dealing with collections. This is because being able to apply chunks of functionality across the items in a collection is a natural fit to the pure function idea.
Consider Listing 1, which takes advantage of the JavaScript map()
function to uppercase the letters in an array.
Listing 1. Using map() and an anonymous function in JavaScript
let letters = ["a", "b", "c"]; console.info( letters.map((x) => x.toUpperCase()) ); // outputs ["A", "B", "C"]
The beauty of this syntax is that the code is tightly focused. No imperative plumbing, such as a loop and array manipulation, is required. The thought process of what is being done is cleanly expressed by this code.
The same thing is achieved with Java’s arrow operator as seen in Listing 2.
Listing 2. Using map() and an anonymous function in Java
import java.util.*; import java.util.stream.Collectors; import static java.util.stream.Collectors.toList; //... List lower = Arrays.asList("a","b","c"); System.out.println(lower.stream().map(s -> s.toUpperCase()).collect(toList())); // outputs ["A", "B", "C"]
Listing 2 makes use of Java 8’s stream library to perform the same task of upper-casing a list of letters. Notice that the core arrow operator syntax is virtually identical to JavaScript, and they do the same thing, i.e., create a function that accepts arguments, performs logic, and returns a value. (It’s important to note that if the function body thus defined lacks braces around it, then the return value is automatically given.)
Continuing with Java, consider the double colon operator in Listing 3. This operator allows you to reference a method on a class: in this case, the toUpperCase
method on the String class. Listing 3 does the same thing as Listing 2. The different syntaxes come in handy for different scenarios.
Listing 3. Java Double Colon Operator
// ... List upper = lower.stream().map(String::toUpperCase).collect(toList());
In all three examples above, you can see that higher-order functions are at work. The map()
function in both languages accepts a function as the argument.
Put another way, you could look at the passing of functions into other functions (in the Array API or otherwise) as functional interfaces. The provider functions (which consume the parameter functions) are plug-ins to generalized logic.
This looks a lot like a strategy pattern in OOP (and indeed, in Java, an interface with a single method is generated under the hood) but the compactness of a function makes for a very tight component protocol.
As another example, consider Listing 4, which defines a route handler in the Express framework for Node.js.
Listing 4. Functional route handler in Express
var express = require('express'); var app = express(); app.get('/', function (req, res) { res.send('One Love!'); });
Listing 4 is a fine example of functional programming in that it allows for the clean definition of exactly what is required for mapping a route and handling requests and responses — although it might be argued that manipulating the response object within the function body is a side effect.
Curried functions
Now consider the functional programming notion of functions that return functions. This is less commonly encountered than functions as arguments. Listing 5 has an example from a common React pattern, where the fat-arrow syntax is chained.
Listing 5. A curried function in React
handleChange = field => e => { e.preventDefault(); // Handle event }
The purpose of the above is to create an event handler that will accept the field in question, and then the event. This is useful because you can apply the same handleChange
to multiple fields. In short, the same handler is usable on multiple fields.
Listing 5 is an example of a curried function. “Curried function” is a bit of a frustrating name. It honors a person, which is nice, but it doesn’t describe the concept, which is confusing. In any case, the idea is that when you have functions that return functions, you can chain together calls to them, in a more flexible way than creating a single function with multiple arguments.
When calling these kinds of functions, you will encounter the distinctive “chained parentheses” syntax: handleChange(field)(event)
.
Programming in the large
The preceding examples offer a hands-on understanding of functional programming in a focused context, but functional programming is intended to drive greater benefits to programming in the large. Put another way, functional programming is intended to create cleaner, more resilient, large-scale systems.
It’s hard to provide examples of this, but one real-world instance is React’s move to promote functional components. The React team has noted that the more concise functional style of component offers benefits that compound as interface architecture grows larger.
Another system that makes use of functional programming heavily is ReactiveX. Large-scale systems built on the kind of event streams that ReactiveX uses can benefit from decoupled software component interaction. Angular fully adopts ReactiveX (RxJS) across the board as an acknowledgment of this power.
Variable scope and context
Finally, an issue that is not necessarily part of functional programming as a paradigm, but is very important to pay attention to when doing functional programming, is that of variable scope and context.
In JavaScript, context specifically means what the this
keyword resolves to. In the case of the JavaScript arrow operator, this
refers to the enclosing context. A function defined with the traditional syntax receives its own context. Event handlers on DOM objects can take advantage of this fact to ensure that the this
keyword refers to the element being handled.
Scope refers to the variable horizon, that is, what variables are visible. In the case of all JavaScript functions (both fat-arrow and traditional) and in the case of Java’s arrow-defined anonymous functions, the scope is that of the enclosing function body—although in Java, only those variables that are effectively final can be accessed. This is why such functions are referred to as closures. The term means the function is enclosed within its containing scope.
This is important to remember: Such anonymous functions have full access to the variables in scope. The inner function can operate against the outer function’s variables. This could be considered a non-pure function side effect.