Search code examples
javascriptjsontheoryself-referencequine

Is there a way for the value of an object to be made aware of its own key dynamically?


This is a purely theoretical question here (though one I think is an interesting thought exercise). I was just working on a JavaScript object (documentation-related) and the somewhat-unusual thought crosses my mind: is there a way to make a key/value pair entry within said object capable of reading its own key as part of its value? That is to say:

Assuming I have a JavaScript object used for the purposes of serializing data:

{
    "someKey":()=>"M-me? MY key is '" + voodoo(this) + "'! Thanks so much for taking an interest!"
}

...is there a way I can get "M-me? MY key is 'someKey'! Thanks so much for taking an interest!" as an (albeit: rather asinine) output when addressing the key? I totally don't care what the structure would look like, nor what the type of the Value of the portion of the KVP would be, NOR what arguments would need passed it (if any? I'm just assuming it would have to be a function, after all).

I mean, of course it's possible; it's code. It's ALL possible (I've seen a quine that can ascertain its own SHA-512 hash, for heaven sake). But I find it to be an interesting thought experiment, and wanted to see if anyone already had some Code Kung Fu/Source Santeria (even at the abstract/pseudo-code level) and/or someone that might have some ideas.

I've tinkered with going so far as to actually parse the JavaScript source file line-by-line and test for the remainder of the output string to place it (worked, but lame... What if it's a constructed object?), then thought of stringifying it and RegEx-ing it out (worked, but still pretty weak... Relies too much on advance knowledge of what would have to be an unchanging structure).

I'm now fiddling with attempting to filter the object on and by itself to try and isolate the key making the request, which I expect will work (-ish), but still leaves me feeling kind of like the bull in a china shop. I can extend the Object prototype (I know, I know. Theoretical, remember?) so the self-reference doesn't pose a problem, but I'm stumped as to providing a means for the KVP to identify itself uniquely without having to search for some set portion of string.

Anyone have any thoughts? No holds barred: this will probably never see the light of a production environment - just an interesting puzzle - so feel free to muck with prototypes, include libraries, fail to indent... whatever*. Frankly, it doesn't really even have to be in JavaScript; that's just what I'M using. It's 2:30am here, and I'm just noodling on if it's DOABLE.

* (Please don't fail to indent. Twitch-twitch (ಥ∻.⊙) It seems I lied about that part.)


Solution

  • Reflexively lookup the key on call

    This is probably the most surefire way to do it. When obj.foo() is called, then foo is executed with obj set as the value of this. This means that we can lookup the key from this. We can examine the object easily the hardest thing is to find which key contains the function we just executed. We can try to do string matching but it might fail for:

    const obj = {
        foo: function() { /* magic */ },
        bar: function() { /* magic */ },
    }
    

    Because the contents of the functions will be the same but the keys are different, so it's not easy to differentiate between obj.foo() and obj.bar() by doing string matching.

    However, there is a better option - naming the function:

    const obj = {
        foo: function lookUpMyOwnKey() { /* magic */ }
    }
    

    Normally, there is pretty much no effect whether you give the function a name or not. However, the thing that we can leverage is that the function can now refer to itself by the name. This gives us a fairly straightforward solution using Object.entries:

    "use strict";
    
    const fn = function lookUpMyOwnName() {
      if (typeof this !== "object" || this === null) { //in case the context is removed
        return "Sorry, I don't know";
      }
    
      const pair = Object.entries(this)
        .find(([, value]) => value === lookUpMyOwnName);
    
      if (!pair) {
        return "I can't seem to find out";
      }
    
      return `My name is: ${pair[0]}`
    }
    
    const obj = {
      foo: fn
    }
    
    console.log(obj.foo());
    console.log(obj.foo.call(null));
    console.log(obj.foo.call("some string"));
    console.log(obj.foo.call({
      other: "object"
    }));

    This is pretty close to the perfect solution. As we can see, it even works if the function is not defined as part of the object but added later. So, it's completely divorced from what object it's part of. The problem is that it's still one function and adding it multiple times will not get the correct result:

    "use strict";
    
    const fn = function lookUpMyOwnName() {
      if (typeof this !== "object" || this === null) { //in case the context is removed
        return "Sorry, I don't know";
      }
    
      const pair = Object.entries(this)
        .find(([, value]) => value === lookUpMyOwnName);
    
      if (!pair) {
        return "I can't seem to find out";
      }
    
      return `My name is: ${pair[0]}`
    }
    
    const obj = {
      foo: fn,
      bar: fn
    }
    
    console.log(obj.foo()); // foo
    console.log(obj.bar()); // foo...oops

    Luckily, that's easily solvable by having a higher order function and creating lookUpMyOwnName on the fly. This way different instances are not going to recognise each other:

    "use strict";
    
    const makeFn = () => function lookUpMyOwnName() {
    //    ^^^^^^   ^^^^^
      if (typeof this !== "object" || this === null) { //in case the context is removed
        return "Sorry, I don't know";
      }
    
      const pair = Object.entries(this)
        .find(([, value]) => value === lookUpMyOwnName);
    
      if (!pair) {
        return "I can't seem to find out";
      }
    
      return `My name is: ${pair[0]}`
    }
    
    const obj = {
      foo: makeFn(),
      bar: makeFn()
    }
    
    console.log(obj.foo()); // foo
    console.log(obj.bar()); // bar

    Making really sure we find the key

    There are still ways this could fail

    • If the call comes from the prototype chain
    • If the property is non-enumerable

    Example:

    "use strict";
    
    const makeFn = () => function lookUpMyOwnName() {
    //    ^^^^^^   ^^^^^
      if (typeof this !== "object" || this === null) { //in case the context is removed
        return "Sorry, I don't know";
      }
    
      const pair = Object.entries(this)
        .find(([, value]) => value === lookUpMyOwnName);
    
      if (!pair) {
        return "I can't seem to find out";
      }
    
      return `My name is: ${pair[0]}`
    }
    
    const obj = {
      foo: makeFn()
    }
    
    const obj2 = Object.create(obj);
    
    console.log(obj.foo());  // foo
    console.log(obj2.foo()); // unknown
    
    
    const obj3 = Object.defineProperties({}, {
      foo: {
        value: makeFn(),
        enumerable: true
      },
      bar: {
        value: makeFn(),
        enumerable: false
      }
    })
    
    
    console.log(obj3.foo()); // foo
    console.log(obj3.bar()); // unknown

    Is it worth making an overengineered solution that solves a non-existing problem just to find everything here?

    Well, I don't know the answer to that. I'll make it anyway - here is a function that thoroughly checks its host object and its prototype chain via Object.getOwnPropertyDescriptors to find where exactly it was called from:

    "use strict";
    
    const makeFn = () => function lookUpMyOwnName() {
      if (typeof this !== "object" || this === null) {
        return "Sorry, I don't know";
      }
      
      const pair = Object.entries(Object.getOwnPropertyDescriptors(this))
        .find(([propName]) => this[propName] === lookUpMyOwnName);
    
      if (!pair) {//we must go DEEPER!
        return lookUpMyOwnName.call(Object.getPrototypeOf(this));
      }
      
      return `My name is: ${pair[0]}`;
    }
    
    const obj = {
      foo: makeFn()
    }
    
    const obj2 = Object.create(obj);
    
    console.log(obj.foo());  // foo
    console.log(obj2.foo()); // foo
    
    
    const obj3 = Object.defineProperties({}, {
      foo: {
        value: makeFn(),
        enumerable: true
      },
      bar: {
        value: makeFn(),
        enumerable: false
      },
      baz: {
        get: (value => () => value)(makeFn()) //make a getter from an IIFE 
      }
    })
    
    
    console.log(obj3.foo()); // foo
    console.log(obj3.bar()); // bar
    console.log(obj3.baz()); // baz

    Use a proxy (slight cheating)

    This is an alternative. Define a Proxy that intercepts all calls to the object and this can directly tell you what was called. It's a bit of a cheat, as the function doesn't really lookup itself but from the outside it might look like this.

    Still probably worth listing, as it has the advantage of being extremely powerful with a low overhead cost. No need to recursively walk the prototype chain and all possible properties to find the one:

    "use strict";
    
    
    //make a symbol to avoid looking up the function by its name in the proxy
    //and to serve as the placement for the name
    const tellMe = Symbol("Hey, Proxy, tell me my key!"); 
    const fn = function ItrustTheProxyWillTellMe() {
      return `My name is: ${ItrustTheProxyWillTellMe[tellMe]}`;
    }
    fn[tellMe] = true;
    
    const proxyHandler = {
      get: function(target, prop) { ///intercept any `get` calls
        const val = Reflect.get(...arguments);
        //if the target is a function that wants to know its key
        if (val && typeof val === "function" && tellMe in val) {
          //attach the key as @@tellMe on the function
          val[tellMe] = prop;
        }
        
        return val;
      }
    };
    
    //all properties share the same function
    const protoObj = Object.defineProperties({}, {
      foo: {
        value: fn,
        enumerable: true
      },
      bar: {
        value: fn,
        enumerable: false
      },
      baz: {
        get() { return fn; }
      }
    });
    
    const derivedObj = Object.create(protoObj);
    const obj = new Proxy(derivedObj, proxyHandler);
    
    console.log(obj.foo()); // foo
    console.log(obj.bar()); // bar
    console.log(obj.baz()); // baz

    Take a peek at the call stack

    This is sloppy and unreliable but still an option. It will be very dependant on the environment where this code, so I will avoid making an implementation, as it would need to be tied to the StackSnippet sandbox.

    However, the crux of the entire thing is to examine the stack trace of where the function is called from. This will have different formatting in different places. The practice is extremely dodgy and brittle but it does reveal more context about a call than what you can normally get. It might be weirdly useful in specific circumstances.

    The technique is shown in this article by David Walsh and here is the short of it - we can create an Error object which will automatically collect the stacktrace. Presumably so we can throw it and examine it later. Instead we can just examine it now and continue:

    // The magic
    console.log(new Error().stack);
    
    /* SAMPLE:
    
    Error
        at Object.module.exports.request (/home/vagrant/src/kumascript/lib/kumascript/caching.js:366:17)
        at attempt (/home/vagrant/src/kumascript/lib/kumascript/loaders.js:180:24)
        at ks_utils.Class.get (/home/vagrant/src/kumascript/lib/kumascript/loaders.js:194:9)
        at /home/vagrant/src/kumascript/lib/kumascript/macros.js:282:24
        at /home/vagrant/src/kumascript/node_modules/async/lib/async.js:118:13
        at Array.forEach (native)
        at _each (/home/vagrant/src/kumascript/node_modules/async/lib/async.js:39:24)
        at Object.async.each (/home/vagrant/src/kumascript/node_modules/async/lib/async.js:117:9)
        at ks_utils.Class.reloadTemplates (/home/vagrant/src/kumascript/lib/kumascript/macros.js:281:19)
        at ks_utils.Class.process (/home/vagrant/src/kumascript/lib/kumascript/macros.js:217:15)
    */