Skip to content

14. Complex Multidimensional Object (pCMO.js)

Pimp Trizkit edited this page May 14, 2018 · 9 revisions
<< Previous        Back to Table of Contents        Next >>

    “We are all part of the same rainbow. We are all reflections of each other. As unique and diverse as we are in character and skills, the source of all creation is as multidimensional as we are.” ― Suzy Kassem, Rise Up and Salute the Sun: The Writings of Suzy Kassem

This one is complex. Long story short, its like a multidimensional array. But not one with set lengths and dimensions. As it is not actually a JavaScript Array. It is fully dynamic, instant run-time, and simulates pseudo-infinite length arrays at any dimension and a pseudo-infinite number of dimensions.

This is effectively not much difference than making your own object and putting more objects inside, ad-infinitum. But now with syntactical sugar. Its is kind of like a mixture of two alias, one shim, and a couple of grains of custom code. On its default usage just call x = pCMO() and the object it creates can then be used like a multidimensional array, like x[4][5][2] .. without declaring those dimensions, nor their lengths.

But this is not all that complex right? Right. So lets keep going. When you run x = pCMO(); there will be only one dimension (and node) and it will be empty, or unset. As soon as you attempt to access a value in any dimension such as running this line: alert(x[5][3]); It will first create x[5], then create x[5][3], then return x[5][3] (to alert). Which right now, is an empty object/node. If you run x = pCMO(); then x[3][2][5] = "foo"; It will first create x[3], then create x[3][2], then it runs x[3][2][5] = "foo"; such that x[3][2][5] is a string object and not a node object. Therefore, if you later run alert(x[3][2][5]); you will get a "foo" popup. And if you use any other indexing combination for x in this alert, it will create those nodes and return an empty node and the alert will pop-up [object Object]. Note, since x[3][2][5] is now a string object ("foo") and not a node object, then this is the last dimension/node on this branch. The string object is not an actual part of the CMO (Complex Multidimensional Object), except that the CMO holds the string object. For example, assuming that x[3][2][5] = "foo" then trying to access x[3][2][5][0] will result in accessing the string "foo" at index 0 which will yield f. Therefore x[3][2][5][5] will be undefined and x[3][2][5][5][1] will crash as well as x[3][2][5][0][1][2]. Strings make things weird, as they are arrays of characters. So then that array is actually the last dimension. If instead we ran: x[3][2][5] = new Object(); then x[3][2][5][0] would be undefined and x[3][2][5][0][1] will crash.

But that is also not very complex. Hold on, I'm just getting started. Let's take two steps back before we take a step forward and look at the internals a little. First, pCMO uses the new Proxy object. Therefore, when looking at a node, it will act just like an empty object except that it will print as Proxy in the console. Proxy allows for some simple (boxed) metaprogramming, without dealing with prototype or Object statics. In this case the metaprogramming is hijacking the get and set for the node object, in which it creates the needed nodes as called for, on-demand. Since these created nodes are just regular objects then the members it creates are just regular object members/attributes as is in javascript. In my examples here I use integers with pCMO (like arrays do). Therefore when using a node, it will have indexing similar to an array. In our most recent example above, the root node will look like this (but each node wrapped in Proxy): {3:{2:{5:"foo"}}}. However, you can use any identifier here, not just integers. Long story short, if you manually add any new members to a node, don't use the same scheme as used for the CMO for their names. There is also a symbol-addressed member that empty nodes have. More on that later but it is invisible in normal object usage and only exists on empty nodes.

Symbols? What are those? If you are asking this question... let Google answer it. You will need a bit of knowledge on this one. But more important, you will need knowledge of the Proxy object. And when the get is called from our proxy it will be sent different Symbols when javascript is trying to convert an object for string concat or maybe use it as a number in an expression. One of these symbols is utilized in pCMO, the toPrimitive, look it up. But basically, you can give it a new function to run when this conversion is asked for by javascript. This new toPrimitive function takes one argument and the this is bound to the underlying object. The one argument is hint and helps to tell your function how to output the data (ie: number, or string, or default). Anyhow, to try to summarize, pCMO has aliased this behavior behind a little filter. You can pass into pCMO a function and pCMO will run that function for the toPrimitive call when the node is empty. This function is stored in the node under a unique symbol. It is run from there when needed if the node is empty but if an assignment is made to the node in the CMO then it will delete this attribute function (which marks the node as non-empty). When the node is non-empty, it will just pass thru and return the node object as default. Moving forward, this will allow you to simulate pre-filling the multidimensional array, as well as format it correctly for strings or numbers or objects, not to mention you can write whatever complex code you want in this function, and it will only run and output when the contents of an empty node are needed. For example, you can pass in ()=>42 and it will seem like the entire multidimensional array is pre-filled with the number 42. Note, this will run EVERY TIME an empty node is accessed, and each execution could be completely unique if you desire to design something like that. These results are not injected into the CMO and are not otherwise saved. Anyhow, since I mainly use this to simulate pre-filling, I will not call it the toPrimitive function but rather the pre-filling function. So, using our last example, and with adding in the ()=>42 function for pre-filling... then these next statements will pass assert() => x[3][2][5] == "foo" and x[3][2] == "[object Object]" and x[4] == 42 and x[3][4] == 42 and x[3][2][6] == 42 or anything unset will return a freshly made instance of 42.

A little bit further on that. An example of a slightly more complex pre-filling function: (hint)=>{if (hint == "string") return "Forty Two"; if (hint == "number" ) return 42; if (hint == "default") return 42;}. Note that in this example the check for number and default are for didactic purposes and not actually needed here. Therefore, only a check for string is needed as such: (hint)=>{if (hint == "string") return "Fourty Two"; return 42;}. Also note that default is used more than you would like. And finally, of course, you could use this to create and return any sort of object as the pre-fill.

Now we are getting a touch more complex. Time to kick it up a notch. pCMO also takes another function as an argument. This function will be run to create the nodes. If omitted, it will default to using the function: function(){return new Object();}. Or rather the shorthand version: ()=>({}). Whenever a node is accessed and does not exist, this function will be run to create that node, on-demand. Therefore your nodes are of custom design as well. Keep in mind that these nodes are wrapped in Proxy so the return from this object creator function is what is passed into Proxy.

Proxy can take any Object, Function, or Array in JavaScript (Therefore, your node generator function will need to perform the same). Making for some interesting complex multidimensional constructs. Like a multidimensional function construct. Or if you use an Array, then it will even better simulate multidimensional arrays, by giving you length based indexes at the node level.

Even tho I described it backwards, the node generator function is actually the first parameter to pCMO and the second parameter is the pre-fill generator function. Both are optional. Use false to skip the first argument. The default node generator function just creates a new Object and there is no default pre-fill function. If this pre-fill argument is not present, it will not simulate pre-fill and it will just return the empty node itself when accessed. Remember, pre-fill does not run on non-empty nodes.

Code:

const pCMO =(cO,cPF)=> {
    let cmo = {
        PFS:Symbol(),
        CO:typeof(cO)=="function" ? cO : ()=>({}),
        CPF:typeof(cPF)=="function" ?  cPF : false,
        P: ()=> {
            let x = cmo.CO();
            if (cmo.CPF) x[cmo.PFS] = cmo.CPF;
            return new Proxy(x,cmo.H);
        },
        H: {
            get: (o,p)=> { if (typeof(p) == 'symbol') { if (p == Symbol.toPrimitive && typeof(o[cmo.PFS]) == "function" ) return o[cmo.PFS]; } else if ( !(p in o) ) o[p] = cmo.P(); return o[p]; },
            set: (o,p,n)=> { delete o[cmo.PFS]; o[p]=n; return true; }
        }
    };
    return cmo.P();
};

Usage:

let a = pCMO();
a[3][6][9] = "foo";   // creates `a[3]`, then `a[3][6]`, then runs `a[3][6][9] = "foo";`
alert(a[3][6][9]);   // The pop-up has nothing but `foo`
alert(a[3][6]);      // The node at `x[3][6]` ( [object Object] )
alert(a[3][6][9][0]); // The pop-up says `f`
alert(a[3][6][9][0][0]); // The pop-up says `undefined`
alert(a[3][6][9][0][0][0]); // This will crash javascript
alert(a[2][5]);      // unset nodes get created on the fly, this will output the new node ( [object Object] )

let b = pCMO( ()=> ( {myVal:42} ) );    // using a custom node creator function
// all tests above run the same except now you can do this:
alert(b[4][3].myVal);    //  prints 42

let c = pCMO( false, ()=> 42 );   // skip custom node generator function, but use custom pre-fill generator
// all tests above run the same except there is no `myVal` and this next example is different:
alert(c[2][5]);  // Now prints 42, instead of the node.

let x = 0;
let d = pCMO( ()=>({name:"Node_"+(++x),count:x}), (hint)=>{if (hint=="string") return "Unset item at "+this.count; return -1;} );  // using both custom generator functions.
d[3][6][9] = "foo";   // no change
alert(d[3][6][9]);   // no change
alert(d[3][6]);      // no change, except that this node has attributes `name` and `count`
alert(d[3][6][9][0]); // no change
alert(d[3][6][9][0][0]); // no change
alert(d[3][6][9][0][0][0]); // no change, crash
alert(d[2][5]); // outputs -1
alert(`${d[2][5]}`);  // outputs "Unset item at <count>"

Return:

A Proxy object wrapping an instance of the object returned from the node generator function. With handlers to allow these features. With an optional redirect for toPrimitive for printing empty nodes.

Params:

pCMO(nO,nPF)

nO = < function > Optional

  • Use if custom nodes are required. This must be a function that returns a new instance of the node.
  • If omitted or passed falsy, it will default to using function(){return new Object();} or rather its shorthand twin brother, ()=>({}).

nPF = < function > Optional

  • Use if you want to simulate pre-filling.
  • This must be a function that returns any value. It will run whenever an empty node is accessed. And instead of the empty node; the result of this function is returned.
  • If omitted or passed falsy, no simulation of pre-fill will occur, and you will get the node returned regardless if the node is empty. Remember, a non-empty node could be the data itself (being stored in the CMO), and not actually a real node in the CMO; this would be the last dimension on this branch. Otherwise, a non-empty node is just a regular CMO node with at least one other node (dimension) inside.

Clone this wiki locally