Question on JXA returning multiple properties of a list of objects

@ComplexPoint

Please forgive me for not wording this better - as I get older, I find it harder to be as clear as I'd like to be, and my memory gets foggier and foggier.

Consider this JXA code:

const km = Application("Keyboard Maestro");
const macros = km.macros();

I want to iterate over the macro objects in macros, accessing a few properties for each macro. Let's say I want macro.name() and macro.id().

IIRC, each access of macro.name() and macro.id() is a function call with some amount of overhead, and it seems to me in the back of my mind that there's a way to gather the name() and id() of all the macros in one "call".

Does that make any sense to you? Is there something like this in JXA, or am I mistaken? Thanks.

Hi Dan,

The trick, I think, is to delay the closing () in km.macros(), and just write km.macros

(The first gives a JS Array, and the second a reference to a Collection)

then, batch-fetching arrays of properties values, and zipping them together:

(() => {
    "use strict";

    const main = () => {
        const km = Application("Keyboard Maestro");

        // A reference to a collection, rather than an Array.

        // km.macros vs km.macros()
        const macros = km.macros;

        // Two batch-fetched arrays of property values,
        const ids = macros.id();
        const names = macros.name();

        // zipped together
        const pairs = zip(ids)(names);

        return JSON.stringify(pairs, null, 2);
    };

    // --------------------- GENERIC ---------------------

    // zip :: [a] -> [b] -> [(a, b)]
    const zip = xs =>
        // The paired members of xs and ys, up to
        // the length of the shorter of the two lists.
        ys => Array.from({
            length: Math.min(xs.length, ys.length)
        }, (_, i) => [xs[i], ys[i]]);

    // MAIN ---
    return main();
})();
1 Like

and in practice, a zipWith can be more useful than a zip here:

(() => {
    "use strict";

    const main = () => {
        const km = Application("Keyboard Maestro");

        // km.macros vs km.macros()
        // Collection reference, vs Array
        const macros = km.macros;

        // Zipped together with some custom function
        // applied over each pair.
        return zipWith(
            uuid => title => `${uuid}\t${title}`
        )(
            macros.id()
        )(
            macros.name()
        )
        .join("\n");
    };

    // --------------------- GENERIC ---------------------

    // zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
    const zipWith = f =>
        // A list constructed by zipping with a
        // custom function, rather than with the
        // default tuple constructor.
        xs => ys => xs.map(
            (x, i) => f(x)(ys[i])
        )
        .slice(
            0, Math.min(xs.length, ys.length)
        );

    // MAIN ---
    return main();
})();

and, of course, for arbitrary numbers of batch-fetched property value lists, there are various ways of defining a zipWithN to combine them all.

For example:

(() => {
    "use strict";

    const main = () => {
        const km = Application("Keyboard Maestro");

        // km.macros vs km.macros()
        // Collection reference, vs Array
        const macros = km.macros;

        // Zipped together with some custom function
        // applied over each tuple of property values.
        return zipWithN(
            uuid => size => title => `${uuid}\t${size}\t${title}`,
            macros.id(),
            macros.size(),
            macros.name()
        )
        .join("\n");
    };

    // --------------------- GENERIC ---------------------

    // zipWithN :: (a -> b -> ... -> c) -> ([a], [b] ...) -> [c]
    const zipWithN = (...args) => {
        // Uncurried function of which the first argument is
        // a function, and all remaining arguments are lists.
        const rows = args.slice(1);

        return 0 < rows.length
            ? (() => {
                const
                    n = Math.min(...rows.map(x => x.length)),
                    // Uncurried reduction of zipWith(identity)
                    apZL_ = (fs, ys) => fs.map(
                        (f, i) => (f)(ys[i])
                    )
                    .slice(0, n);

                return rows.slice(1).reduce(
                    apZL_,
                    rows[0].map(args[0])
                );
            })()
            : [];
    };

    // MAIN ---
    return main();
})();

Holy Crap! Your code flies!

I added macro.macroGroup.name() and macro.macroGroup.id() to your last example, and for my 2,079 macros:

old method: 6.083 seconds
new method: 0.458 seconds

Your code returned the exact same results, in literally a fraction of the time my iterative code takes.

It was so fast I had to do a "compare" to verify the results were the same, and of course they match exactly. Sorry I had even a moment of doubt.

image

Thanks for the help! I appreciate that you're always willing to help with these types of things.

1 Like

Wish I could take credit :slight_smile:

( just the way the osascript interfaces are built – those Apple Events are expensive   ¯\_(ツ)_/¯  )

( and thank you for all that very solid and useful tool-building, as well ! )

Teachers should always take credit, even though the things they teach are rarely their own ideas.

What I find interesting about your code is that it's not as incomprehensible to me as it once was. I still have to squint my brain to figure it out, but at least I can figure it out now. Mostly. ;p

That's awesome! I had to laugh - when I put the little guy in a quote, I had to escape his right arm. How geeky is that?

image (that's a fist bump, in case it isn't clear)

1 Like

FWIW I guess we should also be able rearrange slightly to fetch each group uuid and name just once:

Expand disclosure triangle to view JS source
(() => {
    "use strict";

    // Getting each group id and name just once
    // A slight rearrangment

    const main = () => {
        const km = Application("Keyboard Maestro");

        const
            macroGroups = km.macroGroups,
            macros = macroGroups.macros;

        // Zipped together with some custom function
        // applied over each tuple of property values.
        return zipWithN(
            gpUUID => gpName => macroIds => macroSizes => macroNames =>
                zipWithN(
                    uuid => size => name =>
                        `${gpUUID}\t${gpName}\t${uuid}\t${size}\t${name}`,
                    macroIds,
                    macroSizes,
                    macroNames
                )
                .join("\n"),
            macroGroups.id(),
            macroGroups.name(),
            macros.id(),
            macros.size(),
            macros.name()
        )
        .join("\n");
    };

    // --------------------- GENERIC ---------------------

    // zipWithN :: (a -> b -> ... -> c) -> ([a], [b] ...) -> [c]
    const zipWithN = (...args) => {
        // Uncurried function of which the first argument is
        // a function, and all remaining arguments are lists.
        const rows = args.slice(1);

        return 0 < rows.length
            ? (() => {
                const
                    n = Math.min(...rows.map(x => x.length)),
                    // Uncurried reduction of zipWith(identity)
                    apZL_ = (fs, ys) => fs.map(
                        (f, i) => (f)(ys[i])
                    )
                    .slice(0, n);

                return rows.slice(1).reduce(
                    apZL_,
                    rows[0].map(args[0])
                );
            })()
            : [];
    };

    // MAIN ---
    return main();
})();
2 Likes

Yes, that makes sense, but what I have now is plenty fast. You're starting to scare me. :wink:

And I have to laugh. We're just not capable of shutting our minds off, once we get a hold of something like this, are we?

Me: "Mind? I've already moved on, Let it go."
Mind: "Yeah, right. You should know better. I'll let it go when I'm good and ready."

And for me, my mind is the perfect example of interrupt-driven software.

1 Like

Absolutely :slight_smile:

( a mixed blessing – XKCD captured the downside, I think xkcd: Nerd Sniping )

That is absolutely perfect. I find myself sniped many times a day. Of course, my ADD doesn't help.

But instead of "Oh look - a squirrel!", it's "Oh look - If we just modified this slightly..."

Thanks god for source control, making it easy to jump off the train when if I realize it isn't going anywhere I want to need to should go right now.

2 Likes