Sunday 29 August 2021

Proxying a Function, Callable Attempt and More

As of late I've been again delving into some unfrequent features found in some languages like Groovy and Phyton. One of those features is the "callable" concept in python. Any object is callable (that is, invokable with "()") if it has a __call__ method in its class (or any class up in its inheritance chain). It wrongly came to my mind that we could simulate this feature in javascript by means of creating a Proxy for a function with the apply trap.

I have to admit that I have never used proxies for functions. It's so simple to wrap a function with another function that I've never seen the reason to use the Proxy machinery. So it seemed cool to have found a use case (my main idea behind doing an object callable would be to express with a "myInstance();" call that I want to invoke the main, most important method in that object). Well, if I had fully read the documentation, I would have seen this: The target must be a callable itself. That is, it must be a function object.. So my approach was useless. The curious thing is that you won't get an error when creating the Proxy with the apply trap for a "non function", but when you try to invoke it.


class Formatter{
    constructor(){
        this.defaultWrapText = "||";
    }

    format(str){
        return str.toLowerCase();
    }

    wrapAndFormat(txt, wrapText){
        wrapText = this.defaultWrapText || wrapText;
        return wrapText + this.format(txt) + wrapText;
    }
}

let formatter1 = new Formatter();
console.log(formatter1.wrapAndFormat("HI"));

//let's try to do the formatter1 object a "callable" that would invoke "format". It's like a way to express a "main-default" method in the object 
let callableF1 = new Proxy(formatter1, {
    apply: function(target, thisArg, argumentsList) {
        console.log("invoking as callable");
        return target.format(...argumentsList);
      }
});

//does not work: "caught TypeError: callableF1 is not a function"
// from MDN: The target must be a callable itself. That is, it must be a function object. 
try{
    console.log(callableF1("HeY"));
}
catch(ex){
    console.log(`${ex.name}:\n${ex.message}`);
}

After this I found this article where a very smart guy explains how to do callable objects in JavaScript by inheriting from Function.

The use of a Proxy around the original object/function should be completely transparent to the final user, I already talked about it. For example if I get the name of a proxied function, the name of the target function will be returned. However, there is one case where this "Proxy transparency" seems to fail. If we do a "toString" on a proxied function, we don't get the source of the target function, but a "function () { [native code] }" string, quite odd. Let's see both cases:


function sayHi(){
	console.log("hi");
}

console.log(sayHi.toString());

//output:
/*
function sayHi(){
	console.log("hi");
}
*/

let f3 = new Proxy(sayHi, {
	apply(target, thisArg, argumentsList) {
        console.log("invoking sayHi");
        return target();
	}
});

f3();

//the Proxy is transparent, it returns the name of the target
console.log(f3.name);
//sayHi

//however, toString does not return the source of the target function!
console.log(f3.toString());
//output:
//function () { [native code] }


The behaviour of Function.prototype.bind when applied to a Proxy has quite surprised me. As it returns a new "bound" function, I was expecting that this new function would not be proxied, but it is. So it returns a new Proxy object around the new bound function!


function formatText(txt, str1, str2){
    return str1 + str2 + txt + str2 + str1;
}

let formatProxied = new Proxy(formatText, {
    apply(target, thisArg, argumentsList) {
        console.log("invoking formatText");
        return target(...argumentsList);
	}
});

console.log("- using proxy:");
console.log(formatProxied("Bonjour", "|", "+"));
console.log(formatProxied.name); //formatText

console.log("---------------");
console.log("- after binding");

let boundFormatProxied = formatProxied.bind(null, "Bonjour");
console.log(boundFormatProxied("*", "+"));
//it returns a new Proxy object that proxies the new bound function!!!

// I can see how the name now is "bound formatText"
console.log(boundFormatProxied.name);

console.log("---------------");

// output:
- using proxy:
invoking formatText
|+Bonjour+|
formatText
---------------
- after binding
invoking formatText
*+Bonjour+*
bound formatText
---------------


Notice in the example above that when we bind a function, the name of the returned function is: "bound 'original name'"

No comments:

Post a Comment