Minimise Side Effects
A side effect is a change that is not local to the function that caused it. A function might do something like manipulate the DOM, modify the value of a variable in a higher level scope or write data to a database. The results of these actions are side effects.
Side effects are not inherently evil. A program that produced no side effects would not affect the world, and so there would be no point to it (other than perhaps as a theoretical curiousity). They are, however, dangerous and should be avoided wherever they are not strictly necessary.
When a function produces a side effect you have to know more than just its inputs and output to understand what that function does. You need to know about the context and history of the state of the program, which makes the function harder to understand. Side effects can cause bugs by interacting in unpredictable ways, and the functions that produce them are harder to test thanks to their reliance on the context and history of the program's state.
Minimising side effects is such a fundamental principle of functional programming that most of the following sections can be understood as outlining techniques to avoid them.
Treat Data as Immutable
Why would we want to avoid mutating data?
A mutation is a side effect. The fewer things that change in a program, the less there is to keep track of, and the result is a simpler program.
Object.freeze, but only one level deep:
There are, however, several excellent libraries out there that solve this issue, the most well-known of which is Immutable.
For most applications, using a library to enforce immutability is overkill. In most cases you will gain most of the benefits of immutable data simply by treating data as though it were immutable.
Avoiding Mutations: Arrays
concat can be used instead of
push mutates the original array, whereas
concat returns a new array comprised of the array it was called on and the array provided as its argument, leaving the original array intact.
Avoiding Mutations: Objects
Instead of directly editing objects, you can use
Object.assign, which copies the properties of source objects into a target object and then returns it. If you always use an empty object as the target object, you can use
Object.assign to avoid directly editing objects.
const is useful, but it does not make your data immutable. It prevents your variables from being reassigned. These two things should not be conflated.
Write Pure Functions
A pure function is a function that does not change the program's state and does not produce an observable side effect. The output of a pure function relies solely on its input values. Wherever and whenever a pure function is called, its return value will always be the same when given the same inputs.
Pure functions are an important tool for keeping side effects to a minimum. In addition, their indifference to context make them highly testable and reusable.
myFunc from the section on side effects is an impure function: note how it's called twice with the same input and gives a different result each time. It could, however, be re-written as a pure function:
Ultimately, your program will always produce some side effects. Where they occur they should be handled carefully and their effects constrained and contained as much as possible.
Write Function-Generating Functions
Find someone who has never programmed before and ask them to guess what the following pieces of code do.
Everyone I've tried this test on has had more luck with the second example. Example One exemplifies an imperative approach to printing out a list of numbers. Example Two exemplifies a declarative approach. By packaging away the details of how to loop through an array and how to print to the console into the functions
Adopting this approach involves writing a lot of functions. This process can be made DRY-er by writing functions to generate new functions from existing ones.
Combined, these features allow you to write functions that return other functions which "remember" the arguments passed to the function that generated them, and are able to use those arguments elsewhere in the program.
Functions can be combined to form new functions through function composition. Here is an example:
You may find yourself repeating this pattern of generating a more complex function from smaller functions. Often it's more efficient to write a function that does the composition for you:
You could go even further and write a more general composition functions:
The exact form of your composition function will depend on the level of generality you need and the kind of API you prefer.
Partial Function Application
Partial function application is the process of fixing the value of one or more of a function's arguments, and then returning the function to be fully invoked later.
In the following example,
quadruple are partial applications of
Currying is the process of translating a function that takes multiple arguments into a series of functions that each take one argument.
Currying and partial application are conceptually similar (and you'll probably never need both), but still distinct. The main difference is that currying will always produce a nested chain of functions that each accept only one argument, whereas partial application can return functions that accept more than one argument. This distinction is clearer when you compare their effects on functions that accept at least three arguments:
A recursive function is a function that calls itself until it reaches a base condition. Recursive functions are highly declarative. They're also elegant and very satisfying to write!
Here's an example of a function that recursively calculates the factorial of a number:
However, this can be avoided with tail call optimisation.
Tail Call Optimisation
A tail call is a function call that is the last action of a function. Tail call optimisation is when the language compiler recognises tail calls and reuses the same call frame for them. This means that if you write recursive functions with tail calls, the limits of the call stack will never be exceeded by them as it will reuse the same frame over and over.
Here is the recursive function from above rewritten to take advantage of tail call optimisation:
Functional programming contains many ideas that we can use to make our own code simpler and better. Pure functions and immutable data minimise the hazards of side effects. Declarative programming maximises code readability. These are important tools that should be embraced in the fight against complexity.