Returning Execute JavaScript results in an informative envelope

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

var a = { test: "text" };
var b = a;

log(a);
log(b);

function log(obj) {
...
}

What gets passed to “log”? a, b, or { test: “text” }? The answer is { test: “text” }. and { test: “text” } does not have a name property.

Well, I suppose it could have a name property:

var a = { test: "text", name: "ralph" };

… but it wouldn’t have anything to do with the name of the variable. :slight_smile:

By the way, MDN is my go-to reference source.

Assuming you mean a text expansion, I think you're probably right here.

So, I'd enter the object variable name, like "kmResults", and it would type:

logObj(kmResults, "kmResults");

where I have:

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

Good enough!

I know you guys can, and probably will, write your own macros for this, but for any interested readers, here's my simple macro:

###MACRO:   Paste logObject Function in JavaScript

~~~ VER: 1.0    2016-08-25 ~~~

####DOWNLOAD:
Paste logObject Function in JavaScript.kmmacros (3.6 KB)


###ReleaseNotes

TRIGGER: Typed String of:
;lo.ObjectVarName
followed by a [SPACE]

EXAMPLE:
;lo.kmResults

Assumes the log function name is:
logObject(Object, "ObjectName");

###Example Results

logObject(kmResults, "kmResults");

###logObject function

Just for completeness, here's the function I use:

function logObject(pObject, pObjectName) {
    console.log(pObjectName + ": " + JSON.stringify(pObject, null, 2));
}