Operator Overloading Strawman for Harmony

Mark S. Miller erights at google.com
Thu Jan 8 21:12:45 PST 2009


First, let me acknowledge that there's been a long history of attempts to
add operator overloading to EcmaScript, that these have all died for various
reasons leaving much documentation in its wake, and that I have not read
this documentation at the present time. If I am simply rehashing ideas which
are already well refuted, my apologies for wasting everyone's time. But I
did verbally discuss these ideas with Waldemar, who has lived through this
history, and he thought this was plausible enough to be worth proposing.

Why raise this difficult issue again? We some may remember from the decimal
wars, my motivation in this is to enable library designers to create new
numeric types -- like rational, complex, quaternions, vector, matrix,
surreal numbers, whatever -- so that not every numeric type need come from
the EcmaScript committee. If we do our job as language designers -- of
providing adequate abstraction mechanisms -- then library authors can use
these to build a diversity of abstractions. For efficiency reasons, some
scalar numeric types, like integers and decimal, may still be provided
primitively. But once we have a framework such that these *could* be
provided as a library, then adopting them as directly supported primitives
becomes merely an optimization. (Indeed, efficiency considerations in the
presence of multicore may demand that vectors and matricies be primitives,
but let's not worry about that yet.)

At the request of the rest of the committee, one of Sam's early decimal
attempts did exactly this. It defined a decimal API equivalent to one that
could be provided as a library on ES3.1. ES3.1 has no operator overloading
whatsoever, so we thought we could do this and be downwards compatible from
a future language 9ES4 at the time) which would allow the operators to be
used directly on decimal values. These plans ran aground on "+", which
clarifies what problems need to be solved.


                  Strawman proposal

Define a new nominal type named, let's say, "Operable". (I'm not stuck on
this name. Perhaps "Numeric"?) We don't yet know what nominal type system
Harmony will have, so for concreteness, and to separate issues, let's use
"instanceof" as our nominal type test, where only functions whose
"prototype" property is frozen can be treated as types. (Otherwise,
instanceof is not monotonic.) Let's say that Operable is ES3.1-like function
acting as an purely abstract class (or in Java, a marker interface).
Operable throws if called (hey, it's abstract), it's prototype property is
frozen (non-writable, non-enumerable, non-configurable), and points at an
empty object that inherits from Object.prototype. We redefine the original
Number.prototype so that it inherits from Operable.prototype, so all numbers
are Operable. Crucially, strings remain non-Operable. (I'm dangerously
ignoring here the distinction between primitives and wrappers. In fact, in
ES3 and ES3.1, "e instanceof Number" is false. Fixing the strawman
accordingly presents no problems but makes it tedious. I will continue
ignoring this for now.)

We are concerned only about the "normal" operators, which exclude at least
&&, ||, ?:, ++, --, ., or ===.  For each of the normal operators, we insert
the following tests at the beginning of their definition after all operands
are evaluated and GetValue()d but before they are ToPrimitive()d. For
example, for infix "+" at 11.6.1 The Addition Operator in the ES3.1 spec,
after step 4, taking a few self-hosting notational shorthands:

    Let Left = Result(2)
    Let Right = Result(4)
    If isOperable(Left) and isOperable(Right) then
        harmony code: return Left['+'](Right)
    // else fall through to the current behavior.

To make this work, for each of the operators that operate on numbers, we'd
add a corresponding built-in method to the original Number.prototype. In
self-hosting style:

Number.prototype['+'] = function(arg) {
    if (isNumber(arg)) {
        return primAdd(this, arg);
    } else {
        return arg['reverse+'](this);
    }
};

Number.prototype['reverse+'] = function(receiver) {
    if (isNumber(receiver)) {
        return primAdd(receiver, this);
    } else {
        throw ...;
    }
};

This trick is adapted from Smalltalk, which did something similar. Newer
numeric abstraction understand the build in ones and some number of widely
adopted older ones. But the older ones don't understand the new ones. If X
is a primitive Number and Y and Z are user-defined Complex numbers, then

Y + Z // fine, since complex Y knows what to do with complex Z
Y + X // fine, since complex Y knows what to do with number X
X + Y // number X doesn't know what to do with Y, so it asks Y to handle it
by
          // Y['reverse+'](X), which Y can successfully handle.

What if we compose together two new numeric types, say Rational and Complex,
where each was defined in ignorance of the other. Say R is a Rational.

Y + R // Y is clueless, so it asks R: R['reverse+'](Y)
          // R is clueless, so it gives up rather than re-reversing

This distinction between "+" and "reverse+" also handles the
non-commutativity of some operators.


An elaboration suggested by Mike Samuel:

Rather than have Operable.prototype be empty, since one expects various
operators to have algebratic relationships to each other, we can install
default methods defining some operators in terms of a smaller set than new
numerics will still need to override. For example

    Operable.prototype['+'] = function(arg) { return this - -arg; };

A related issue: Should various internal methods have similar tests for
Operable, and invoke the operable to handle this. Waldemar raised coercion
to boolean. Perhaps ToBoolean(foo), if foo is an Operable, should invoke
foo['!!'] or something. We'd then install default methods for these in
Operable.prototype.

Mike also raised the issue of whether we should split Operable (+,-,*,/,...)
vs Comparable (<,<=,==,>=,>). I find that plausible, given that we have a
nominal type system that can handle non-tree subtype relationships, i.e.,
something other than instanceof.


So, is there some reason why this won't work?

-- 
   Cheers,
   --MarkM
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20090108/90d13224/attachment-0001.html>


More information about the Es-discuss mailing list