JavaScript / JXA Q&A

  1. No danger for you, just squeamish about recommending anyone to experiment at all with (or even be conscious of the whereabouts of) a file which contains so much work and resource :slight_smile:
  2. Reduce is JavaScript's version of fold or foldl / foldLeft, which is just a very general relationship between a list and any single value which can be derived from it. See more below.
  3. The same function is used in two places there, one inside the reduce and one outside, so to minimize maintenance I just bound a name to it in one place.

The short version of fold (reduce) was recently tweeted as:

A fuller version, worth skimming, I think, because fold/reduce is so universal and powerful, is at:

And here are some simple illustrative folds (most of them left folds, one of them a right fold (reduceRight in JS). A surprising number of things, even map and filter, can be written as a fold. Good instrument to have in the toolbag.

(function () {
    'use strict';

    // sum :: [Number] -> Number
    function sum(lstNumbers) {
        return lstNumbers
            .reduce(
                function (accumulator, x) {
                    return accumulator + x;
                },
                0 // initial value of accumulator
            )
    }

    // length :: [a] -> Integer
    function length(ListOfAnything) {
        return ListOfAnything
            .reduce(
                function (accumulator, x) {
                    return accumulator + 1;
                },
                0 // initial value of accumulator
            )
    }

    // filtered :: (a -> Bool) -> [a] -> [a]
    function filtered(f, lst) {
        return lst
            .reduce(
                function (accumulator, x) {
                    return (
                        f(x) && accumulator.push(x),
                        accumulator
                    )
                }, [] // initial value of accumulator is empty list
            )
    }

    // reversed :: [a] -> [a]
    function reversed(lst) {
        return lst
            .reduceRight(
                function (accumulator, x) {
                    return (
                        accumulator.push(x),
                        accumulator
                    );
                }, [] // initial value of accumulator is empty list
            )
    }

    function isEven(n) {
        return n % 2 === 0
    }

    return {
        sum: sum([1, 2, 3, 4, 5, 6, 7, 8, 9]),

        length: length([1, 2, 3, 4, 5, 6, 7, 8, 9]),

        filtered: filtered(isEven, [1, 2, 3, 4, 5, 6, 7, 8, 9]),

        reversed: reversed([1, 2, 3, 4, 5, 6, 7, 8, 9])
    }

})();
1 Like

Heh, you have a higher regard for my level of intelligence than is warranted. That paper speaks in a language with which I'm not familiar. But thanks for thinking so highly of me! :slight_smile:

Ah, I get it (your examples). I was skipping over the fact you were passing an empty array, initially. I saw the comment but it didn't stick.

For you examples, there are alternatives, aren't there? Things like "filter" and the like, right? You were just demonstrating the method, right?

See, in C#, I'm used to extension methods like Aggregate, Where, First, Max, etc.

So I don't really even think much about how they're accomplished. For these methods, I just need to return whatever I want accumulated, or true/false, etc, and I don't even think about how it's done on the back-end. I mean, obviously I know what's happening, but I don't need to even consider it.

It's good to have something for me to compare with.

Sure, there are pre-cooked alternatives of those examples in most languages including JS, and fold/reduce itself can be implemented in various ways, recursively or iteratively.

The great thing about it is that its a well-tested and very general black box, which can protect you from having to mess with the details (declaring and incrementing iterators) and get things wrong (out of bounds errors etc), while tightening up scope, in a surprising range of different contexts.

1 Like

Donā€™t get me wrong - I think itā€™s great! For me, I need to be able to say ā€œthat thing is kind of like this thingā€. So knowing what this is similar to, for instance I know itā€™s a hammer, and I know how to use a hammer, even if the handle on this one is different, Iā€™m good.

But if someone tries to explain it to me, saying itā€™s a device that operates on the principal of inertia, mass and acceleration (or whatever), then Iā€™m lost.

So thanks!

1 Like

Rob, many thanks for your tutorial on fold/reduce. Very interesting.

Since you seem to be in an educational mood, I wonder if you could help neophytes like me better understand some of the symbols I've seen you use in your scripts?

From the fold article:

I've often seen you use this notation, but I don't understand it.
For example:
// writePlist :: Object -> String -> IO ()

I did do some reasearch, and found this, but it does not include the double colon ::
Mathematical Symbols

So, if you have the time and interest, an explanation of (or a reference for) the above symbols used in the fold statement would be great!

Thanks.

The type signature

map :: (a -> b) -> [a] -> [b]

would be a brief way of announcing the intention that a function named map

had, as its first argument, another function with the type signature (a -> b)

(a function (a -> b) is taking an argument of any type, and returning a value which may be of a different type)

and as its second argument: an array of values of type a.

Finally it tells us that the output of this function is intended to be an array of values of type b.

(In other words, of any consistent type, which doesn't have to be the same as type a, but will be the same as the output of the (a -> b) function.

JavaScript is not a strongly typed language, but it can still be helpful to use type signatures to communicate clarity about the intended type behaviour of a function.

See for example, the entries in this JS library:

http://ramdajs.com/docs/#chain


If you work with Haskell, in which the compiler can actually make use of the optional type signature, there is a very useful search engine (Hoogle) which allows you to find functions by their type signature rather than by their name.

There is, in fact, a sense in which the type signature is more eloquent and fundamental than the name.

https://www.haskell.org/hoogle/

1 Like

Rob, merged per your request. Please edit as needed.

1 Like

Got another question. Consider this code:

function getKMMacroAndGroupByMacroUUID(macroUUID, plist) {
    plist.forEach(function(group) {
        var macros = group.macros;
        if (macros) {
            var macro = macros.find(function(m) {
                return m.uid === macroUUID;
            });
            if (macro) {
                return {
                    macro: macro,
                    group: group
                };
            }
        }
    });
    return undefined;
}

Obviously this doesnā€™t work. Iā€™m looking through an array of Groups that may have an array of Macros. I want to find a Macro with a particular UUID (UID), and if I find it, return the Macro and its Group.

I wasnā€™t really thinking, and I tried to do it with a ā€œreturnā€ in the anonymous function that the ā€œforEachā€ calls, and of course that doesnā€™t work.

I can get something to work, of course, and I will, but I thought this was probably the perfect time to learn one of those fancy-sounding functions. :slight_smile:

This is what I came up with:

function getKMMacroAndGroupByMacroUUID(macroUUID, plist) {
    var _result = undefined;
    plist.find(function(group) {
        var macros = group.macros;
        if (!macros) {
            return false;
        }

        var macro = macros.find(function(m) {
            return m.uid === macroUUID;
        });
        if (macro) {
            _result = { macro: macro, group: group };
            return true;
        }
        return false;
    });
    return _result;
}

Using ā€œfindā€ allows me to exit the iteration loops immediately if I get a hit. Using a result variable in the outer scope allows the result to be accessible after the loops exit.

Is there a better way? This doesnā€™t smell too awful.

Looks good.

The only thing that jumps to the eye is case sensitivity in JS names, so for example, I think you may need:

  • group.Macros
  • m.UID

Iā€™m very much in favour of built-in array methods like find.

If you ever wanted to do it more iteratively, the while(i--) idiom is typically the fastest with JS compilers, so one other way might be something like:

(function () {
    'use strict';


    function macroUUIDFound(lstGroups, strUID) {
        var i = lstGroups.length,
            dctGroup,
            lstMacros;

        while (i--) {
            dctGroup = lstGroups[i];
            lstMacros = dctGroup.Macros;

            if (lstMacros && lstMacros
                .find(function (m) {
                    return strUID === m.UID;
                })) {
                return true;
            }
        }
        return false;
    }


    var lstSeln = Application("Keyboard Maestro")
        .selectedmacros(),
        strUID = lstSeln.length ? lstSeln[0] : undefined;


    var lstMacroGroups = strUID ? ObjC.deepUnwrap(
            $.NSDictionary.dictionaryWithContentsOfFile(
                $(
                    '~/Library/Application Support/Keyboard Maestro/Keyboard Maestro Macros.plist'
                )
                .stringByStandardizingPath.js
            )
        )
        .MacroGroups : undefined;



    return strUID ? macroUUIDFound(lstMacroGroups, strUID) : "No macro selected"
})();

You would think that, wouldn't you? That's what I thought also. Except in this case, they're lower case. I suspect this is a mistake on Peter's part, when he returns the result of "getmacros". But let's not tell him, OK? :slight_smile:

But I'm assuming, since you said you're a fan of built-in methods, that you normally wouldn't do that unless speed was an issue, right? 'Cause to me, that feels like going back to the dark ages. :open_mouth: :stuck_out_tongue:

Except in this case, they're lower case.

That's interesting ... I wonder if we are working with different builds ?

(On this system it fails with lower case, and shows Upper in the debugger ... )

that feels like going back to the dark ages.

Absolutely agree. Performance seldom seems a genuine or even perceptible issue in Mac scripting ā€“ scripter time is certainly real.

Here's what I'm using (I'm editing this slightly manually, so if it doesn't compile, fix it. :slight_smile:)

var _kme = Application("Keyboard Maestro Engine");
var macros = kme.getmacros({
    asstring: true
});
console.log(macros);

I wonder if the issue is that the key strings can get written out (or have historically been written out) to the .plist in varying cases ?

If that were ā€˜the caseā€™ it might need a double test ā€¦

Heh.

All other instance I've seen with KM plists, to my recollection, have been upper case.

Or perhaps we are reading the plist into JavaScript objects in different ways ?

I am getting 'Macros' and 'UID'

from:

$.NSDictionary.dictionaryWithContentsOfFile

We are doing it differently. Look again. Iā€™m calling Peterā€™s KME method ā€œgetmacrosā€. Iā€™m not reading from a file.

Hereā€™s a more complete example:

(function Run() {
    'use strict';

    function convertArrayToPlist(array) {
        return ObjC.deepUnwrap(
            $.NSPropertyListSerialization.propertyListWithDataOptionsFormatError(
                $(array).dataUsingEncoding($.NSUTF8StringEncoding), 0, 0, null));
    };

    function getAllKMGroupsAndMacrosArray(kme) {
        var _kme = kme || Application("Keyboard Maestro Engine");
        var _macros = _kme.getmacros({
            asstring: true
        });
        console.log(_macros);
        return convertArrayToPlist(_macros);
    }

    var plist = getAllKMGroupsAndMacrosArray();
    // ...
})()

Worth knowing ...

Perhaps a pity that they differ ?

(Possibly also worth enabling functions to behave sensibly with either parse ?)

(Looking at your original post ā€“ I think the source of the plist parse was left open)

@peternlewis - It looks like the data you return from KMEā€™s ā€œgetmacrosā€ uses lower-case keys ā€œnameā€ and ā€œuidā€ (and probably others), whereas the rest of the time, theyā€™re ā€œNameā€ and ā€œUIDā€.

Was this intentional?

Speaking of case, you probably know this but 'just in case':

A top level function named run (lower case) has a special status in osascript processing ā€“ If it's found, it's treated as the Main().

function run() {
}

To create local variables, avoiding pollution of the global namespace, and making it easier to find your own variables in the debugger, you clearly need to enclose your own name bindings in something, and just using a run() function is a perfectly good way of doing that.

My personal habit is usually to use just an anonymous

(function() {
'use strict';

})();

mainly because it gives me more flexibility in argument handling. (run() gets a special kind of argument in some script launch contexts)

(I often use the arguments of the anonymous module function as a place to hold script options)

But perhaps your capitalized Run is a preferred shorthand for indicating the top level of the module ?