Higher order functions and currying!
In the first two articles 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. We also covered key tenets of implementing functional code such as what purity is, what immutable data is as well as function composition and the concept of point-free function calls.
We will now delve into some of the more abstract concepts with functional programs in general and JavaScript and TypeScript in particular. Notably, higher order functions and the concept of currying. We will also touch on the concept of a “closure” and how they are beneficial and even critical to being able to implement functional code in a purely functional manner.
Higher Order Functions
Ok. We’ve talked about what functions are. We’ve talked about what functional programming is. We’ve covered some of the key tenets of functional implementation. Let’s step into a couple more foundational concepts that expose some of the real, raw functional power of JavaScript and TypeScript as functional languages.
Previously, in the “Descriptive, Explanatory” section of part 1 of this series, I defined some functions that probably looked really strange. The syntax was probably unfamiliar to many JavaScript developers and it requires some explanation. First off, to repeat the previous code for reference purposes:
// ---- identifier --v export const findCustomerNamed = ({ first, last}) => (customers: Customer[]) => customers.find(customer => customer.first === first && customer.last === last; // -------- identifier --v export const attachAddressForCustomer = (address: Address) => (customer: Customer) => customer ? { ...customer, address } : null; // ----------- identifier --v export const findCustomerAndAttachAddress = ({first, last, address}) => pipe( findCustomerNamed({first, last}), attachAddressForCustomer(address) );
These are all “arrow functions” in official JavaScript terms. These are also, in fact, higher order functions or HOF. A HOF is a function that accepts other functions as input and/or returns functions as output. Higher order functions are an extremely powerful concept and tool in the functional programmer’s toolbox!
Currying
A curried function is a higher order function that breaks down its argument list into a series of functions that accept one of the parameters and return functions that accept the rest. If we have the following standard function:
const outer = (firstArg: any, secondArg: any) => ...
The act of “currying” this function converts it into this notation:
// ------------------------------v inner function const outer = (firstArg: any) => (secondArg: any) => ...;
Curried functions need not be limited to just two parameters. They can in fact “take” as many parameters as you need. They are simply functions that return functions and can be decomposed into any number of nested HOFs returning another function:
const pluck = (paths: ...string[]) => // outer (defaults?: ...any[]) => // inner level 1 (options?: { ... }) => // inner level 2 (obj: any) => // inner level 3 ...; // implementation
Each arrow =>
here denotes the end of a function signature and the beginning of its implementation. Functions that return functions can very easily be chained with arrows to create “multi-parameter” curried functions. We simply break each parameter into a more deeply nested function returned by a higher level function.
Currying with Standard Functions
If we transform this into alternative, and perhaps more familiar, terms the first version of outer
above might be declared as such:
function outer(firstArg: any, secondArg: any) { // Do something with the arguments, maybe return a value }
This is a standard javascript function. We can implement a curried version of this, also using standard functions, if we so desired. The second version might be declared as follows:
function outer(firstArg: any) { return function(secondArg: any) { // Do something with the arguments, maybe return a value } }
So, why not just do this? It’s clearer, right? Possibly. Maybe at first. As you first get into functional programming and are relying on your existing knowledge of the language. However, more words are not always clearer. More words will usually result in a higher cognitive load when reading and interpreting code. The more verbose approach when combined with an actual implementation could well become harder to read, interpret and understand in the long run. Especially once you have a more thorough grasp of arrow functions, expressions/lambda expressions, and higher order functions.
There is a certain value in the brevity of the arrow function versions. It is far fewer characters to type and, as you delve deeper into functional programming, you will find you write LOTS of functions. Far more functions than you write now. Possibly more functions than you may have ever thought possible. Arrow functions also have preferable semantics with regards to this
context.
Functional programming, especially when the developer is strongly disciplined and follows key guidelines of quality code, will NECESSARILY have a greater surface area than similar imperative/procedural code counterparts. It may even be that object-oriented code has a smaller surface area. When you go “full-on functional” you will find ways of breaking functions, even ones that seem fairly simple, into even smaller functions…and possibly break even those functions down into further simpler ones! There are many benefits to this down the line…from the sheer simplicity of each function, the greater composability of the little units of your application, the highly expressive nature of each function and compositions of many functions, code that often reads forward as a clear and concise story of what is actually happening, without even requiring the developer to interpret any real expressive or instructional code. Ultra simple, small functions are extremely easy to test, and the time it takes to test such functions drops to minimums…potentially allowing great gains in code coverage for far less cost than the time and effort often required to unit test imperative and object-oriented code.
The arrow function notation also allows functions to be defined more akin to other functional languages and are more expressive in nature. Expressiveness is another key tenet of functional programming: when you aren’t writing functions you’ll probably be writing an expression. This normalization across the two helps lighten the cognitive load as well: even function declarations become expressions!
Partial Application
One of the benefits of curried functions is that they introduce the possibility of partial application into languages that otherwise don’t have first-class support for it.
Why Curry or use HOFs?
Previously, in this article, I’ve talked about how writing quality functional code can lead to more expressive, descriptive, and self-explanatory code. Sometimes there are use cases where a standard non-curried function where all arguments are passed in explicitly every time are less expressive, require more verbosity and in turn, can lead to less efficiency and greater hurdles to interpreting and understanding code.
Not every function must be curried. The use case often determines when a function should be curried. If you ultimately rely on a third-party library to assist in the implementation of functional code, such as Ramda, currying may even be an automatic, dynamic aspect of every function where parameters are curried on demand, as needed (this can be a pro and a con! As a beginner, I recommend starting simple. Only add Ramda once you’ve reached a level where you fully grasp the nature of dynamic currying).
Many functions may only take a single input. When using a pipe
a single primary input for pipeable functions is often a necessity but currying can provide specific benefits for certain use cases with functional pipelines.
Iterative Callback Handlers
One of the reasons why some functions may need to be curried is when they are handlers for the functions of iterative processes. These will notably be Array
prototype methods. Third-party libraries may also operate on arrays or produce arrays and benefit from or even require curried functions as input. Much of RxJs and its operators on Observable streams, in the Observable pipe, are in fact curried functions!
When a method of an Array
, such as filter()
or map()
must be handled, normally it would be done with an anonymous method implemented inline:
export const filterCustomersWithOrders = (orders: Order[]) => (customers: Customer[]): Customer => customers.filter(customer => orders.some(order => order.customerId === customer.id ) );
Note the call to customers.filter()
here and how an arrow function is supplied directly to the filter call as a parameter. This indicates that filter()
is a higher order function! It implies an opportunity to decompose this function further.
Basic Function Decomposition
Taking a larger function and breaking it down into its constituent parts for each logical stage or step (say filtration or mapping, outer vs. inner scopes, etc.) is called decomposition. This can provide value on many fronts. From simpler functions overall to significantly greater code reuse down the road.
export const ifHasOrdersIn = (orders: Order[]) => (customer: Customer): boolean => orders.some(order => order.customerId === customer.id); export const filterCustomersWithOrders = (allOrders: Order[]) => (customers: Customer[]): CustomerWithOrders[] => customers.filter(ifHasOrdersIn(allOrders));
Here we have extracted the anonymous function previously passed to customers.filter()
and created a new function to handle that operation on each individual customer. Note the distinction between filterCustomersWithOrders
and ifCustomerHasOrders
. Also, note the improved clarity for the code:
customers, filter if has orders in all orders
There is something else to call out with how this function has been implemented. It is a curried function. however, the customer is passed to the function returned by the outermost function which accepts orders as input. Why is this? Why does the outer function accept orders, rather than customer? It would seem that customer should be provided “first”, right?
Order Reversal when Currying
This has to do with the nature of a function that returns a function and what the function we are calling expects as input. customers.filter()
expects a function that accepts a single customer as input and that returns a boolean result. If we defined our ifHasOrdersIn
function with customers passed to the outer function:
export const ifHasOrdersIn = (customer: Customer) => (orders: Order[]): boolean => orders.some(order => order.customerId === customer.id);
The implementation actually remains the same but we now have no way of properly passing this curried function to customers.filter()
:
customers.filter(ifHasOrdersIn(/* oops...I need a customer here! Where do I get it?? */)(orders))
We have no customer to pass into ifHasOrdersIn
as it comes from the filter function on the array itself, and even if we somehow did, we end up having to call the inner function to pass in orders and the inner function returns a boolean instead of the function that customers.filter()
expects as input!
By reversing the order of the parameters in the curried function ifHasOrdersIn
we are able to set up the inner function so that it “closes around” the parameters of the outer function.
This allows our outer function to return a function that has the signature that customers.filter()
expects: (customer: Customer) => boolean
. It also ensures that we are able to supply the right array of orders within the context of each call to customers.filter()
so we can perform the necessary functionality.
“Configuring” Functions
With this basic form of currying, we are able to set up functions that allow the configuration of an inner function. This is, in effect, what our ifHasOrdersIn
curried function is doing. The outer function, in essence, “configures” the inner function’s context. It allows additional data to be provided that cannot be provided directly by the Array.prototype.filter()
function (which will only supply the item being filtered).
When decomposing complex functions into simpler functions (for functions to be used within array methods, or functions to be used within functional pipelines) any function that needs context setup can add an outer function call to set up a context inner function calls.
Closures
What does it mean for something to “close around” something else in JavaScript? This means the inner function can access the parameters (and even variables, if you were writing imperative code) supplied to or by the outer function and that the specific values passed to those parameters are a fixed and preserved context for each call to the inner function(s). This is true as long as the instance(s) of the inner function(s) exists and can be called.
This is true even if the outer function is called again with different parameters: subsequent calls to the outer function DO NOT disrupt previous instances of the inner function! However, they do affect subsequent instances of the inner function provided by those subsequent calls to the outer function. Each context created by a call to an outer level of curried functions is unique and isolated for the inner functions returned by those calls. For curried functions, with many nested levels of inner function calls, each call to a deeper function creates another nested context for the next level of inner function(s).
Closure contexts will eventually be cleaned up by the runtime garbage collector once all references to the inner functions have been released. This cleanup will remove both the function instances and any contexts they have closed around.
Further Decomposition
We can, in fact, decompose these functions even further! There is another anonymous function passed to the orders.some()
call:
export const isForCustomer = (customer: Customer) => (orders: Order): boolean => order.customerId === customer.id; export const ifHasOrdersIn = (orders: Order[]) => (currentCustomer: Customer): boolean => orders.some(isForCustomer(currentCustomer)); export const filterCustomersWithOrders = (knownOrders: Order[]) => (customers: Customer[]): CustomerWithOrders[] => customers.filter(ifHasOrdersIn(knownOrders));
Again, note the improved clarity of the code. Note the small changes to parameter names to improve readability and to achieve the goal of “code that tells a story.” Also, note how each function serves one single responsibility! This code fully conforms to SRP or the Single Responsibility Principle! Each function does one single thing, does that thing well, and does nothing more.
orders, some is for customer currentCustomer
order's customerId is equivalent to the customer's id
Expressions. Simple functions. Composition.
We can actually make some of these functions more general purpose or more “atomic” by tweaking some of the input types (aspect of the function signatures, from which TypeScript may perform type checking). A key generalization when using TypeScript is to replace strict concrete types, such as Customer
, with more general types that are more forgiving in what kinds of values they allow to be passed in:
export interface IHasCustomerId { customerId: number; } export const isForCustomer = (customer: Customer) => <T extends IHasCustomerId>(mightHave: T): boolean => mightHave.customerId === customer.id; export const ifHasMatchingCustomer = <T extends IHasCustomerId>(mightHaves: T[]) => (currentCustomer: Customer): boolean => mightHaves.some(isForCustomer(currentCustomer));
We now have some reusable “atomic” and “molecular” building blocks! We can easily reuse isForCustomer
anywhere we need to check if anything that has a customerId
property that matches a specified customer
. We have renamed ifHasOrdersIn
to ifHasMatchingCustomer
. The latter might not tell quite as specific a story but it will still tell just as clear a story as the previous function when passed to customers.filter()
.
We call such an augmentation of a function a “promotion”. The function is made available to a greater number of consumers, or dependents, and provides more generalized services and functionality. As a general best practice start with more specific implementations, such as the ifHasOrdersIn
, and only promote such a specific implementation to a more generalized implementation if and when necessary!
Avoid premature generalization (much the same way it is a good practice to avoid premature optimization). If you do promote a function “early”, or even write it in a more generalized form upfront, you may find that you end up writing more code than you need in the long run; you may eventually determine that you only have a single use case when all is said and done. You then end up implementing little bits of helper code such as IHasCustomerId
needlessly, consuming keystrokes and thus time.
Functional for the Win!
One of the great benefits of purely functional code is its potential for self-explanatory, story-like, clear, and human-readable code. Note that this is a potential. It is not realized automatically. It requires strong discipline to develop a mode of writing code where the developer automatically decomposes complex problems into their smallest atomic parts then recomposes those parts to create a more complex whole.
To formulate an analogy of a highly functional codebase. You could think of it as “atomic” and “molecular” or even “biological” programming. If you think about the nature of microbiology…we are made up of the same general kind of thing. Atomic parts with generalized purposes, composed into larger molecules further composed into complex proteins that perform functions of greater, often higher order complexity. Nano-technology!
Our own code can be implemented much the same way. Atomic parts composed into larger parts, further composed into even larger parts, eventually attached to some element of a user interface allowing a user to interact with said code. The potential for reuse can be extreme. The simplification of code can be significant. The reduction in effort for many much-loathed development tasks such as unit testing can be game-changing.