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

Spread Syntax and Fat Arrow (aka, it's gettin, it's gettin, it's getting kinda weedy)

1591645912

Edited 1591646089
timmaugh
Pro
API Scripter
(This builds on a previous discussion I had started looking to prioritize one argument in the list supplied by the user.) I am trying to expand my understanding of fat arrow syntax and the spread operator, but I'm having trouble using them together. How do I refer to the spread elements of an array within the function to which they are sent? Example... given the chat command line of: !the_script_name --foo:bar --qud:zu --qud:the dangles --q:third --port:out --starboard:home --posh --notes:Best Practices: vol. 1 ...I create an array of arguments ( args ) in the form of: [["foo","bar"],["qud","zu"],["qud","the dangles"],["q","third"],["port","out"],["starboard","home"],["posh",""],["notes","Best Practices: vol. 1"]] If I want to copy out the elements from this array that match the argument "qud" (the first element of the inner arrays), I can do this: let matchedArgs = []; matchedArgs.push(...args.filter(lookFor("qud"))); // this works But if I had a bunch   of arguments that I wanted to pull out (more than just "qud"), I would need to pass each through that call to lookFor . If I wanted to pull out both the "qud" arguments and the "q" arguments, I would have an array ( argAliases ) in the form of: ["q","qud"] This starts to sound like a case for the spread syntax, only I'm not sure how to reference the element from the argAliases array within the function that I'm sending it to. Here is what I am trying: (...argAliases) => (alias) => { matchedArgs.push(...args.filter(lookFor(alias))); }; I read that as "take each element of argAliases and pass it as ' alias ' into the lookFor() function, which will be used as the basis of a filter on the args array; push each result into the matchedArgs array." It's basically taking my statement that works (for just the static value "qud") and trying to turn it into a fat arrow function to which I can pass each element of argAliases , but I'm just not having any luck. Sandbox and Full Code This is in the form of an HTML file to mimic the chat so I don't have to constantly break and restart my sandbox. The functionality should be the same. If there is still some question about why I'm trying to find an answer to this specific problem, I can explain more about the logic behind it. <!DOCTYPE html> <html> <body> Command Line Supplied: <input type="text" id="msg" value="!the_script_name --foo:bar --qud:zu --qud:the dangles --q:third --port:out --starboard:home --posh --notes:Best Practices: vol. 1" style="width:400px;"> <button onclick="handleInput()">Test It</button> <p id="log"></p> <script> function log(stmt) {     document.getElementById("log").innerHTML += JSON.stringify(stmt) + '<br><br>'; return; }; 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 lookForVal = (arg) => (a) => { return a[1] === arg; }; const getKeyByValue = (object, value) => { let matchArr = Object.entries(object) .filter(lookForVal(value)) .map((a) => { return a[0]; }); return matchArr; }; const argAliasTable = {     f:"foo", foo:"foo", b: "bar", bar: "bar", ba: "baz", baz: "baz", q: "qud", qud: "qud", }; const handleInput = () => {     document.getElementById("log").innerHTML = "";     let msg = {}; msg.content = document.getElementById("msg").value;     let args = msg.content.split(/\s--/)      .slice(1)      .map(splitArgs)      .map(joinVals); log(args); processQud(args); log(args); return; }; const processQud = (args) => {     let argAliases = getKeyByValue(argAliasTable,"qud"); log(`===== argAliases =====`); log(argAliases); let matchedArgs = []; matchedArgs.push(...args.filter(lookFor("qud"))); // (...argAliases) => (alias) => { matchedArgs.push(...args.filter(lookFor(alias))); }; log(`===== matchedArgs =====`); log(matchedArgs); if (matchedArgs.length) { log(`This is where the stuff happens.`); // remove each element from args array     } return; }; </script> </body> </html>
1591646430
The Aaron
Roll20 Production Team
API Scripter
Just to address a slight misunderstanding real quick, you actually have two operators here: the Rest operator ... the Spread operator ... Not the least bit confusing, I know...  =D The Rest operator is used in a context where you have multiple arguments, but you just want to "put the rest in here": const func = (a, b, c, ...theRest) => { /* do stuff */ } Here, you have the named arguments a, b, and c, but then the "rest" of them are in the argument theRest as an array. The Spread operator is when you have an array of things and you want to "spread" them out as if they were are arguments: let low = [1,2,3]; let high = [4,5,6]; let all = [...low, ...high]; Here, you construct an array all by "spread"ing out the values from a couple of arrays, low and high. There are other cases where you might use these, but that's the basics.
1591647012
The Aaron
Roll20 Production Team
API Scripter
This starts to sound like a case for the spread syntax, only I'm not sure how to reference the element from the  argAliases  array within the function that I'm sending it to. Here is what I am trying: (...argAliases) => (alias) => { matchedArgs.push(...args.filter(lookFor(alias))); }; Function currying can be a bit confusing in any language.  What is going on here is a function that returns another function.  What makes this powerful is the idea of a closure created by the first function and accessible by the second.  A closure is like an unnamed object that contains all the things created in a function, and is accessible by all the things created in that function, including functions returned from it.  So, rewriting the above more verbosely: const funcBuilder = /* closure definition begins */ (...argAliases) =>{ return (alias) => { matchedArgs.push(...args, filter(lookFor(alias))); }; /* closure definition ends */ }; const builtFunc = funcBuilder("a","b","c"); /* create new function with new closure where argAlias == ["a","b","c"] */ builtFunc("b"); // use new function with specific closure For each function that funcBuilder returns, there is a new closure that has a variable argAliases, which is an array of the parameters passed to funcBuilder.  Returned functions can then access those via array notation or other array functions: if(argAlias.includes(alias)) { /* do something */ } Not sure how best you can use that knowledge, but that's where you need to start with understanding it.
1591651638
timmaugh
Pro
API Scripter
Thanks, Aaron. So if the argAliases array has 4 items, and I use it with the spread operator like so: funkyBunch(...argAliases); ...I am passing 4 arguments to the function. If that funkyBunch signature uses the rest operator: const funkyBunch (a, ...aliases) => {}; ...then it will have argAliases[0] in a , and aliases will be an array of length 3? And, if I'm understanding everything, the spread operator won't iterate an operation using the array elements... but for that we have map(), reduce(), filter(). Editorial Postscript: As for how best to use the knowledge, it's just fun to learn! ...that, and I'm trying to get from the point of having this array of arguments to the object model you suggested in the other thread (prioritizing one argument in the list first). Whether the model is myObj.qud or qud.value, I only want the one entry for qud, and it might come in under an alias (q, or qud). I'm thinking now that I can use map() to change all of the aliased arguments to the standard version (all q become qud)... and then do an Object.fromEntries(), so that only the last is kept. That sounds simpler. Also, on a side note, I have a portion of my script that outputs user supplied variables beside what they mapped out to (ie, "you gave me a value for the color... here's how I interpreted it"), and it currently outputs unrecognized arguments, too (you gave me a value for 'leftshark'... I'm not sure what to do with a leftshark"). To accomplish that with the object model (myObj.qud) I could get the keys of the object, filter them where they're not in my list of recognized arguments, and then output what's left.
1591668321
The Aaron
Roll20 Production Team
API Scripter
I believe you're right on your understanding of rest and spread, but don't let that stop you from trying it out. =D
1591714380
timmaugh
Pro
API Scripter
Excellent! So let's have some fun. If I want to construct an equivalent to the functionality I thought spread syntax gave us (iterable operations into a function), I can do: const spread = (f, a) => {     for(let i = 0 ; i < a.length ; i++) {         f(a[i]);     } }; const theFunc = (arr) => {     log(arr); }; const doTheThing = () => {     let args = [["foo","bar"], ["qud","zu"], ["q","blue"]];     log(args);     spread(theFunc, args); }; Simple enough in that even though it uses a full for loop, it's wrapped up in a function that simplifies calling and later coding of the same lines. We just have to trust that any function (f) we build will know how to handle its own (a). But I'm not sure how well it handles edge cases or whether it could be expanded to be more helpful/flexible. Then again, maybe I'm just not fluent enough in the language to know if the above couldn't be better achieved through some application of map, filter, reduce, and _.chain could accomplish the same thing.
1591715144

Edited 1591715228
timmaugh
Pro
API Scripter
For example, if I wanted to get the key/val pairs from an array into an object, spread syntax has unexpected results with Object.assign . I could use Jakob's suggestion: let myObj = Object.fromEntries(arr); ...unless I already had properties attached to that object... let myObj = { foo:"bar",   baz:"boo"}; myObj = Object.fromEntries(arr); // original properties are lost Anything that resets myObj would overwrite those initial properties, so the same would go for destructuring... myObj = Object.assign(...arr.map(([key, val]) => ({[key]: val}))); So, in my limited capacity with the language, I haven't found a native way to concisely write the key/val properties of an array over an existing object (short of a loop). If there isn't one (*he says with great melodrama*), then I could see my solution being useful if I could also pass an object... maybe as an optional parameter to the original spread()...? Then test for the presence of the object and route the call to f() to be either of the variety that sends the object, or not. Kind of like ad hoc overloading for the function...?
1591717148

Edited 1591717194
The Aaron
Roll20 Production Team
API Scripter
Tim R. said: Excellent! So let's have some fun. If I want to construct an equivalent to the functionality I thought spread syntax gave us (iterable operations into a function), I can do: const spread = (f, a) => {     for(let i = 0 ; i < a.length ; i++) {         f(a[i]);     } }; const theFunc = (arr) => {     log(arr); }; const doTheThing = () => {     let args = [["foo","bar"], ["qud","zu"], ["q","blue"]];     log(args);     spread(theFunc, args); }; Simple enough in that even though it uses a full for loop, it's wrapped up in a function that simplifies calling and later coding of the same lines. We just have to trust that any function (f) we build will know how to handle its own (a). But I'm not sure how well it handles edge cases or whether it could be expanded to be more helpful/flexible. Then again, maybe I'm just not fluent enough in the language to know if the above couldn't be better achieved through some application of map, filter, reduce, and _.chain could accomplish the same thing. Unless I'm missing something, that's just .map(): const theFunc = (arr) => {     log(arr); }; const doTheThing = () => {     let args = [["foo","bar"], ["qud","zu"], ["q","blue"]];     log(args);     args.map(theFunc); };
1591717553
The Aaron
Roll20 Production Team
API Scripter
Tim R. said: For example, if I wanted to get the key/val pairs from an array into an object, spread syntax has unexpected results with Object.assign . I could use Jakob's suggestion: let myObj = Object.fromEntries(arr); ...unless I already had properties attached to that object... let myObj = { foo:"bar",   baz:"boo"}; myObj = Object.fromEntries(arr); // original properties are lost Anything that resets myObj would overwrite those initial properties, so the same would go for destructuring... myObj = Object.assign(...arr.map(([key, val]) => ({[key]: val}))); So, in my limited capacity with the language, I haven't found a native way to concisely write the key/val properties of an array over an existing object (short of a loop). If there isn't one (*he says with great melodrama*), then I could see my solution being useful if I could also pass an object... maybe as an optional parameter to the original spread()...? Then test for the presence of the object and route the call to f() to be either of the variety that sends the object, or not. Kind of like ad hoc overloading for the function...? .reduce() would probably be my goto for this: const doTheThing = () => {     let args = [["foo","bar"], ["qud","zu"], ["q","blue"]];     log(args);     let asObj = args.reduce((m,o)=>{ m[o[0]]=o.slice(1); return m;      },{}); log(asObj); }; Once we get the updated API sandbox that's currently only on the dev server, it gets even nicer: const doTheThing = () => {     let args = [["foo","bar"], ["qud","zu"], ["q","blue"]];     log(args);     let asObj = args.reduce((m,o)=>({...m,[o[0]]:o.slice(1)}),{}) ; log(asObj); }; In the current API sandbox, the spread operator only for arrays.  The new one supports spread for objects.
1591721907

Edited 1591722048
timmaugh
Pro
API Scripter
The Aaron said: Unless I'm missing something, that's just .map() Duh. Of course. Regarding the reduce statement, I had been trying to get it to work, and your example showed me what I was doing wrong... so thank you! And good that you told me the spread operator doesn't work for the objects, because that was going to be my next try. But I'm seeing a problem... I tweaked the code just slightly to how I intuit things (and added a join() just because that's what I need), but the following: const doTheThing = () => {     let args = [["foo","bar"], ["qud","zu"], ["q","blue"]];     log(args); let asObj = args.reduce((o,a)=>{ o[a[0]]=a.slice(1).join(); return o; },{}); log(asObj); }; ...would construct asObj as: {foo:"bar", qud:"zu", q:"blue"} ...but if I already had properties on asObj, they get lost: const doTheThing = () => {     let args = [["foo","bar"], ["qud","zu"], ["q","blue"]];     log(args); let asObj = { jangly:"things", sprockle:"wackits" };     asObj = args.reduce((o,a)=>{ o[a[0]]=a.slice(1).join(); return o; },{}); log(asObj); // we've lost our jangly things and sprockle wackits }; So, if I have the following: | NAME:    | myObj     |   args | | TYPE:    | (object)    |    (array) | |  | | | | CONTENT: |  a: origA | ["a", newA] | |          |  b: origB    |    ["b", newB] | |          |  c: origC    |    ["d", newD] | |          |              |    ["f", newF] | | | | | ...what's the best way to get an output of: | NAME:    | myObj     | | TYPE:    | (object)    | |  | | | CONTENT: |  a: newA | |          |  b: newB     | |          |  c: origC    | |          |  d: newD     | | | f: newF | | | | Using my "duh" moment from before, maybe map? args.map((v,i,a)=>{ myObj[v[0]] = v.slice(1).join(); }); That doesn't change myObj. What am I missing? EDIT : Found it; I was missing the parens after join. Fixed in the above. Still wondering if this is the best approach? Side Question: What is the empty brace at the end of the reduce() statement?
1591724083
The Aaron
Roll20 Production Team
API Scripter
try: const doTheThing = () => {     let args = [["foo","bar"], ["qud","zu"], ["q","blue"]];     log(args); let asObj = { jangly:"things", sprockle:"wackits" };     asObj = args.reduce((o,a)=>{ o[a[0]]=a.slice(1).join(); return o; }, asObj ); log(asObj); // we've lost our jangly things and sprockle wackits };
1591724236
The Aaron
Roll20 Production Team
API Scripter
Tim R. said: Side Question: What is the empty brace at the end of the reduce() statement? That would be the base object to modify, the initial state of the memo passed into the function. =D
1591728597
timmaugh
Pro
API Scripter
Excellent. You guys give so much help! And I tell you, every time you help, the path of understanding => applying gets shorter. It's neat that the answer to both questions was really the same thing. Now, for one further level of abstraction from this exercise (and a question at the end)... the reduce() verbiage is a lot to type if we can be guaranteed that we have an array of [key,val] pairs. So we could dump that out to its own reusable function: const extendObjectFromArray = (o,a) => { return a.reduce( (o,e) => {     o[e[0]]=e.slice(1).join();         return o;     }, o); }; ...and then supply the object and array when we call it: extendObjectFromArray (myObj, args); We could even curry that into the object itself, if we wanted a more intuitive "extend THIS object using THAT array": let asObj = {     jangly:"things",     sprockle:"wackits",     extendFromArray: (a) => { extendObjectFromArray(asObj, a); } }; // now we could write: asObj.extendFromArray(args); So, a question. Actually, 2 questions. First, is there a problem with the above (an object referencing/passing itself)? Any chance that properties could ever be unavailable (meaning: in the wild, outside of Roll20 sandbox environment where I know the closure persists once the sandbox has spun up)? Second, is there a cleaner way for the object to refer to itself? I thought that within an arrow function "this" wasn't rebound, so it would be picked up by the declaration block of the object, but I can't seem to make that work.
1591729649
The Aaron
Roll20 Production Team
API Scripter
Minor correction: that's not function currying, that's just assigning a value to an object.  Function currying is taking a multi-argument operations and representing it as a chain of functions that each take a single argument and return the next function in the chain, or a final value: const uncurried = (a,b,c,d) => a*b*c*d; const curried = (a) => (b) => (c) => (d) => a*b*c*d; let v = uncurried(1,2,3,4); let v2 = curried(1)(2)(3)(4); let f1 = curried(1)(2); let f2 = f1(3); let v3 = f2(4); let v4 = f1(3)(4); v === v2 === v3 === v4; Curried functions let you build other functions with some of the parameters already specified, among other benefits. Fat arrow functions don't bind this and thus inherit it from the scope where they are created.  In this case, that is the surrounding function scope, not the object you're creating.  Prototypal inheritance is kind of bizarre and hard to understand, and you're better off with the new classes in Javascript, in my opinion.  In your extendFromArray function, this.asObj would be the asObj you are defining, probably... Just to be completely pedantic, that's not actually referring to the object, it's referring to the variable that contains the object.  If asObj was then reassigned something else, it would have a new value.  If you wanted to guarantee your reference didn't break, you'd need to create a closure to contain a reference only that function had, or specifically bind this on that function to that object. That said, I find that way of doing it somewhat unclean.  If you want that sort of behavior, look into creating a class to represent those things would be my recommendation.  You can look at some of the *Op classes in TokenMod for some examples.  or the internet. =D
1591730370
timmaugh
Pro
API Scripter
I stand corrected on currying! I'm still trying to get my arms around the subject. Those examples are extremely helpful. Classes I'm more familiar with from other languages, I was just under the impression that javascript didn't have them (not true classes, anyway). So, of course my hacks are unclean! That's the second time today that I tried to fake some functionality it turned out js already had! {-D Eventually, I'm sure, I will get out of my own way! Now, I'll go check out that script to see how you implemented classes. Thanks for all the help, Aaron! You rock!
1591730531
The Aaron
Roll20 Production Team
API Scripter
heheheh.  Javascript actually has classes you'll likely be very happy with if you're coming from an OO language like C++, Java, or PHP (shudder).  The new sandbox actually supports some VERY nice class concepts around encapsulation and private members and such.  Looking forward to having that in prod. =D