Skip to content
Advertisement

Higher-order function with recursion in Javascript

newbie here… I’m trying to grasp the concept of functional programming in Javascript, but I got stuck.

I’m trying to apply a function to another function with recursion (higher-order function). Let’s say I have an input that can be a variable or an array, for example:

const A = [5, 14, 23, 32, 41];
const B = 50;

My basic function should convert Fahrenheit to Celsius (but it could really be any function)

const convertF2C = x => (x - 32) / 1.8;

So the way I would normally solve it would be:

const result = array => array.map ? array.map(result) : convertF2C(array); // using recursion if the input is an array

The problem with the above is that if I would like to change the convertF2C in the “result” function, I would have to modify the code

So, thinking functionally I should be able to create a general function that takes a basic function, like this:

const arrayResult = apply2Array(convertF2C);

console.log(arrayResult(A)); // Output: [-15, -10, -5, 0, 5]
console.log(arrayResult(B)); // Output: 10

Where i’m guessing that the general function “apply2Array”, should look something along the lines of:

const apply2Array = fn => (...args) => args.map ? args.map(apply2Array) : fn(...args); // does not work

I found a “sort of” similar question here, but it did not help me : Higher-order function of recursive functions?

Any guidance, help or pointing me in the right direction would be much appreciated.

Advertisement

Answer

I’m a bit confused by the answers here. I can’t tell if they are responding to requirements I don’t actually see, or if I’m missing something important.

But if you just want a decorator that converts a function on a scalar to one that operates on either a scalar or an array of scalars, it’s quite simple, and you weren’t far off. This should do it:

const apply2Array = (fn) => (arg) => 
  Array .isArray (arg) ? arg .map (fn) : fn (arg)

const convertF2C = (t) => (t - 32) / 1.8

const A = [5, 14, 23, 32, 41]
const B = 50

const arrayResult = apply2Array(convertF2C);

console .log (arrayResult (A))
console .log (arrayResult (B))
.as-console-wrapper {max-height: 100% !important; top: 0}

I would suggest that you should use Array.isArray for the check and not the existence of a map property. A property named map might be something other than Array.prototype.map, perhaps something to do with cartography.

Other comments and answers suggest you also want to work the same on nested arrays, to convert something like [5, [[14, 23], 32], 41] into [-15, [[-10, -5], 0], 5]. That wouldn’t be much harder. All you need to do, as Bergi suggest, is to wrap the recursively applied function in the same decorator:

const apply2Array = (fn) => (arg) => 
  Array .isArray (arg) ? arg .map (apply2Array (fn)) : fn (arg)
  //                               ^^^^^^^^^^^
const convertF2C = (t) => (t - 32) / 1.8

const A = [5, 14, 23, 32, 41]
const B = 50
const C = [5, [[14, 23], 32], 41]

const arrayResult = apply2Array(convertF2C);

console .log (arrayResult (A))
console .log (arrayResult (B))
console .log (arrayResult (C))
.as-console-wrapper {max-height: 100% !important; top: 0}

Don’t do this

Still, I would suggest that this enterprise if fraught with potential pitfalls. Imagine, for instance, you had a sum function that operated on an array of number, and you want to use it to operate on either an array of numbers or on an array of arrays of numbers.

If you wrapped it up with either version of apply2Array, it wouldn’t work properly. With the first version, the function will work as expected if you supply an array of arrays of numbers, but will fail if you simply supply an array of number. The second one will fail either way.

The trouble is that sometimes your basic function wants to operate on an array. Making a function that does multiple things based on the types of its inputs loses you some simplicity.

Instead, I would suggest that you create multiple functions to do the different things that you need. You can still use a decorator, but a more general one than the above.

Here we use one called map, which reifies Array.prototype.map:

const map = (fn) => (xs) => 
  xs .map (x => fn (x))

const convertF2C = (t) => (t - 32) / 1.8
const convertAllF2C = map (convertF2C)

const A = [5, 14, 23, 32, 41]
const B = 50

console .log (convertAllF2C (A))
console .log (convertF2C (B))
.as-console-wrapper {max-height: 100% !important; top: 0}

And if you also wanted deep mapping, you might rename the decorator above, and do this:

const map = (fn) => (xs) => 
  xs .map (x => fn(x))
const deepMap = (fn) => (arg) => 
  Array .isArray (arg) ? arg .map (deepMap (fn)) : fn (arg)

const convertF2C = (t) => (t - 32) / 1.8
const convertAllF2C = map (convertF2C)
const deepConvertF2C = deepMap (convertF2C)

const A = [5, 14, 23, 32, 41]
const B = 50
const C = [5, [[14, 23], 32], 41]

const arrayResult = deepMap (convertF2C);

console .log (convertAllF2C (A))
console .log (convertF2C (B))
console .log (deepConvertF2C (C))
.as-console-wrapper {max-height: 100% !important; top: 0}

Having three separate functions to call for your three cases is generally simpler than one function that can be called with three different styles of input associated with three different styles of output. And as these are built from our base function with only some generic decorators, they are still easy to maintain.

But doesn’t that contradict…?

Some people know me as a founder and primary author of Ramda. And Ramda has a map function related to this. But it seems to operate on multiple types, including arrays, objects, functions, and more. Isn’t this a contradiction?

I’d say no. We just need to move up a layer of abstraction. FantasyLand specifies an abstract generic type, Functor (borrowed from abstract mathematics). These are types which in some way contain one or more values of another type, and to which we can create a similarly-structured container by mapping the function supplied to each of those values. There are certain simple laws that your map function must obey for it to be considered a Functor, but if you do, then Ramda’s map will work just fine with your type. In other words, Ramda’s map does not work on Arrays specifically, but on any Functor. Ramda itself supplies implementations for Arrays, Objects, and Functions, but delegates the call to other types to their own map methods.

The basic point, though, is that Ramda does not really impose additional complexity here, because the input type of Ramda’s map is Functor instead of Array.

Simplicity

Functional programming is about many things. But one of the central topics has to be simplicity. If you haven’t seen Rich Hickey’s talk Simple Made Easy, I would highly recommend it. It explains an objective notion of simplicity and describes how you might achieve it.

Advertisement