2011-12-17

Fake operator overloading in JavaScript

Update 2012-01-29: The post “What is {} + {} in JavaScript?” looks at the addition operator in more detail.

This post describes how to do a limited version of operator overloading in JavaScript. With the technique described here, you’ll be able to implement a type StringBuilder that can be used as follows:

    var sb = new StringBuilder();
    sb << add("abc") << add("def");
And a type Point that can be used as follows:
    var p = new Point();
    p._ = new Point(1, 2) + new Point(3, 4) + new Point(5, 6);
    p._ = new Point(1, 2) * new Point(3, 4) * new Point(5, 6);

Letting operators call methods

One version of real operator overloading works like this: Given the expression
    obj1 + obj2
The plus operator is applied to two objects. This triggers the method call
    obj1.operator+(obj2)
The closest thing you can achieve in JavaScript is fake operator overloading – triggering two method calls:
    obj1.valueOf()
    obj2.valueOf()
Those are made because + only works with primitive values and thus needs to convert obj1 and obj2 to primitives. It does so by invoking their valueOf() method. Fake operator overloading is much less useful than real operator overloading: You don’t get access to both operands at the same time and you can’t influence the value returned by +. We’ll later look at tricks that work around these limitations.

What operators can be used for fake operator overloading? All operators that coerce (convert) their operands to primitives. The following two objects allow you to test which ones do:

    var obj1 = {
        valueOf: function () {
            console.log("valueOf1");
            return 1;
        }
    };
    var obj2 = {
        valueOf: function () {
            console.log("valueOf2");
            return 2;
        }
    };
For example:
    > obj1 + obj2
    valueOf1
    valueOf2
    3
The following binary operators coerce:
    + - * / % 
    & | ^ << >> >>>
    < <= > >=
Equality operators, inequality operators, and boolean operators can work with objects and thus don't coerce. For example:
    > obj1 === obj2
    false
    > obj1 && obj2
    { valueOf: [Function] }
Triggering calls. If its operands are produced by function calls, then a binary operator triggers a total of four function (or method) calls: First, it evaluates the two operand expressions to values (in this case, objects). Then, it converts two objects to primitives. The following function allows us to examine what happens and in which order:
    function func(x) {
        console.log("func_"+x);
        return {
            valueOf: function() { console.log("valueOf_"+x) }
        };
    }
Let’s use it to examine the << operator:
    > func("LEFT") << func("RIGHT")
    func_LEFT
    func_RIGHT
    valueOf_LEFT
    valueOf_RIGHT
Hence: First, the left operand is evaluated, then the right operand. Next, the left value is converted to a primitive, then the right value. It is slightly perplexing to see that valueOf_LEFT does not happen directly after func_LEFT, but it makes sense if you think of function calls, where one also first evaluates the parameters before executing the function body. Some operators even convert their right operands first:
    > func("LEFT") > func("RIGHT")
    func_LEFT
    func_RIGHT
    valueOf_RIGHT
    valueOf_LEFT

Implementing StringBuilder

Recap: We want StringBuilder to behave as follows:
    var sb = new StringBuilder();
    sb << add("abc") << add("def");
    console.log(sb.toString()); // abcdef
Fake operator overloading allows us to do this:
    function StringBuilder() {
        this.data = "";
    }
    
    // Called by <<
    StringBuilder.prototype.valueOf = function () {
        StringBuilder.current = this;
    };
    
    // Used to access the aggregated string
    StringBuilder.prototype.toString = function () {
        return this.data;
    };

    function add(value) {
        return {
            valueOf: function () {
                StringBuilder.current.data += value;
            }
        }
    }
Explanation: The << operator calls StringBuilder.prototype.valueOf which marks the current instance as the “receiver” of subsequent “messages” (by storing it in StringBuilder.current). The messages are sent via add(), which wraps its argument value in an object. When that object is contacted by the operator, it adds value to the current receiver.

def.js – fake operator overloading used for an inheritance API

Tobias Schneider’s def.js is where I first saw the idea of fake operator overloading. def.js uses it to implement a small domain-specific language, giving you a rubyesque syntax for inheritance in JavaScript. Example:
    def ("Person") ({
        init: function(name){
            this.name = name;
        },

        speak: function(text){
            alert(text || "Hi, my name is " + this.name);
        }
    });

    def ("Ninja") << Person ({
        init: function(name){
            this._super();
        },

        kick: function(){
            this.speak("I kick u!");
        }
    });

    var ninjy = new Ninja("JDD");

    ninjy.speak();
    ninjy.kick();
There are two ways to use the API:
     (I) def ("Person") ({...})
    (II) def ("Ninja") << Person ({...})
Therefore, whatever is returned by def() must be both callable (i.e., a function) and a potential fake operand. Furthermore, there are two ways of invoking Person (which has been created by def.js):
  • As a constructor: Then it simply produces a new instance.
  • As a function: Then it must return an object that works as a fake operand.
Expression (II) is evaluated in three steps (simplified for the purpose of this explanation):
  1. Perform the function call def("Ninja") which creates a new constructor Ninja and returns a function D. D could be called, like in (I). In which case it would add properties to Ninja.prototype. But here it is used as a fake operand – def() has added the method D.valueOf() which will be invoked in step 3. D is also stored in a global variable (hidden inside a closure).
  2. Person ({...}) is invoked as a function and stores two values inside D:
    • D.props holds Person’s argument
    • D.super refers to Person
  3. The operator calls D.valueOf() which first lets Ninja (which is still accessible via D’s closure) inherit from D.super and then calls D to add the properties stored in D.props.

Triggering even more calls

func() above allowed us to get four function (and method) calls out of the following expression:
    func("LEFT") << func("RIGHT")
Can we rewrite func() to get even more calls? Yes, we can:
    function func(x) {
        console.log("func_" + x);
        return {
            valueOf: function () {
                console.log("valueOf_"+x);
                return {}; // not a primitive
            },
            toString: function () {
                console.log("toString_"+x);
                return 0; // a primitive
            },
        }
    }
The above implementation of func() gives us six calls:
    > func("LEFT") << func("RIGHT")
    func_LEFT
    func_RIGHT
    valueOf_LEFT
    toString_LEFT
    valueOf_RIGHT
    toString_RIGHT
The trick here is that << internally performs a ToNumber() [1] conversion. ToNumber() first tries valueOf(). If that method does not return a primitive value, it continues with toString(). If toString() does not return a primitive, either, then a TypeError is thrown. Note that ToNumber() only expects the result of either valueOf() or toString() to be a primitive which it then converts to a number. Hence, toString() returning a string is only enforced by convention in JavaScript.

Detecting the operator

If you combine fake operator overloading with a setter, you can detect which operator is used [credit: inspired by an idea of Tobias Schneider’s]:
    var p = new Point();

    p._ = new Point(1, 2) + new Point(3, 4) + new Point(5, 6);
    console.log(p.toString()); // Point(9, 12)

    p._ = new Point(1, 2) * new Point(3, 4) * new Point(5, 6);
    console.log(p.toString()); // Point(15, 48)
This works as follows. Each of the operands of the plus operator is converted via valueOf():
    Point.prototype.valueOf = function () {
        Point.operands.push(this);
        return 3;
    }
This method stores the operand we actually want to use away in a global variable. It then returns 3 (a value that the plus operator can work with). That number is the lowest natural number x for which all of the following expressions produce different results:
    x + x
    x - x
    x * x
    x / x
p._ has a setter that receives the result of 3 being added, multiplied etc., figures out which operator was used and processes Point.operands accordingly.
    Object.defineProperty(Point.prototype, "_", {
        set: function (value) {
            var ops = Point.operands;
            var operator;
            if (ops.length === 2 && value === 0) { // 3 - 3
                operator = this.setSubtract;
            } else if (ops.length === 2 && value === 1) { // 3 / 3
                operator = this.setDivide;
            } else if (ops.length >= 2 && (value === 3 * ops.length)) {
                // 3 + 3 + 3 + ...
                operator = this.setAdd;
            } else if (ops.length >= 2 && (value === Math.pow(3, ops.length))) {
                // 3 * 3 * 3 * ...
                operator = this.setMultiply;
            } else {
                throw new Error("Unsupported operation (code "+value+")");
            }
            Point.operands = []; // reset
            return operator.apply(this, ops);
        }
    });
For addition and multiplication, the number ops.length of operands can be greater than 2. We therefore test the result value of the operator application as follows:
  • Addition: value === 3 * ops.length
  • Multiplication: value === 3ops.length
The full source code of Point is available on GitHub.

How useful is fake operator overloading?

While fake operator overloading is a fun hack, you probably shouldn’t use it in production code: you cannot freely use the result computed by an operator and need side effects (such as storing values in global variables) to access all operands at the same time. Furthermore, many operands need to be wrapped in an object with a valueOf() method in order for this scheme to work. For example, we couldn’t use strings directly with StringBuilder, we had to convert them via add(). But at the very least, fake operator overloading allows you to impress your friends by unusual-looking code.

Related reading

  1. ECMAScript 5.1 language specification, Sect. 9.3, “ToNumber”.
  2. JavaScript values: not everything is an object

9 comments:

Dan said...

I met with similar approach when I was trying to find out how operators work in Paper.js.
Is there any way how to find out which operator called the valueOf function?

Imagine simple Point object and attempt to perform basic math operation. E.g. p1 + p2 = p3 and p3 has sum x and y coordinates, with this approach it would be possible to make it work for one operator but not for all of them (+; -; /; *; ...).

I would not like to wrap whole piece of code into some function or compile it as paper.js does but it's the only solution is not it?

Axel Rauschmayer said...

See the new section “Detecting the operator”.

Paul Grenier said...

Something I found interesting:  When you chain further operators (like func("LEFT") >> func("MIDDLE") >> func("RIGHT");) the additional func calls are not made until the first two have concluded their underlying operator business (bitshifting, modulous, etc.).  So, in the above example you get:
func_LEFTfunc_MIDDLEvalueOf_LEFTtoString_LEFTvalueOf_MIDDLEtoString_MIDDLEfunc_RIGHTvalueOf_RIGHTtoString_RIGHT

Maybe that has some value... just gotta figure out what that is :P

Axel Rauschmayer said...

Yes, interesting. It is associativity at work:
a << b << c === (a << b) << c

Romain Guerin said...

Nice :)

It is the same underlying effect that don't call the method in this example (it stops on the first truthy value) ?
myVar || true || myFunc()

bebraw said...

It's possible to use JSShaper and little AST trickery allow proper operator overloading. See http://nixtu.blogspot.com/2011/05/using-jsshaper-to-provide-operator.html . I believe this is close to what Dart uses these days.

Jon Ege Ronnenberg said...

Typo x + x
y - x <- y should be x (produces 0)
x * x
x / xWhile you make clever use of the different signatures the above produce wouldn't italso be possible to achieve a similar effect by using enumeration values?E.I. Object.defineProperty(Point.prototype, "_", { set: function (value) { var ops = Point.operands; var operator; switch (value) { case valEnum.do : console.log('do -> 1'); operator = this.setSubtract; break; case valEnum.another : console.log('another -> 2'); operator = this.setAdd; break; case valEnum.thing : console.log('thing -> 4'); break; case (valEnum.another+valEnum.thing) console.log('another thing -> 6'); operator = this.setMultiply; break; default: throw new Error("Unsupported operation (code "+value+")"); } Point.operands = []; // reset return operator.apply(this, ops); } });var valEnum = { do: 1, another: 2, thing: 4}Point.prototype.valueOf = function () { Point.operands.push(this); return valEnum.another;}On seconds thought.. no not really.

Axel Rauschmayer said...

Fixed the typo, thanks. Can you format your comment with PRE tags? Then it is easier to read.

Axel Rauschmayer said...

There it is short-circuiting plus associativity. It is very unusual for an operator not to first evaluate all of its operands. || and && are special here. As you mention, || only evaluates its second operand if its first operand is false.

Web Analytics