Tuesday 21 February 2017

MethodMissing in Javascript

If one compares ES6 to Groovy (that for me is, based on some playing with it years ago, the most expressive language that I can think of) one could spot some missing features, like methodMissing (present in many other languages) or the superclean interception via invokeMethod. The truth is that you can get both features by using proxies and a get trap.

I'll use the term "class" regardless of whether we use ES6 class syntax or the "traditional" style. Proxies looked powerful to me, but I used to see a main problem with them: having to create a proxy for each object for which I want the feature. I'd like to do it directly at the class level. Well, as everything in javascript is an object, you just have to proxy the correct object, the prototype. You'll see that people use this technique to get other features, like multiple inheritance. I've cooked a sample of how to implement method missing.

My first approach was just to wrap "MyClass.prototype" in a proxy and reassign it, like this:

let aux = MyClass.prototype;
MyClass.prototype = new Proxy(aux, {
 //your get trap here
});

This does not work because when using the class syntax, the prototype of the constructor function is neither writable nor configurable, so you can not do the reassignment. To work around it we have to create a subclass using the old syntax so that we can do the prototype reassignment. So if I have an Employee class that I want to do "method missing aware" I'll define a sort of subclass like this.

class Employee{
 //whatever
}

function EmployeeWithMethodMissing(name){
  //Employee.call(this); not allowed to call a constructor other than with new
  let emp = new Employee(name);
  Reflect.setPrototypeOf(emp, EmployeeWithMethodMissing.prototype);
  return emp;
 }
EmployeeWithMethodMissing.prototype = new Employee();
EmployeeWithMethodMissing.prototype.constructor = EmployeeWithMethodMissing;

And I define a function that will receive a constructor function and wrap its prototype in a proxy that adds the method missing ability to it

//manageMissingItem: function that will be called when an access to a missing property takes place
function addMethodMissingCapabilityToClass(classConstructor, manageMissingItem){
 let _proto = classConstructor.prototype;
 classConstructor.prototype = new Proxy(_proto, {
  get: function(target, key, receiver){
   //console.log("get trap invoked");
   
   if (key in target){
    return target[key];
   }
   else{
    //the problem here is that I can not know if they were asking for a method of for a data field
    //so this makes sense for a missing method, but for missing properties it does not
    return manageMissingItem(target, key);
   }
  }
 });
}

The function above receives as second parameter a function where we will define specific behaviours for when a missing method call happens. I use it below to enable synomymous (alias) to a method. I consider calls to the missing methos "tell" or "speak" as if they were calls to "say"

addMethodMissingCapabilityToClass(EmployeeWithMethodMissing, 
  //manageMissingItem function
  function(target, key){
   if (key === "tell" || key === "speak"){
    console.log(`method missing, but ${key} is synonymous with "say"`);
    return function(...args){
     console.log("calling the returned function with " + this.name);
     return this.say(...args);
    };
   }
   else{
    return undefined;
   }
  }
 );
 
 let e1 = new  EmployeeWithMethodMissing("Laurent");

 console.log(e1.doWork("task1", "task2"));
 console.log(e1.say("Hi"));
 console.log(e1.tell("Hi Again"));
 

I've put it all in a gist

This technique works fine when you have a usage sample like the above, you have a restricted set of possible names of the missing methods. If you just want to trap any missing method invokation you have a problem. In javascript method invokation is done in 2 steps. First the function for that method is retrieved (we get it) and then it's invoked (with the corresponding "this" and parameters). This means that in our get trap we can not know if what is being retrieved is a function or a data field. We can not make the distinction that we make in groovy between methodMissing and propertyMissing. If we return a function for just any "missing retrieval", we'll have a problem for those cases where what they were expecting to get were data. The consumer would be expecting for example a string (or undefined) and we are returning a function that will not get invoked, as he was just accessing data only the retrieval happens, but not the invokation.

Trick: return a function with a "missingMethodReturn" property, so we can dosomething like: let data = p.dataField; data = data.missingMethodReturn ? undefined : data; -->

No comments:

Post a Comment