satyr / coco

Unfancy CoffeeScript
http://satyr.github.com/coco/
MIT License
499 stars 48 forks source link

Feature: operator overloading #60

Open adrusi opened 13 years ago

adrusi commented 13 years ago

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.

satyr commented 13 years ago

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

adrusi commented 13 years ago

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 commented 13 years ago

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

satyr commented 13 years ago

only changing it on values that have the overloading method

How? Sample code and compilation would be great.

adrusi commented 13 years ago
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.

satyr commented 13 years ago
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)?

adrusi commented 13 years ago

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.

satyr commented 13 years ago

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 commented 13 years ago

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

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

adrusi commented 13 years ago

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 commented 13 years ago

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.

satyr commented 13 years ago

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.

adrusi commented 13 years ago

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.

satyr commented 13 years ago

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
adrusi commented 13 years ago

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 :)

satyr commented 13 years ago

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 ;).

adrusi commented 13 years ago

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

FireyFly commented 13 years ago

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))

satyr commented 13 years ago

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));
FireyFly commented 13 years ago

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

adrusi commented 13 years ago

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

askucher commented 9 years ago

+1