Image courtesy 123rf.com

Higher-Order Functions

Jim Armstrong
ngconf
Published in
9 min readMay 14, 2021

--

Turn one function into many

Introduction

High-Order functions are simply functions that accept functions as arguments and/or return functions as a result. I have used higher-order functions since the 1990s in C++ along with the Standard Template Library. That practice continued with ActionScript development in Flash/Flex, and more recently in TypeScript.

Since JavaScript allows functions to be created dynamically at runtime, there is almost no practical limit on the number of higher-order functions that can be created from a simple base set of functions.

This article covers eight higher-order functions that I use regularly in applications, particularly in situations where application behavior is data-driven. You can literally parlay these eight functions into hundreds of useful utilities.

Repository

You are welcome to copy and paste code directly from this article, or you might prefer to fork this GitHub,

I should note that to make the code more compact, I’ve taken some liberty with JavaScript coercions (to truthy/falsy) and compressed some code into a single line in lieu of indentation. I’ve also been a bit terse with some variable names, again to compact space.

Deconstructing the code requires a fairly strong background in JavaScript and TypeScript principles such as generics.

Examples

Following is a list of each function’s source code and at least one example usage. For cases where a function is imperatively created, some care must be taken to ensure the integrity of the source string as it is possible to inject malicious script.

Two types are used throughout the source for each function,

export type PredicateFunction<T> = (x: T) => boolean;

export type XformFunction<T> = (x: T) => T;

DYNAMIC FILTER

This function filters an input array based on a runtime-supplied predicate whose function body is supplied as a string argument.

/**
* Dynamically filter an array based on a function whose body is passed as an argument and return result in a new array
*
* @param predicateBody Body of a function that takes a single argument, 'x', and returns a boolean
*
* @param inputs Apply the predicate to this collection of input values
*/
export function dynamicFilter<T>(predicateBody: string, inputs: Array<T>): Array<T>
{
if (!predicateBody || predicateBody === '' || predicateBody === ' ') return [];
if (!inputs) return [];

const result: Array<T> = new Array<T>();
const f: (x: T) => boolean = new Function('x', predicateBody) as (x: T) => boolean;

inputs.forEach( (x: T): void => {if (f(x) === true) result.push(x)} );

return result;
};

Example: all values greater than zero from the array, [1, 0, -1, 2, -2, 4].

const result: Array<number> = dynamicFilter<number>('return x > 0', [1, 0, -1, 2, -2, 4]);

Result: [1, 2, 4]

REVERSE FILTER

This function filters an array based on the inverse of a supplied predicate.

/**
* Filter an array based on an inverse of a supplied predicate
*
* @param predicate Function that takes a single input value and returns a boolean
*
* @param arr Input array to be filtered; return values are inputs where the predicate returns false
*/
export function reverseFilter<T>(predicate: (value: T) => boolean, arr: Array<T> ): Array<T>
{
if (!arr || !predicate) return [];

const result: Array<T> = new Array<T>();

arr.forEach( (value: T): void => {if (predicate(value) === false) result.push(value)} );

return result;
};

Example: all empty strings or strings of unit length

const f: (x: string) => boolean = (x: string) => x.length > 1;
const g: (x: string) => boolean = (x: string) => x.length > 0 ? x.charAt(0) === 'a' : false;
let result: Array<string> = reverseFilter<string>(f, ['1', 'abc', 'd', 'x', '-2', 'x-link', 'y-link']);

Result: [‘1’, ‘d’, ‘x’]

Example: all empty strings or strings that DO NOT begin with the letter ‘a’

result = reverseFilter<string>(g, ['abc', 'def', '123', 'a1234', 'ax', 'by', 'xyz']);

Result: [‘def’, ‘123’, ‘by’, ‘xyz’]

DYNAMIC TRANSFORM

This function transforms an input array based on a predicate whose function body is supplied as a string argument. The original array is not modified.

/**
* Dynamically transform an array based on a function whose body is provided as an argument and return result in a new array
*
* @param xformFunctionBody Body of a function that takes a single argument, 'x', and returns a boolean
*
* @param inputs Input array to be transformed
*/
export function dynamicXform<T>(xformFunctionBody: string, inputs: Array<T>): Array<T>
{
if (!xformFunctionBody || xformFunctionBody === '' || xformFunctionBody === ' ') return [];
if (!inputs) return [];

const result: Array<T> = new Array<T>();
const f: XformFunction<T> = new Function('x', xformFunctionBody) as XformFunction<T>;

inputs.forEach( (x: T): void => {result.push(f(x))} );

return result;
};

Example: increment each number in an array by 1

let result: Array<number> = dynamicXform<number>('return x + 1', [1, 0, -1, 2, -2, 4]);

Result: [2, 1, 0, 3, -1, 5]

Example: clear an array of numbers (reset to zero)

result = dynamicXform<number>('return 0', [1, 0, -1, 2, -2, 4]);

Result: [0, 0, 0, 0, 0, 0]

As a side note, I often use this function frequently with decision trees. The tree is processed first by level and then by depth until either a level is exhausted (no action) or a leaf is encountered (action). The function body is provided as a data property of a leaf’s node.

EXTENDED REDUCER

This function performs array reductions based on a helper function that is supplied as an argument. This allows different array reductions to be performed by imperatively changing the helper function at runtime. If a new reduction is required (based on a change-request, for example), the original code need not be changed. Only a new helper function is required.

/**
* Reduce an array based on a helper function provided as an argument
*
* @param helper Reducer function
*
* @param initialValue Initial value for the array reduction
*
* @param inputs Input array to be reduced
*/
export function extendedReducer<T>(helper: (value1: T, value2: T) => T, initialValue: T, inputs: Array<T>): T | null
{
if (!helper || !inputs) return null;

if (inputs.length === 0) return null;

const result: Array<T> = new Array<T>();

return inputs.reduce( (acc: T, current: T): T => {
const value: T = helper(acc, current);

result.push(value);

return value;
}, initialValue );
};

Example: find the minimum and maximum values of an array of numbers

const findMin: (min: number, x: number) => number = (min: number, x: number) => x < min ? x : min;const findMax: (max: number, x: number) => number = (max: number, x: number) => x > max ? x : max;

const minValue = extendedReducer<number>(findMin, Number.MAX_VALUE, [4, 8, 15, 26, 23, 42]);
const maxValue = extendedReducer<number>(findMax, Number.MIN_VALUE, [4, 8, 15, 26, 23, 42]);

Result: minValue = 4 and maxValue = 42

HAS ANY

This function tests if an array has any values that match a predicate.

/**
* Does an array contain any values that satisfy a predicate whose function body is provided as an argument?
*
* @param predicateBody Body of a function that takes a single argument, 'x', and returns a boolean
*
* @param inputs Input array to which the predicate is applied
*/
export function hasAny<T>(predicateBody: string, inputs: Array<T>): boolean
{
if (!predicateBody || predicateBody === '' || predicateBody === ' ') return false;

if (!inputs) return false;

const f: PredicateFunction<T> = new Function('x', predicateBody) as PredicateFunction<T>;

return inputs.reduce( (acc: T, x: T): boolean | T => acc || f(x), false ) as boolean;
};

Example: are any numbers exactly equal to zero?

let result: boolean = hasAny<number>('return x === 0', [1, 0, -1, 2, -2, 4]);

Returns true.

Example: are any numbers greater than 10?

result = hasAny<number>('return x > 10', [1, 0, -1, 2, -2, 4]);

Returns false.

HAS NONE

This function is the inverse of ‘has-any’; it tests if an array has no values that satisfy a predicate.

/**
* Is an array completely devoid of any values that satisfy a predicate whose function body is provided as an argument?
*
* @param predicateBody Body of a function that takes a single argument, 'x', and returns a boolean
*
* @param inputs Input array to which the predicate is applied
*/
export function hasNone<T>(predicateBody: string, inputs: Array<T>): boolean
{
if (!predicateBody || predicateBody === '' || predicateBody === ' ') return false;

if (!inputs || inputs.length === 0) return false;

const f: PredicateFunction<T> = new Function('x', predicateBody) as PredicateFunction<T>;

return inputs.reduce( (acc: T, x: T): boolean | T => !acc && !f(x), true ) as boolean;
};

Example: are none of the numbers in the array equal to zero?

let result: boolean = hasNone<number>('return x === 0', [1, 5, -1, 2, -2, 4]);

Returns true.

Example: are none of the numbers in the array equal to zero?

result = hasNone<number>('return x === 0', [1, 0, -1, 2, -2, 4]);

Returns false.

PARTITION

This function partitions an array into two collections based on a predicate. I find this function particularly valuable when I must separate server-supplied data into two groups based on dynamic criteria. The criteria may also determine navigation options supplied to a user as well as a set of Angular Components that govern the data display.

/**
* Partition an array into 'left' and 'right' sections based on a predicate that is evaluated at each array value
*
* @param predicate Function taking a single argument and returning a boolean
*
* @param arr Input array to which the predicate is applied
*/
export function partition<T>(predicate: (value: T) => boolean, arr: Array<T> ): {left: Array<T>, right: Array<T>} | null
{
if (!arr || !predicate || arr.length === 0) return null;

const result: {left: Array<T>, right: Array<T>} = {left: [], right: []};

arr.forEach( (value: T): void => {
if (predicate(value) === true) {
result.left.push(value);
} else {
result.right.push(value);
}
});

return result;
};

Example: separate an array of integers into a list of all even and all odd integers

const result: {left: Array<number>, right: Array<number>} = partition<number>((x: number) => x % 2 === 0, [1, 5, 0, 2, -2, 4, 7, 9]);

Result: left property is [0, 2, -2, 4] and right property is [1, 5, 7, 9]

CACHE

The term memoization is often used to refer to the process of storing interim results in memory and returning those results from a search in lieu of repeated (expensive) computations. Although the term memoization dates back to the 1960s, the term caching was more common until recent times.

This particular implementation requires a utility function that accepts a single array of arguments instead of an arbitrary number of individual arguments. Enforcing this uniformity in function signature provides numerous benefits in data-driven applications.

/**
* Cache returns from a function and return those values in the case of a repeated function call with the same arguments
*
* @param f Function that takes an array of values and returns an arbitrary result
*/
export function cache(f: (...args: Array<any>) => any): (...rest: Array<any>) => any
{
const localCache: Record<string, any> = {};

return (...rest: Array<any>): any => {
const key: string = JSON.stringify(rest);
let result: any = localCache[key];

if (result !== undefined) return result;

result = f(rest);
localCache[key] = result;

return result;
}
};

Example: Euclid’s algorithm (non-recursive)

// Non-recursive Euclid's algorithm
export function GCD(args: Array<number>): number
{
if (!args || args.length != 2) return 0;

const x1: number = args[0];
const x2: number = args[1];

if (isNaN(x1) || !isFinite(x1)) return 0;
if (isNaN(x2) || !isFinite(x2)) return 0;

const n1: number = Math.floor(Math.abs(x1));
const n2: number = Math.floor(Math.abs(x2));

let a = Math.max(n1, n2);
let b = Math.min(n1, n2);
let r = 0;

while (b > 0)
{
r = a % b;
a = b;
b = r;
}

return Math.floor(a);
};
.
.
.
const cachedGCD: (a: number, b: number) => number = cache(GCD);
.
.
.
let result: number = cachedGCD(3, 7); // 1

result = cachedGCD(2, 4); // 2
result = cachedGCD(3, 7); // 1
result = cachedGCD(3, 9); // 3
result = cachedGCD(3, 9); // 3

For the final two results, the GCD of 3 and 9 is computed once and that return value is extracted directly from cache instead of calling the function a second time. The cache higher-order function is most useful when optimizing calls to expensive functions that have a high probability of repeated calls with the same arguments.

I hope you find a number of useful applications for these functions.

Now that you’ve read this article and learned a thing or two (or ten!), let’s kick things up another notch!
Take your skills to a whole new level by joining us in person for the world’s first MAJOR Angular conference in over 2 years! Not only will You be hearing from some of the industry’s foremost experts in Angular (including the Angular team themselves!), but you’ll also get access to:

  • Expert panels and Q&A sessions with the speakers
  • A friendly Hallway Track where you can network with 1,500 of your fellow Angular developers, sponsors, and speakers alike.
  • Hands-on workshops
  • Games, prizes, live entertainment, and be able to engage with them and a party you’ll never forget

We’ll see you there this August 29th-Sept 2nd, 2022. Online-only tickets are available as well.
https://2022.ng-conf.org/

--

--

Jim Armstrong
ngconf
Editor for

Jim Armstrong is an applied mathematician who began his career writing assembly-language math libraries for supercomputers. He now works on FE apps in Angular.