Function Implementations & Mutability
In the first in this series, we started with the most foundational of foundations: What functional programming is, is not, and what functions are, and how to describe them. For most programmers this may have been somewhat mundane, however, we also introduced some best practices as well, and laid the groundwork for how these articles will discuss and demonstrate functions and functional programming.
We will further delve into more aspects of functional programming with this installment of the series. First, we will be covering the key tenets of functional programming and why they matter. We will also cover mutability and the contexts where it might actually be “allowable” and quite beneficial when done carefully and properly.
Tenets of Functional Implementation
Beyond the anatomical nature of a function itself are the tenets of their implementations. Some functional languages strictly enforce some of these tenets, such as immutability. Others are encouraged as much as possible, such as purity. These are again aspects of developer discipline with JavaScript and TypeScript, however, following them ensures you gain the most out of functional programming.
Some of the key tenets of functional programming include:
- Purity
- Immutability
- Composability
There are more tenets that could be listed, however, these are some of the most fundamental. We will explore these aspects more deeply in future articles in this series.
Purity
A key tenet of functional programming is function purity. Purity is one of the highest goals to aim for when implementing functions. Not every function can be pure, and without impurities in some functions, software generally cannot do anything truly useful, at least not outside of an academic context. However, whenever possible, aim to implement pure functions. They are usually simpler, easier to test, less prone to causing execution errors themselves, and when composed lead to fewer issues than impure functions.
Purity, with regards to functions, is defined as follows:
- Its return value is the same for the same arguments (no variation with local static variables, non-local variables, mutable reference arguments, or input streams from I/O devices).
- Its evaluation has no side effects (no mutation of local static variables, non-local variables, mutable reference arguments, or I/O streams).
In other words, pure functions operate solely on their inputs, and for any given input, the output is the same. A simple test of functional purity is, for a given set of inputs, the function call could be replaced with the value of the output, and the function would operate the same for that initial state.
Note that a given output may be returned for more than one input, if appropriate…a function is still pure. The key tenet is that for a given input, the same output is always returned. This ultimately allows us to implement various performance optimizations if necessary…such as memoization (returning a previously computed value, if the input has not changed on subsequent calls, thus avoiding re-computation).
// pure! operates only on inputs, no observable side effects, same value returned for any given inputs export const add = (x: number) => (y: number): number => x + y; // impure! writes to the console log!! <- observable side effect export const log = (..msgs: string[]): void => console.log(...msg); // impure! updates DOM!! <- observable side effect export const hideElem = (id: string): void => document.getElementById(id).style.display = 'none'; export const showElem = (id: string, display = 'block'): void => document.getElementById(id).style.display = display;
Side Effects
Side effects are a key consideration when implementing pure functions. Side effects are also often obscure, and simple, mundane things you might not think are side effects, in fact, actually are. For example, any kind of logging…is a side effect. If a function logs, it is performing a side effect and is therefore no longer pure! Just about any change to any state, anywhere, for any reason, is in fact a side effect.
A truly pure function causes no side effects at all. This is primarily achieved by operating SOLELY on inputs, without any reference to any other state. Further, in order for input-only operations to be truly pure, a pure function may not mutate those inputs directly and instead must compute a new value for output. Mutation of an input (say a reference to an object) can cause all manner of problems down the road to other code that references the same objects, and is definitely a side effect!
There is one narrow window of opportunity where mutations may be performed, and not have side effects. This allowable mutation, often supported in purely functional languages, will be covered a little later in the article.
Immutability
Another key tenet of functional programming is immutability. Immutability refers to the inability of previously referenced data from changing unexpectedly. More strictly, especially in truly functional languages with immutable internal data constructs, no data may ever actually change outside of explicit mutative contexts, and some functional languages do not even have that option! Those that do…only allow data to mutate in a manner that cannot possibly cause side effects to any existing references.
Immutability effectively guarantees that data cannot change for a given reference to it. An “older” reference to immutable data will always point to that state or “shape” of the data. Always.
// immutable: clones object const address = getAddress(...); const customerWithAddress = { ...customer, address };
When data needs to change, instead of mutating the data, which will change the data that every existing reference points to (thus potentially introducing side effects or unexpected errors in any area of your application that references said data), a new version of the data will be created.
Immutable data handling often requires more deliberate thought about exactly how to achieve this goal of immutability, however in the long run the benefits far outweigh the costs. With practice, writing code that ensures immutability becomes second nature, programmed procedural memory that will automatically produce the necessary constructs whenever you need to augment data in a function.
// mutated: modifies object const address = getAddress(...); customer.address = address; // Observable side effects!
Give yourself time to fully make the paradigm shift to programming with immutable data. Use frameworks that support and even enforce immutable data. Do what you can yourself to ensure immutable data (i.e. Object.freeze). Protect yourself from yourself, and ultimately you will reap the benefits that come with leveraging immutable data everywhere.
Composability
The final tenet I’ll cover here is function composability. This becomes important when you start thinking about how to avoid using variables! Without variables you have to start thinking about other ways to move data around. That often includes using functional tools to compose many function calls together in various ways.
A common compositional tool in functional languages is the functional pipe. You may have noticed the pipe()
function used in the code example in the “Declarative by Nature” section above. A pipe is something that accepts an input and a list of functions to execute. The initial input is passed to the first function. The return value from that function is passed to the next, and so on and so forth.
Point Free
The passing of the inputs to each function within a pipe is usually done “point free” (which you may also have noticed mention of in the Declarative by Nature code example.) Point free parameter passing, where a parameter or parameters is passed “implicitly” rather than explicitly, is a common facet of functional composition.
It can be a confusing aspect, however, it is also one of the key ways we solve the problem of avoiding variables, variable assignment, function blocks, etc. These are all things that increase the complexity of code and muddy up what can otherwise be very neat, clean, “story-like” code.
export const combineCustomersWithOrdersAndFilterHighTotals = ( orders: Order[], minTotal: number): CustomerWithOrders[] => pipe( // v-- initial customers: Customer[] passed in here (point free!) filterCustomersWithOrders(orders), // --| // |-- filtered customers -----------------| // v passed in here (point free!) attachCustomersToTheirOrders(orders), // --| // |-- customers with orders -----------------| // v passed in here (point free!) filterCustomersWithHighTotals(minTotal) // --| // |-- filtered customers with orders ----------| // |--> customers with orders filtered by totals returned here, // and returned by pipe() to caller of // combineCustomerswithOrdersAnfFilterHighTotals() ); // <- customers: Customer[] as initial input (implied, point free)
We will dive deeper into functional composability, point free functions, and in fact the very nature and implementation of the pipe()
function in a future article of this series when we delve into higher order functions and currying.
Mutability
Ah, here we go. The great Monster of Mutability!! It is one of the things about imperative programs with all their variables and properties and nested structures that can lead to so many little and obscure bugs.
One of the key sources of complexity and potential bugs in programs, in general, is when data is mutated. Mutated data may result in unexpected outcomes. Many pieces of code may reference a single piece of data. Depending on exactly how that data is used, mutating its value (changing the value that is referenced by all of these pieces of code point to or reference) may result in unexpected charges.
Some changes may be that something does not happen…such as a UI not updating to reflect the new value. Other changes may be a newly computed value indeed using the updated value, but no longer being in alignment with other computed values. Mutations, in general, are something that should be avoided as a general rule, and in fact, they are relatively easy to avoid once you learn how.
Performance Considerations
When it comes to relying solely on immutable data, there are performance factors to consider. Constantly copying data, especially if it is larger data sets, comes with certain costs, and potentially non-trivial if even performance-devastating costs.
The performance of immutable data structures in functional languages has been a subject of research and optimization for decades, and there are many data structures and approaches to handling immutable data that can greatly improve performance, largely normalizing it with the performance of code that mutates.
Time and Place
As mentioned earlier in the side effects section, there is a time and a place for mutations. These cases are rare, and must be managed with great care…however there are times when data may be mutated without observable side effects. Note the use of observable here…this is one of the key tenets of pure functions…side effects must be observable in order to potentially lead to bugs. Side effects that cannot be observed cannot lead to bugs… however, they may well lead to much faster code!
Some true functional languages support explicit mutative contexts wherein data may be mutated. These contexts are usually explicitly called out using one facility or another of the language. Sadly neither JavaScript nor TypeScript (currently!) support such clarity (perhaps someday!) That said, with proper care, mutations may be made to data without running the risk that said data causes unexpected observable side effects elsewhere in the application. Notably, when the mutations are made to data not yet referenced by any susceptible part of the code. In other words: unobservable side effects.
Mutating Internal Data
If a piece of data is not yet referenced by code that might suffer from a mutation, a mutation may be acceptable. More explicitly, within the context of a pure function, before the function returns, if data is created, and then mutated within the same context and call, such mutations are inherently acceptable.
This is a powerful concept and can lead to great, sometimes immense performance improvements in code that is often, and quite naturally so, less performant than imperative alternatives (that is a large enough subject for its own article!) When data is copied around and only copies changed, unless the very underlying nature of the platform supports this mechanism in the most efficient manner possible, it inevitably leads to more computations, more memory, and lower performance. Using mutations when possible in contexts that will not cause unexpected side effects is a critical tool in the functional JavaScript or TypeScript programmer’s toolbox.
An example of the use of mutations can be found in the implementation of NgRx Auto-Entity’s entity transform
feature. Transformation of an entity is performed by declaratively attaching and composing transform handlers to an entity through a decorator. A complete composition of a transformation may require performing many changes to an entities data, and if the data is copied repeatedly by each composable part of a transformation pipeline, it could decimate the performance of heavily used code. Even in simpler applications, use cases have been found where efficiency in this process is of the utmost importance.
@Entity('Customer', { transform: [ ISODateToDate('dateCreated'), ISODateToDate('dateUpdated'), ISODateToDate('lastContactedAt'), AddressStringToAddressStruct('shippingAddress'), AddressStringToAddressStruct('billingAddress'), AddressStringToAddressStruct('corporateAddress'), ... ]}) export class Customer { ... }
Internally, Auto-Entity will clone the original data (from server) or entity (from client) before passing it through each transform. This allows transforms to MUTATE the data, rather than repeatedly clone it and generate an updated copy of the property, for significant performance gains:
export const ISODateToDate = (prop: string) => ({ fromServer: data => (data[prop] = data[prop] ? new Date(data[prop]) : data[prop], data), toServer: entity => (entity[prop] = entity[prop] ? entity[prop].toISOString() : entity[prop], entity) });
The implementation of a basic Auto-Entity transform may be as simple and expressive as possible while also being as efficient as possible. Simple reassignment of the property value on the already-cloned server data or entity object! A VIABLE mutative context! This context is also wholly encapsulated within Auto-Entity, ensuring that no code that might suffer consequences from mutating data is ever able to get the data until the mutative context has closed.
Libraries and Tools for Easier Immutability
Immutable data types are often first-class in true functional languages. Clojure, in particular, has internal data types that are designed explicitly to deliver the best balance of performance and effectiveness for most programs using Vectors, Tries, and other structures that can be efficiently searched, and efficiently augmented with shared branches for copy-on-write semantics (where only data that changes is created, and any shared data between a new and previous version is referenced).
There are libraries that provide immutable data types or immutable data management with JavaScript and TypeScript. A more true-to-heart immutable data structure library is Immutable.js. This library provides immutable maps, vectors, etc. that can be used to write functional code that uses truly immutable data structures. It was quite popular a couple years back as of this writing, however, its use has fallen with the introduction of an alternative option.
Immer is a more modern, more javascript-esque library for managing data modifications in an immutable manner. Intriguingly, even a simple spread with JS effectively does copy-on-write semantics. It creates new data for the properties that must be updated and copies everything else. For references, the original references are copied as well, while primitives will usually be copied regardless of whether they are changed or not. Nested spreads can be used to support deeper copy-on-write semantics.
Immer supports the same general semantics, although with a simpler and more familiar “assignment” paradigm. Data is only copied literally on write, and all other data continues to reference the original data structure. Internally Immer uses normal JavaScript data, so it does not require switching to entirely new and unfamiliar (to JavaScript programmers) data structures. It may not be quite as effective in all use cases as Immutable.js, but it is far easier to slip into most codebases where immutability is desired.
Reliable Code
With good programming practices, like those outlined in these key tenets, and with careful management of mutations, writing reliable code that is reliable in its very nature becomes standard. Code that is “safe” from unexpected outcomes.
As we continue to delve into functional programming, we will begin to employ these key tenets of functional programming to build not only reliable code, but very clear, understandable code as well, code that is easier to test, easier to maintain in the long run.