Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

Quickest Way to Detect & Prioritize One Argument Over Others (and various fat arrow syntax questions)

1591384947

Edited 1591385082
timmaugh
Pro
API Scripter
I say "quickest", but really for most cases I think we're talking about a difference of milliseconds, so for the purposes of this discussion, let's refer to some desirable hybrid of code concision and processing performance. I know that sounds dangerously like "best" for a forum topic, but I'll take my chances. =P I have a situation where I want to prioritize processing one argument in the API command line before the others. For instance, given a command line like this: !the_script_name --foo:bar --qud:zu --port:out --starboard:home --posh --notes:Best Practices: vol. 1 I want to process the qud argument first, because what I do with it will change what I do with the others. For the purposes of the " quickest best most elegant" portion of this question, I should note that there are both splittable arguments (i.e., foo:bar,  etc.) and non-splittable, 'trigger' arguments ( posh ). We also have arguments that can potentially contain the character driving our split (the value of notes includes the ':' character). To accomplish that, I am currently using a script that looks like this: var the_script_name = the_script_name || (function () {     'use strict';     const splitArgs = (a) => { return a.split(":") };     const joinVals = (a) => { return [a.slice(0)[0], a.slice(1).join(":").trim()]; };     const lookFor = (a) => { return a[0] === "qud"; };          const handleInput = (msg_orig) => {         if (msg_orig.type !== 'api' || !msg_orig.content.toLowerCase().startsWith('!the_script_name ')) {             return;         }         let args = msg_orig.content.split(/\s--/)          // split at argument delimiter             .slice(1)                                      // drop the api tag             .map(splitArgs)                                // split each arg (foo:bar becomes [foo, bar])             .map(joinVals);                                // if the value included a colon (the delimiter), join the parts that were inadvertently separated         log(args);         processQud(args);                                  // look for qud argument first         log(args);                                         // qud argument, if present, has been removed // process other arguments here     };     const processQud = (args) => {         let qud = args.filter(lookFor)[0] || [];         if (qud.length) {             // ...stuff happens here...             args[args.findIndex(lookFor)] = args.pop();    // pop the last item and copy it over the qud argument         }         return;     };     const registerEventHandlers = () => {         on('chat:message', handleInput);     };     return {         RegisterEventHandlers: registerEventHandlers     }; })(); on('ready', () => {     the_script_name.RegisterEventHandlers(); }); As far as the best way to prioritize an argument, I have seen it done a few different ways. I've seen a switch{} block (looking for the arg, breaking when found). I've seen assigning all the argument pairs (foo:bar) to an object (as key:value), and then using hasOwnProperty. I think I've seen some of Aaron's code using the underscore _.has function, but I'm not sure if that only works for an object. So, is one faster/better/more trusted/more versatile? Are there edge cases for the way arguments could be written that my code wouldn't catch? Also, a few questions about the syntax of my code, above, to help me learn... Question 1 How can I pass another variable into a fat arrow block for use with a map(), filter(), or reduce() function? The above code works fine if I am only looking to prioritize the argument qud , but if I need to prioritize a second argument after that, my lookFor function is currently hard coded to only filter on "qud". Relevant bits: const lookFor = (a) => { return a[0] === "qud"; };     //... and ... // let qud = args.filter(lookFor)[0] || []; Question 2 I am trying to explore and learn the fat arrow syntax, and have used it for the interior function declarations. I know that my script declaration can use it, too: var skilllister = skilllister || (() => { ...but before I use that, I'm trying to understand the closing of that function block. Specifically, why the () that follows the parentheses encompassing the function declaration: ?)(); Are those to turn the declaration into a executable function, so that when the sandbox starts up and concatenates all of the scripts, the script gets run its one and only time? (please say yes... please say yes... please say...) Question 3 --I think I answered this one, in typing this up--
1591387788

Edited 1591387808
Jakob
Sheet Author
API Scripter
Faster doesn't matter .  You said it already, but anything you do (unless it's something really silly, like an exponential-time algorithm) will be dwarfed by the speed of interacting with Roll20. Optimize for readability, not speed. You seem to be generally doing the right thing with separating parsing and the business logic. As to the questions: 1) You can curry functions like this: const fn = (termToSearchFor) => ((a) => (a[0] === termToSearchFor)); The braces probably aren't needed, I just added them for readability. fn("qud") is a function which does the same things as your lookFor function. So fn is a function that returns a function by binding the termToSearchFor argument in the inner function. (yep, functions returning functions...). Though to be honest, I would just write this inline instead of defining a separate function.  (a) => (a[0] === "foo")  seems more readable than fn("foo"). ( side remark: arrays are rarely good data structures if the array elements mean different things. consider converting your array of two-element arrays to an object, then you don't have to do awkward things like comparing a[0] ). 2) Yes and no. It is already a function, it's not converted to it; what it does is execute this function once. This is called an IIFE (immediately invoced function expression) for reasons you might imagine. And so yeah, it gets run exactly once. The point of the  var skilllister = skilllister || (IIFE)();  is that after first execution,  skilllister    will hold a value (namely an object with the single key RegisterEventHandlers) and so the function will not be invoked anymore. This only matters if you add the script more than once by accident. A more modern way to write this would be  const skilllister = (IIFE)();  The const makes sure that you can't define this more than once (it will crash if you add the script twice).
1591388503
The Aaron
Roll20 Production Team
API Scripter
Jakob said pretty much everything I would say. =D If it were me, I'd probably parse all your arguments into an array of objects, which each object representing an argument and it's various parameters.  Then I'd either sort the array to allow processing in the desired order, or traverse and extract the elements I want to parse first, etc.  Kind of depends on what you're doing.  If your arguments are intended to be unique, you could use them as keys on an object and then parse each order important key first, then deal with the remaining ones.
1591391407
timmaugh
Pro
API Scripter
Thanks, guys. To follow up... @Jakob... I had thought that passing the variable would have to look something like that (nested functions), but couldn't quite wrap my mind around it. And although I had read several explanations of the IIFE structure and the better practice of non- var declarations, converting my script to a const declaration had always failed with a "You can't reference the thing before it's a thing... what are you, some kind of heathen?" error. (Pretty sure that's what it said. Pretty sure.) Now I get that it was the || that was the problem. Thank you! @Aaron... I would *think* that my args should be unique. If you supply the same arg twice in the line, I am only going to store and use one of those instances... probably the last one. @Both... That would seem to point me to the object model. I assume you are both talking about the same idea, there. I would map the command line args of foo:bar  and qud:zu  to myObj so that myObj.foo = bar and myObj.qud = zu (with appropriate precations for type and spaces in the arg name). Tell me if I'm on the right track with assuming that I would do that at the point where I have an array like: [["foo","bar"],["qud","zu"],["port","out"],["starboard","home"],["posh",""],["notes","Best Practices: vol. 1"]] ...and I would probably utilize the spread operator like... (fair warning: air-coding incoming)... let myObj = {}; Object.assign(myObj, {...args[][0] : ...args[][1] }; ...no... that doesn't feel right. Hmm.. would it be a spread operator utilizing an arrow function that implemented a single line of value assignment to the object? let myObj = {}; let fill = (myObj,...args) => ((a) => { myObj[a[0]] = a[1] }); That feels closer, but I'm pretty sure I'm not there.
1591395084
Jakob
Sheet Author
API Scripter
There's a built-in-method for this (and there isn't built-in syntax using spread operators, I believe): myObj = Object.fromEntries(array_of_pairs);
1591397215
timmaugh
Pro
API Scripter
Ok, so now I feel like the guy just pushing, and pushing, and pushing on an apparently locked door... until Jakob kindly points out the "Pull to Open" sign. Oh. Cheers, Jakob! I will give that a whirl.
1591478496
timmaugh
Pro
API Scripter
Running into to a bit of a problem. How should it look when I call that declaration of currying functions? (I know the line itself is simple enough that I could just type it, and I know that with only two arguments I need to use it for, I could just type it twice, but this is a learning exercise, as well, so I want to understand.) When I replace the lookFor declaration with this: const lookFor = (arg) => (a) => return a[0] === arg; ...the articles I've read seem to suggest a call looking like: lookFor("value for arg") ("value for a") ...which makes my findIndex() statement: args[args.findIndex(lookFor("qud")(a)]=args.pop(); But that doesn't work. So then I think that findIndex() function is looking for an argument => () structure, so I try... args[args.findIndex((a)=>lookFor("qud"))]=args.pop(); And that doesn't work either. So I leave that as is, but I rewrite my declaration to be: const lookFor = (arg) => return a[0] === arg; And then... THEN ... I think I have it. Narrator's Voice: He doesn't. He doesn't have it at all. Help?
1591480686

Edited 1591480817
Jakob
Sheet Author
API Scripter
No worries!  findIndex  expects as its argument a function that will be called with an element of the array. So it should be a function which accepts an array argument (since elements of your array are two-element arrays) and which returns a boolean. I think inspecting types is pretty instructive here, so let me do it: lookFor("qud")(a) This can't work right? This is not a function. If a was defined, this would be a boolean, but in fact a is undefined (you can't see a definition for a anywhere), so this will just crash because of an undefined symbol a. (a)=>lookFor("qud") This is a function which takes a single argument (good, this is what we wanted) and returns lookFor("qud"), which is a function of a single argument (bad, we wanted a boolean!). In fact, the return value of this function doesn't depend on a at all, also not good. const lookFor = (arg) => return a[0] === arg; Different approach, also can't work: a is undefined here, this function will crash because you have an undefined symbol. The second approach was almost good: using the original lookFor (which was correct!): const lookFor = (arg) => (a) => return a[0] === arg; we can have the following function (a)=>lookFor("qud")(a) This is now a function which will accept a single argument a and return the value of lookFor("qud") evaluated at a, which is a boolean (also good!). But now this function is the same thing as lookFor("qud") basically because f   is the same thing as x => f(x)  (more or less, it's a bit more subtle when f can take more than one parameter, but this doesn't matter here). So the right thing to write would be args.findIndex(lookFor("qud"))
1591538374

Edited 1591564149
timmaugh
Pro
API Scripter
Got it. It all makes sense once you see it working together... I feel like I have a good grasp on the concepts from my other programming experience, but with the fat arrow syntax I feel like I'm not only learning a new language, I'm trying to learn that language's slang at the same time. :-D It's super helpful to sit at someone's elbow like this (who knows what they're doing), so thanks to you both for helping us along the way! At least for my local testing (an html mock-up so I don't have to constantly load and break my sandbox), your initial suggestion worked except that I had to include the curly braces. (I will post the revised code below for anyone who finds this thread later.) But I'm glad that it took an extra back and forth between us because that type inspection discussion was pretty illuminating. It's good to realize my intuition was correct, that the findIndex  function wanted a hook to pass the elements of the array, so something in the shape of: (a) => f(); ...except that f(x) is a mathematical singularity where x ∈ V and |V| = 1. One value in, one value out. Like you said, f = f(x). I missed the fact that the presence of the f(x) breaks the calls ability to pass variables from an outer arrow to an inner. So... correct me if I'm wrong... but this WON'T work... (a) => f(x) => (a) => a === x; ...specifically because f(x) isn't passing the first a variable to the interior function (and I know I don't need the f, there, but include it for readability). But I can expect that the following SHOULD work: (a) => f(x)(a) => (a) => a === x; ...which is where we ended up. (a) =>lookFor("qud")(a) And because f("qud") is always a single thing, that simplifies to lookFor("qud") Is that a valid analysis? Here's the revamped code for anyone who finds this thread later and wants to see it work: const the_script_name = () => ({     'use strict';     const splitArgs = (a) => { return a.split(":") };     const joinVals = (a) => { return [a.slice(0)[0], a.slice(1).join(":").trim()]; };     const lookFor = (arg) => (a) => { return a[0] === arg; };          const handleInput = (msg_orig) => {         if (msg_orig.type !== 'api' || !msg_orig.content.toLowerCase().startsWith('!the_script_name ')) {             return;         }         let args = msg_orig.content.split(/\s--/)          // split at argument delimiter             .slice(1)                                      // drop the api tag             .map(splitArgs)                                // split each arg (foo:bar becomes [foo, bar])             .map(joinVals);                                // if the value included a colon (the delimiter), join the parts that were inadvertently separated         log(args);         processQud(args);                                  // look for qud argument first         log(args);                                         // qud argument, if present, has been removed // process other arguments here     };     const processQud = (args) => {         let qud = args.filter(lookFor("qud"))[0] || [];         if (qud.length) {             // ...stuff happens here...             args[args.findIndex(lookFor)] = args.pop();    // pop the last item and copy it over the qud argument         }         return;     };     const registerEventHandlers = () => {         on('chat:message', handleInput);     };     return {         RegisterEventHandlers: registerEventHandlers     }; })(); on('ready', () => {     the_script_name.RegisterEventHandlers(); }); EDIT: I'd left the old script declaration at the top, and have changed it for the fat arrow version. Don't think I need fat arrow portion, though (the "() =>"), right? I'll never be passing anything. I just need the script to equal the IIFE.