JavaScript actions – more general map and fold/reduce functions

The crown jewels of JavaScript for Automation (JS generally, in fact) are the ‘higher-order’ Array functions, particularly Array.map() and Array.reduce()

Map returns a transformed version of an array (applying a supplied function to each of its members)

Reduce (often called fold or foldl, foldLeft etc in other languages) takes:

  • an array, and
  • an initial seed or ‘accumulator’ value,

and works through each value in the Array, using a supplied function to update the accumulator value, and then returns a final value.

A surprising number of problems can be solved by working through arrays of values with map and reduce/fold, but they do have one limitation - they only work with simple flat arrays. They don’t work with nested arrays, dictionaries of key:value pairs, or combinations of these two.

Here, for the snippet library of anyone using Execute JavaScript for Automation actions, or Execute JavaScript in (Safari|Chrome) actions, are slightly more general versions of map and reduce: fmap and fold

fmap(Function, JSValue) can produce exactly the same result as Array.map(), if its second argument is a flat Array, but it can also map a single argument function over values in arbitrarily nested Arrays and/or Key:Value objects.

fold(Function, StartValue, JSValue) can produce exactly the same result as Array.reduce(), if its second argument is a flat Array, but it can also progressively update an accumulator by working a two argument function (accumulator, value) through all the values in arbitrarily nested Arrays and/or Key:Value objects.

Code with examples of use:

(function () {
    'use strict';


    //  A MORE GENERAL map -- NOT JUST FOR FLAT ARRAYS

    // fmap :: Function -> JavaScript Object or Atom -> transformed ditto
    // fmap :: (a -> b) -> f a -> f b
    function fmap(f, jso) {
        var t = typeof jso;

        if ("undefined" !== t) {
            if ("object" === t) {
                if (null === jso) return null;

                // Array
                if (jso instanceof Array) {
                    return jso.map(function (v) {
                        return fmap(f, v);
                    });
                }

                // Dictionary
                var fb = {};
                return Object.keys(jso)
                    .forEach(function (k) {
                        return fb[k] = fmap(f, jso[k]);
                    }), fb;

                // Atomic value
            } else return f(jso);
        }
    }



    //  A MORE GENERAL fold / reduce -- NOT JUST FOR FLAT ARRAYS

    // fold :: Function -> Start Value ->JavaScript Object or Atom -> transformed ditto
    // fold :: (a -> b -> a) -> a -> c -> a
    function fold(f, v, jso) {
        var t = typeof jso;

        if ("undefined" !== t) {
            if ("object" === t) {
                if (null === jso) return v;


                // Array or dictionary
                var lst = jso instanceof Array ? (
                        jso
                    ) : Object.keys(jso)
                    .map(function (k) {
                        return jso[k];
                    }),
                    a = v;

                return (
                    lst.forEach(function (x) {
                        a = fold(f, a, x);
                    }),
                    a
                );

                // Atomic: String | Number | Bool
            } else return f(v, jso);
        }
        return v;
    }


    /******* EXAMPLES ******/

    var doubleNumber = function (x) {
        return typeof x === 'number' ? x * 2 : x;
    }

    var sumNumbers = function (a, b) {
        return (!isNaN(a) && !isNaN(b)) ? a + b : a;
    }


    var fmapExamples = {
            fmapSimpleArray: fmap(doubleNumber, [1, 2, 3, 4, 5]),
            
            fmapNestedArray: fmap(
                doubleNumber, [1, 2, 3, [4, 5, 6], [], [[7, 8, 9]]]
            ),
            
            fmapDictionary: fmap(doubleNumber, {
                alpha: 10,
                beta: 20,
                gamma: 30
            }),
            
            fmapMixed: fmap(doubleNumber, [{
                    alpha: 40,
                    beta: 50
                }, {
                    gamma: 60,
                    delta: [64, 128]
                }])
        },

        foldExamples = {
            foldSimpleArray: fold(sumNumbers, 0, [1, 2, 3, 4, 5]),
            
            foldNestedArray: fold(
                sumNumbers,
                0, [1, 2, 3, [4, 5, 6], [], [[7, 8, 9]]]
            ),
            
            foldDictionary: fold(sumNumbers, 0, {
                alpha: 10,
                beta: 20,
                gamma: 30
            }),
            
            foldMixed: fold(sumNumbers, 0, [{
                    alpha: 40,
                    beta: 50
                }, {
                    gamma: 60,
                    delta: [64, 128]
                }])
        }


    return [fmapExamples, foldExamples];
})();
2 Likes