coco icon indicating copy to clipboard operation
coco copied to clipboard

Feature: operator overloading

Open adrusi opened this issue 14 years ago • 22 comments

Operator overloading is a great feature of languages like ruby and c++, especially for things like matrix arithmetic. I wrote a function for operator overloading (ironically in coffeescript) that works a lot like ruby's operator overloading where operators are methods.

The code is here.

I don't know much about parsing, and as coffeescript is strongly against special functions, I figured I'd ask here. It shouldn't be too hard to implement if you know what you're doing.

adrusi avatar May 19 '11 20:05 adrusi

I'm all for this if we can find a simple, clever implementation. Some consideration off the top of my head:

  • There is no one clear way to implement it:
    • As method: a <+> b => a['+'](b)
    • As binary function:
      • a <+> b => __['+'](a, b)
        (Would require a method holder for symbols that can't be identifier. Maybe Math?)
      • a <add> b => add(1, 2)
        (Haskell-like. Directly calls whatever lexically available.)
    • Precedence, how do we resolve?
  • It's an esoteric pattern, so:
    • It shouldn't interfere with normal code. This makes replacing every operations a non-option.
    • The outcome should play well with JS world. APIs like Matrix::\+ could read well if we went with the method pattern, but they'd be horrible if used in JS code.

satyr avatar May 19 '11 21:05 satyr

Ok, how about replacing all uses of an overloaded operator within the scope that the operator was overloaded. I don't know if this is possible without executing the code during compilation, but how about only changing it on values that have the overloading method.

adrusi avatar May 19 '11 22:05 adrusi

Oh, and I don't think a binary function would do it, it would prevent specifying only certain objects to have redefined operators.

adrusi avatar May 19 '11 22:05 adrusi

only changing it on values that have the overloading method

How? Sample code and compilation would be great.

satyr avatar May 19 '11 22:05 satyr

Array.prototype["@<<_"] = (val) ->
  @push val
arr = [1, 2, 3]
arr << 4
1 << 3

compiles to:

Array.prototype["@<<_"] = function(val) {
  this.push(val);
};
var arr = [1, 2, 3];
arr["@<<_"](4);
1 << 3;

Again, I have no idea if this is possible, but the function I linked to in the beginning would let you do:

Array.prototype["@<<_"] = function(val) {
  this.push(val);
};
var arr = [1, 2, 3];
operate("infix")(arr, "<<", 4);
operate("infix")(1, "<<", 3);

and the operate function would decide whether to use the real operator or the method.

Precedence could be resolved by parsing operators with the highest precedence first, and those with lowest last.

adrusi avatar May 19 '11 23:05 adrusi

Array.prototype["@<<_"] = (val) ->
  @push val
arr = [1, 2, 3]
arr << 4
1 << 3

Not easy. We don't have a way to infer arr as an Array:: descendant.

operate("infix")(arr, "<<", 4);

Why not simply arr['@<<_'](4)?

satyr avatar May 20 '11 00:05 satyr

operate("infix")(arr, "<<", 4); figures out if arr is an array at runtime, so the compiler doesn't have to do partial execution. Here's the source of operate.

adrusi avatar May 20 '11 00:05 adrusi

figures out if arr is an array at runtime

For what? Your compilation figures out 1 << 3 isn't overloaded on compile time.

edit: Uh I see now. Like I said in the first response, It shouldn't interfere with normal code. It can't slow down all other operations.

satyr avatar May 20 '11 00:05 satyr

Precedence could be resolved by parsing operators with the highest precedence first ...

You mean, using JS operators'? What about custom operators?

satyr avatar May 20 '11 00:05 satyr

I wasn't thinking about custom operators, I don't see what advantage they have over functions, especially as function sin coco don't require parens. You could look into languages like haskell to see how they handle infix function precedence, but I believe that they are just evaluated left to right:

[] <append> 1 <append> 2 <append> 3 <append> 4

is interpreted as

((([] <append> 1) <append> 2) <append> 3) <append> 4

how about overloading of native operators as an option that's off by default. "use overloading"? And maybe having the option be enabled only for the scope in which it was declared.

adrusi avatar May 20 '11 00:05 adrusi

and you are very right to worry about performance of this, I tested

# coffeescript because I already have custom textmate commands for it -_-
for i in [0...10000000]
  i * i

versus

for i in [0...10000000]
  operate("infix") i, "*", i

the second took 11 seconds according to the unix time command, and the first took only 4.5.

I think that having options to enable and disable it for the current scope (with disabled by default) would be useful. I know it might not sound great to have compiler options, but ES5 has them, so there's a precedent.

adrusi avatar May 20 '11 00:05 adrusi

You could look into languages like haskell to see how they handle infix function precedence

Haskell allows you to pick a precedence. http://www.haskell.org/onlinereport/decls.html (4.4.2 Fixity Declarations)

"use overloading"?

Though inelegant, simply replacing every occurrence of operators under that declaration would be easy.

satyr avatar May 20 '11 01:05 satyr

I don't know haskell's syntax, so I can't really understand that document. An elegant way to set precedence would be:

expand <append> after <+> (array, value) -> ...

That might be somewhat hard to parse because you would have to modify operator precedence on the fly.

adrusi avatar May 20 '11 01:05 adrusi

simply replacing every occurrence of operators under that declaration would be easy

Here's a proof of concept: https://gist.github.com/982183

$ cat 60.co
$op = (op, left, rite) ->
  switch op
  case \<< then left.push rite; left
  default ...

let
  "use overload"
  console.log [1 2 3] << 4

console.log 1 << 3

$ coco -r ./use_overload.co 60.co
[ 1, 2, 3, 4 ]
8

satyr avatar May 20 '11 01:05 satyr

That looks great, though I assume that there would be some syntactic sugar for $op, and it wouldn't be a bad idea to use if ... else if ... else instead of switch because it's important to be able to test for the type of a value in addition to the operator being used.

As you said, "use overload" is inelegant, but it's not ugly. I would also suggest something along the lines of "don't use overload" hopefully with nicer wording than that.

It's not quite as elegant of an operator overloading implementation as ruby's, but it's as good a Haskell's, and better than perl 6's infix functions.

Thanks for considering my suggestion :)

adrusi avatar May 20 '11 01:05 adrusi

though I assume that ...

Yup, it's just a quick example. You can tweak away the gist for your need (and reach to a more elegant solution along the way ;).

satyr avatar May 20 '11 02:05 satyr

Ok, I'll see if I can understand the parsing going on.

adrusi avatar May 20 '11 02:05 adrusi

Funny, I thought of a similar idea a few days ago. I was thinking of it in terms of Scala's similar feature, where a b c is shorthand for a.b(c), and operators are valid identifiers (I think that's how it handles it, at least). My conclusion was that the best JS equivalent would be a["b"](c) for when b is an "operator".

I think the best approach to this problem would be to disallow overloading of built-in operators altogether, and simply have a blacklist of pre-, post- and infix operators (i.e. the built-in ones). I think it also clarifies that "these operators are built-ins" when skimming through code.

Regarding operator precedence, I wouldn't bother at all. I'd simply see it as syntactic sugar for regular function invocation. IMHO I'd like the following compilation: a + b ++ c <+> d to a + b["++"](c["<+>"](d))

FireyFly avatar Jun 06 '11 13:06 FireyFly

a + b ++ c <+> d to a + b["++"](c["<+>"](d))

That's actually pretty close to what we already have:

$ coco -bpe 'a + b\++ c\<+> d'
a + b['++'](c['<+>'](d));

satyr avatar Jun 06 '11 13:06 satyr

Oh, that's quite nice actually. I guess I'm fine with that syntax. :)

FireyFly avatar Jun 06 '11 14:06 FireyFly

The problem with the scala syntax is that a b c already compiles to a(b(c)). Also infix functions would need a different syntax than <fn> because 1 <fn> 2 is ambiguous with fn = 5; 1 < fn > 2

adrusi avatar Jun 06 '11 15:06 adrusi

+1

askucher avatar Feb 20 '15 13:02 askucher