Returning Execute JavaScript results in an informative envelope

'Execute script' actions return all their results as strings – the very useful common currency of Keyboard Maestro – but ...

A source of complexity is not being sure what kind of string you are getting back:

  • Is this just the expected contents of a text file ? Or maybe it's a warning that a file or folder wasn't found ?

  • Just a numeric output from a calculation ? Or maybe a message that an input was out of range ?

  • What should the macro do next?

    • Continue ?
  • Stop and notify ?

It would be simpler if results came back in 2 channels rather than one:

  • valid: false | true
  • value: undefined | some usable data

in fact , if 2 channels make life simpler, why not 3 ?

  • valid: false | true
  • value: undefined | some usable data
  • log: a record of the stages of processing, or a report of invalid inputs

For various purposes, including debugging, it can be useful for the final result to be accompanied by a log of the stages it went through.

In JavaScript we can:

  • Return a value to Keyboard Maestro as the JSON version of a set of key:value pairs
    • and split the return variable into three separate variables, e.g.
      • resultValid ? (true | false)
      • resultJustValue (any value returned, extracted from its envelope)
      • resultLog (any processing or problem log returned)
  • Automatically convert existing simple functions to derived versions which return their results wrapped in {valid:Bool, value:Any, log:String} envelopes
  • Use a helper function to automate the unwrapping and rewrapping needed to nest the application of these functions, accumulating growing logs, and passing on valid or invalid boolean flags resulting from checks on function inputs at each stage.

A couple of examples below:

  1. Logging the stages of processing

Get a log back from nested JXA functions.kmmacros (30.6 KB)

  1. Returning either a valid boolean and the contents of a file, or an invalid boolean and a message string if the file wasn't found

Try reading a file with JXA - get 3 variables back: (isValid, justValue, log).kmmacros (28.3 KB)

I’m kind of confused. Why not just set the KM result variables in the JavaScript itself, instead of having a second script to parse the json returned from the first script?

Like this:

(function() {
    'use strict';

    function execute() {
        // do something...
        
        kme.setvariable("resultVariable", { to: "myResult" });
        return "OK";
    }

    try {
        return execute();
    } catch (e) {
        return "Error: " + e.message;
    }
})();

If you need to return anything else, set another KM variable.

Capture the script’s result in a variable, and if it’s not “OK”, then there was an error.

What’s the advantage of your method(s)?

1 Like

Sure, that’s clearly another approach, tho I personally don’t know enough about KM to feel confident about whether or not we could end up with two threads in a race – i.e. whether the macro flow will always see the updated value of the variable in time.

More generally, you have probably spotted that those examples are combining the Writer Monad pattern with the Maybe Monad pattern, and as I’m often passing values around inside JavaScript in those kinds of envelopes or wrappers anyway, the simplest and quickest path for me is just to return the final result, without further work, back to KM through JSON.stringify, copy/pasting a standard reusable action to parse them in the macro.

A further advantage is, of course, that the Writer monad pattern also provides the incremental building of a log through a chain of nested function calls, and the Maybe monad pattern means that execution doesn’t have to be interrupted – the invalid flag and any log so far just get passed up through the chain.

(Not strictly ‘error handling’ in other words :slight_smile: )

I'm pretty sure the call to setvariable doesn't return until the variable has been set. @peternlewis can verify this, but I'd bet money on it.

As far as "Monad" patterns are concerned, you got me there. Never heard of that pattern before. Is that from the GoF?

1 Like

No, those patterns are from the functional rather than object tradition - essentially derived in this case from a bit of algebra, and analogous to modal logic, in which simple claims come wrapped in modifying envelopes.

The monad pattern essentially solves the problem of how you compose functions whose outputs are richer (more wrapped) than their inputs. It really just consists of a couple of helper functions, and some kind of useful envelope.

Thanks.

An updated version of one of those examples, to show the use of value wrappings a bit more clearly:

Imagine our Keyboard Maestro JS action uses four simple functions, with no input checking:

   // parse the string of a KM variable as a number
    var readNumber = function (strNum) {
            return parseFloat(strNum, 10);
        },

        // Square root of a number more than 0
        root = Math.sqrt,

        // Add 1
        addOne = function (x) {
            return x + 1;
        },

        // Divide by 2
        half = function (x) {
            return x / 2;
        }

We can use a helper function which converts these functions to versions which:

  1. Check their inputs, and
  2. return their results in a more informative envelope, with a valid flag, and a log of what worked or couldn't be done:
        wrappedReadNumber = wrappingVersion(
            readNumber,
            function (s) {
                return (typeof s === 'string') && s.length > 0 && !isNaN(s);
            },
            'read number from string',
            "can't parse as a number"
        ),

        wrappedRoot = wrappingVersion(
            root,
            function (x) {
                return !isNaN(x) && x >= 0;
            },
            'derived root',
            "can't obtain root for negative or non-numeric value"
        ),

        wrappedAddOne = wrappingVersion(
            addOne,
            function (x) {
                return !isNaN(x);
            },
            'added one',
            "can't add to non-numeric value"
        ),

        wrappedHalf = wrappingVersion(
            half,
            function (x) {
                return !isNaN(x);
            },
            'halved',
            "can't halve a non-numeric value"
        )

Composing and calling these wrapping versions (with a helper function) automatically results in the application of input tests, and the generation of a continuous log of what happened, as well as the result itself.

By the time the final (and still wrapped) result gets back to Keyboard Maestro, we can get three return variables from the script action rather than just one:

If the input KM variable is a valid (positive) numeric string:

and if it is a non-numeric, or numeric but negative (or simply empty) string:

Working example:

Richer returns from enveloped versions of JS functions.kmmacros (28.4 KB)

Dan, I like your method, but it will work ONLY for JXA.

Execute JavaScript in Browser cannot set KM Variables.

What I would find very useful, if anyone should has such, is a SIMPLE method that does this:

  1. In the Execute JavaScript in Browser, create a results variable (JSON?) that is saved to a KM Variable in the Action results block. This would be a results object of name/value pairs. If there is an error, it would have an "Error" object/name, with the value being the error msg and details.
    .
  2. A JXA function that would take this KM Variable (from Step #1), and create/set KM Variables accordingly, using the "name" as the KM Variable name.

I have a feeling this, or something similar, has already been posted, I just don't know where.

Thanks.

I think this will get you there. Let me know if not, or if I misunderstood.

var kmResults = {};

kmResults.Value1 = "My Value 1";
kmResults.Error = "My Error";

return JSON.stringify(kmResults);

// ==================================================

var kme = Application("Keyboard Maestro Engine");
var kmResults = JSON.parse(kme.getvariable("kmResults"));
setKMVariablesFromObject(null, kmResults, "zzz")

function setKMVariablesFromObject(kme, obj, prefix) {
    kme = kme || Application("Keyboard Maestro Engine");
    prefix = prefix || "";
    Object.keys(obj).forEach(function(key) {
        kme.setvariable(prefix + key, { to: obj[key] });
    });
}


Here’s a little bit of an explanation. I know I won’t have all the technical terms right, but the logic is correct:

var kmResults = {};

kmResults.Value1 = "My Value 1";
kmResults.Error = "My Error";

“kmResults” is an Object Literal. In it’s simplest form, it’s a set of key/value pairs. Actually, all JS objects are just key/value pairs.

Let’s look at the JSON string for the above code:

{
  "Value1": "My Value 1",
  "Error": "My Error"
}

See? key:value.

As such, you can reference object properties in a couple of ways:

kmResults.Value1 = "My Value 1";
kmResults["Error"] = "My Error";

If you use the second method, object property names can contain spaces and other characters.


So, if you want to walk through all the properties of an object, you can use

Object.keys(obj)

to get an array of the property names, e.g. keys.

Then you can use the keys to access the object’s properties, like this (or using a “for”, or whatever):

Object.keys(obj).forEach(function(key) {
    console.log("'" + key + "': '" + obj[key] + "'");
});
2 Likes

Dan: Perfect! :thumbsup:

Thanks for all the extra effort to explain.
Not only did it help me, but I'm sure it will be of great help to future readers.

1 Like
Object.keys(obj).forEach(function(key) {
    console.log("'" + key + "': '" + obj[key] + "'");
});

For the benefit of others who, like me, may not get this at first, to use the above statement you have to replace BOTH instances of "obj" with the actual object variable, as in:

Object.keys(kmResults).forEach(function(key) {
    console.log("'" + key + "': '" + kmResults[key] + "'");
});

So, to make it clearer and easier for me, I refactored this into this simple function:

function logObject(pObject) {
  Object.keys(pObject).forEach(function(key) {
    console.log(key + ': ' + pObject[key]);
});
}

// Call it like this:

logObject(kmResults);

// RESULTS:
/* Value1: My Value 1 */
/* MyNum: 100 */

OK, I'm a bit slow this morning -- still on my first cup of java. :wink:

No, you’re not slow. I figured it might take a little for you to dig through what I posted, but I thought you’d enjoy and benefit from the learning process. Figuring out this kind of stuff is fun, at least for me… and when it’s not overwhelming. :slight_smile:

Just curious, what does the “p” stand for - “parameter”?

Exactly. It's been a life-long naming convention to make parameters jump out in the function code.

1 Like

I guess a lazy version, FWIW, might be:

function logObj(o) {
    console.log(JSON.stringify(o, null, 2));
}

1 Like

Nice. I like it.
Is there any way to log the object name as part of that function?

Only if you pass the name. This function (actually both versions of this function) will display the name if you include it, but will only display the result if you don’t include the name.

function logObj(o, name) {
    console.log((name ? name + ": " : "") + JSON.stringify(o, null, 2));
}

var test = {abc: "one"};

logObj(test, "test");
/*
test: {
  "abc": "one"
}
*/
logObj(test);
/*
{
  "abc": "one"
}
*/


// And a little different way
Object.prototype.dump = function dump(name) {
    console.log((name ? name + ": " : "") + JSON.stringify(this, null, 2));
}

test.dump();
/*
{
  "abc": "one"
}
*/

test.dump("test");
/*
test: {
  "abc": "one"
}
*/

OK, I hate having to type the object name twice, but you inspired me. :smile:

How about this:

logObjectName('kmResults');

function logObjectName(pObjectName) {
    var oObject = eval(pObjectName);
    console.log(pObjectName + ": " + JSON.stringify(oObject, null, 2));
}

/* kmResults: {
  "Value1": "My Value 1",
  "MyNum": 100
} */

That works, but if it were me, I’d just create a hotkey to do it. eval is a dangerous beast. Probably not the way you’re using it, but still…

Now that I think of it, your code won’t work all of the time. It will only work if the variable is in the same scope as the eval statement.

So if you were in a function, like this:

function test() {
    var a = "something";
    logObjectName("a");
}

… tt won’t work.

Worse yet would be this scenario:

function test() {
    var a = "something";
    logObjectName("a");
}

var a = "else";

The result of the “logObjectName” call from inside “test()” would be the name “a” with the value “else”.

Oh, well, it was worth a shot!
But you're right, of course.

It is amazing of all of the properties provided by the "Object" construction, name if not one of them:

Object - JavaScript | MDN