From 56bb075411ccefd2d3d7a5cb525d326f025e84bd Mon Sep 17 00:00:00 2001 From: Mike McNeil Date: Tue, 21 Mar 2017 03:14:40 -0500 Subject: [PATCH 1/4] Initial draft for `args: ['foo', 'done()', 'more[]']` and `implementationType: 'classic' --- machine-def.struct.js | 146 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/machine-def.struct.js b/machine-def.struct.js index 5e6f82c..187e639 100644 --- a/machine-def.struct.js +++ b/machine-def.struct.js @@ -33,6 +33,152 @@ module.exports = { someExit: require('./exit-def.struct') }, + + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // FUTURE: + // In the machine specification, we always leverage "s-selection"; that is, the implementation exposes named + // inputs, allowing a caller invoking a machine to configure runtime arguments for particular inputs based + // on their meaning. But this contrasts with how traditional subroutines in many programming languages are + // addressed. Most languages use "c-selection" to assign runtime arguments to parameters (i.e. inputs) of + // a function. + // + // While these kind of syntactic/grammatical relations can be precisely imitated by runtime tooling, it is + // helpful to be able to declare it in a standardized way, as part of the machine definition itself. This + // has practical utility for when full or partial serial assignment is the only option (e.g. a web-based API + // that must support taking in data via route/path parameters in its URL pattern; or a CLI script that must + // accept serial command-line arguments). + // > For an example, see: + // > • https://github.com/sailshq/machine-as-script/tree/e1a54d18d65809eb4cfa64bfe9db4ae925146e77#using-serial-command-line-arguments + // + // ``` + // args: ['someInput'] + // ``` + // + // I. What about variadic usage? + // Currently, the machine specification does not allow for variadic usage (although there is limited, + // non-standard support for this in machine-as-script). This is something that makes perfect sense for + // future versions of the spec. This would allow for numerous additional possibilities; e.g. + // ``` + // args: [ + // ['foo', 'bar', 'baz'], // << indicating order, and which inputs can be satisfied via serial usage (not all of them must be, necessarily -- imagine serial CLI arguments (`sails new foo`) vs. CLI options (`sails new foo --without='grunt'`) ) + // ['foo', 'baz'], // << indicating structual optional-ness of serial args (both of these usages existing indicates that either usage is acceptable) + // ['foo', 'bar', 'baz', 'blazes[]'], // << n-ary/variadic functions that support bundling of extra serial arguments as an array, then passing that array in for a particular input (but note that this is the LAST serial argument, and also that it is only acceptable if it lists at least one more argument than every other serial usage that has been declared. This is just to avoid ambiguity / unnecessary runtime performance degredation for extra data-type checking.) + // ['done()'], // << this special, parentheses-trailed syntax indicates that this serial argument is actually a replacement for `exits` (note that this is not recommended with variadic usage, since the callback should always be the last argument, and variadic usage would force the callback to come first. The name before the parentheses does not actually matter.) + // ] + // ``` + // + // II. Does this affect the implementation (`fn`)? Or is it just a guideline for runners as far as providing usage? + // Out of the box, `args` does not necessarily have any impact on usage. That's up to the runner. + // As far as the implementation: by itself, the presence of `args` does not change the expected implementation of `fn`. + // However, that can be instrumented (see below). + // + // III. Can I use special syntax for an asynchronous callback with a synchronous (blocking) machine? + // No. When `sync: true`, special callback syntax (e.g. `done()`) is NOT permitted in `args`. + // + // IV. Can I specify special syntax for more than one asynchronous callback? + // No. Only standard Node callback syntax is supported in the spec. (Just about any imaginable mash-up + // can be exposed at the runner level using only this abstraction-- including support for promises, async/await, + // generators, and even fibers.) + // + // > See also: + // > • https://en.wikipedia.org/wiki/Theta_role + // > • https://en.wikipedia.org/wiki/Theta_criterion + // > • https://en.wikipedia.org/wiki/Morphosyntactic_alignment + // > • https://en.wikipedia.org/wiki/Grammatical_relation + // > • https://en.wikipedia.org/wiki/Arity#Variable_arity + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // FUTURE: + // In certain situations (mainly just for low-level machines, such as adding two numbers), the default approach + // for implementing machines adds a certain amount of inconvenient overhead & latency; at least when compared + // with a traditional userland function or native operator. This performance hit is irrespective of the runner + // being used-- it's tied to the fact that an `exits` object and multiple handler callbacks must be constructed + // (whereas normally, you can add two numbers without doing all that). + // + // So to work around that, a machine could declare `implementationType: 'traditional'`, which indicates that `args` + // must also be provided, and that `fn` should be implemented something like the following: + // > • Remember: when `sync: true`, special syntax for an asynchronous callback (e.g. `done()`) is NOT permitted in `args`! + // > • If `args` is omitted, runners might opt to rely on key order of `inputs`. (But if they fall back to this, they should log a warning.) + // ``` + // sync: true, + // args: ['numerator','denominator'], + // inputs: { /*...*/ }, + // exits: { + // success: { outputFriendlyName: 'Quotient', outputExample: 123 }, + // badDenominator: { + // description: 'That is not a valid denominator.', + // outputFriendlyName: 'Approximation', + // outputExample: 9007199254740991 + // } + // }, + // implementationType: 'classic', + // fn: function(numerator, denominator) { + // var flaverr = require('flaverr'); + // + // if (Object.is(denominator, -0)) { + // throw flaverr({ + // exit: 'badDenominator', + // output: Number.MIN_SAFE_INTEGER||-9007199254740991 + // }, new Error('Cannot divide by zero!)); + // } + // else if (Object.is(denominator, 0)) { + // throw flaverr({ + // exit: 'badDenominator', + // output: Number.MAX_SAFE_INTEGER||9007199254740991 + // }, new Error('Cannot divide by zero!)); + // } + // else { + // return numerator / denominator; + // } + // + // // (Finally, note: to get access to `env`, you can do `this` -- e.g. `this.req`). + // } + // ``` + // + // Now consider the same thing, but for an asynchronous function: + // > Note that `args` is required, and that it must contain a special item indicating the callback -- e.g. `done()` + // > Also note that other lambda functions -- e.g. an iteratee function -- do not get the `()` suffix! + // > • Since `sync` is not enabled, special syntax like `done()` is REQUIRED in the `args` list + // > • Since `sync` is not enabled, the return value is ignored. In the incident of a caught exception (key word "CAUGHT" -- careful in your callbacks!), the `error` exit is assumed, and triggered automatically. + // ``` + // args: ['criteria','populates','handleEachRecord','done()'], + // inputs: { /*...*/ }, + // exits: { + // success: { outputFriendlyName: 'Num streamed', outputExample: 3 }, + // badCriteria: { description: 'Invalid `criteria`.' }, + // badPopulates: { description: 'Invalid `populates`.' }, + // }, + // implementationType: 'classic', + // fn: function(criteria, populates, eachRecord, done, meta, miscQKs) { + // var flaverr = require('flaverr'); + // + // var numRecordsStreamed = 0; + // this.model.stream(criteria, populates) + // .eachRecord(function(record, next){ + // handleEachRecord(record); + // return next(); + // }) + // .exec(function (err) { + // if (err && err.name === 'UsageError' && err.code === 'E_BAD_CRITERIA') { + // return done(flaverr({exit: 'badCriteria'}, err); + // } + // if (err && err.name === 'UsageError' && err.code === 'E_BAD_POPULATES') { + // return done(flaverr({exit: 'badPopulates'}, err); + // } + // else if (err) { return done(err); } // << misc. (===`error` exit) + // else { return done(undefined, numRecordsStreamed); } + // }); + // } + // ``` + // + // > See also: + // > • https://en.wikipedia.org/wiki/Evaluation_strategy + // > • https://en.wikipedia.org/wiki/Parameter_(computer_programming)#Named_parameters + // > • http://stackoverflow.com/a/7223395/486547 (as an aside re negative zero vs. positive zero and about `Object.is()`) + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - fn: function (inputs, exits) { From 4eef68d1e3386cde46d9bd3518b7eda4ad17b37b Mon Sep 17 00:00:00 2001 From: Mike McNeil Date: Tue, 21 Mar 2017 03:16:34 -0500 Subject: [PATCH 2/4] Fix some lingering typos --- machine-def.struct.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/machine-def.struct.js b/machine-def.struct.js index 187e639..2c1aff5 100644 --- a/machine-def.struct.js +++ b/machine-def.struct.js @@ -152,7 +152,7 @@ module.exports = { // badPopulates: { description: 'Invalid `populates`.' }, // }, // implementationType: 'classic', - // fn: function(criteria, populates, eachRecord, done, meta, miscQKs) { + // fn: function(criteria, populates, handleEachRecord, done) { // var flaverr = require('flaverr'); // // var numRecordsStreamed = 0; From baa0a2b6868177b9ccb5443b132d0777ecba592e Mon Sep 17 00:00:00 2001 From: Mike McNeil Date: Tue, 21 Mar 2017 03:27:36 -0500 Subject: [PATCH 3/4] Fix some typos, add an example of what standard implementationType looks like when defined w/ `args`, and demonstrate variadic usage --- machine-def.struct.js | 63 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/machine-def.struct.js b/machine-def.struct.js index 2c1aff5..96def9d 100644 --- a/machine-def.struct.js +++ b/machine-def.struct.js @@ -144,7 +144,11 @@ module.exports = { // > • Since `sync` is not enabled, special syntax like `done()` is REQUIRED in the `args` list // > • Since `sync` is not enabled, the return value is ignored. In the incident of a caught exception (key word "CAUGHT" -- careful in your callbacks!), the `error` exit is assumed, and triggered automatically. // ``` - // args: ['criteria','populates','handleEachRecord','done()'], + // friendlyName: 'For each user...', + // args: [ + // ['criteria','populates','handleEachRecord','done()'], + // ['criteria','handleEachRecord','done()'], + // ], // inputs: { /*...*/ }, // exits: { // success: { outputFriendlyName: 'Num streamed', outputExample: 3 }, @@ -152,13 +156,28 @@ module.exports = { // badPopulates: { description: 'Invalid `populates`.' }, // }, // implementationType: 'classic', - // fn: function(criteria, populates, handleEachRecord, done) { + // fn: function(criteria, populatesOrHandleEachRecord, handleEachRecordOrDone, doneMaybe) { + // var _ = require('lodash'); // var flaverr = require('flaverr'); // + // // If there are only 3 arguments, then we know `populates` must have been omitted. + // // (so shift everything over as needed) + // var done; + // var handleEachRecord; + // if (_.isUndefined(done)) { + // done = handleEachRecordOrDone; + // handleEachRecord = populatesOrHandleEachRecord; + // } + // else { + // done = doneMaybe; + // handleEachRecord = handleEachRecordOrDone; + // } + // // var numRecordsStreamed = 0; - // this.model.stream(criteria, populates) + // User.stream(criteria, populates) // .eachRecord(function(record, next){ // handleEachRecord(record); + // numRecordsStreamed++; // return next(); // }) // .exec(function (err) { @@ -174,6 +193,44 @@ module.exports = { // } // ``` // + // For reference, here's an equivalent way to write the same example as above: + // (equivalent from a usage perspective, anyway) + // ``` + // friendlyName: 'For each user...', + // args: [ + // ['criteria','populates','handleEachRecord','done()'], + // ['criteria','handleEachRecord','done()'], + // ], + // inputs: { /*...*/ }, + // exits: { /*...*/ }, + // (Notice we removed the `implementationType`!) + // fn: function (inputs, exits) { + // + // // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + // // Note that variadics are take care of automatically by our runner + // // when it maps the incoming data over to this implementation type. + // // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + // + // var numRecordsStreamed = 0; + // User.stream(inputs.criteria, inputs.populates) + // .eachRecord(function(record, next){ + // inputs.handleEachRecord(record); + // numRecordsStreamed++; + // return next(); + // }) + // .exec(function (err) { + // if (err && err.name === 'UsageError' && err.code === 'E_BAD_CRITERIA') { + // return exits.badCriteria(err); + // } + // if (err && err.name === 'UsageError' && err.code === 'E_BAD_POPULATES') { + // return exits.badPopulates(err); + // } + // else if (err) { return done(err); } // << misc. (===`error` exit) + // else { return exits.success(numRecordsStreamed); } + // }); + // } + // ``` + // // > See also: // > • https://en.wikipedia.org/wiki/Evaluation_strategy // > • https://en.wikipedia.org/wiki/Parameter_(computer_programming)#Named_parameters From fc9f0e536b0c97381a8680360e5eb682bfe5dd74 Mon Sep 17 00:00:00 2001 From: Mike McNeil Date: Tue, 21 Mar 2017 03:28:13 -0500 Subject: [PATCH 4/4] tweak number of chars to match reality that we validate against when parsing compact node-machine defs --- machine-def.struct.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/machine-def.struct.js b/machine-def.struct.js index 96def9d..ebc9e43 100644 --- a/machine-def.struct.js +++ b/machine-def.struct.js @@ -3,7 +3,7 @@ module.exports = { friendlyName: 'Do something', - description: 'Do a thing (should be <=80 characters written in the imperative mood)', + description: 'Do a thing (should be <=140 characters written in the imperative mood)', extendedDescription: 'Longer description. Markdown syntax supported.',