14
$\begingroup$

I noticed C++ has operator overloading syntax with just the operator names:

T& operator +(T& lhs, T& rhs) {
    return /* ... */;
}

However, there are 2 distinct operators that are both ++ and --. One being a suffix and one being a prefix. C++ syntax has no way to distinguish them with the syntax, so instead one of them uses an unused int parameter. But this seems ungraceful.

What would be a good alternative syntax for operator overloading that could unambiguously distinguish unary + with binary + and also distinguish ++x from x++ without hacks such as unused parameters?

$\endgroup$
2
  • 3
    $\begingroup$ The advantage of the C++ style syntax is not having to remember made-up names for all the operators (like __add__ in Python for example), although unfortunately you do end up with some cases like mentioned in this question. $\endgroup$ Jun 29, 2023 at 12:54
  • 1
    $\begingroup$ The evil answer would be T &operator(T &t)++; $\endgroup$ Aug 18, 2023 at 10:25

11 Answers 11

12
$\begingroup$

Rust-style traits

In Rust, operator overloading is implemented using core traits built into the language, which allow the operators to be named, without conflicting with methods which have the same name (e.g., you may want x.add() to be different from the + overload, maybe if x is a set).

This would look something like:

impl core::ops::Add for T {
    fn add(...) -> T { ... }
}

The advantage of this is that, unless the core::ops::Add trait is used in a file, this add method will not be accessable. This allows other add functions to be implemented on T without conflict.

Demo

$\endgroup$
12
$\begingroup$

Using names instead

To rectify this problem, you can use names instead of symbols for the names of operator functions. For example, take a look at Kotlin:

operator fun inc(): X {
    // ++a/a++
}
operator fun plus(other: Y): Z {
    // a + b
}
$\endgroup$
2
  • 3
    $\begingroup$ Python also takes this approach. For example, + is implemented by __add__ and __radd__ (a cooperative protocol is used to check the LHS and RHS types for an implementation, and += can be specialized using __iadd__ (although it does not perform the actual assignment). $\endgroup$ May 17, 2023 at 0:01
  • $\begingroup$ And while Python doesn't have pre- or post-increment operators, if it did I would expect them to have distinct dunder methods, e.g. __preinc__ and __postinc__. $\endgroup$
    – Barmar
    Aug 25, 2023 at 0:33
9
$\begingroup$

Prefix the operator with its syntactic type. Raku allows the user to define custom operators with virtually any string of symbols (or letters, in fact) as names. The ambiguity is resolved by an explicit declaration at function site of which form of syntax the operator takes.

# Prefix ++
sub prefix:<++>($arg) {
  ...
}

# Postfix ++
sub postfix:<++>($arg) {
  ...
}
$\endgroup$
8
$\begingroup$

The arguments could be placed next to the operator in relation to their usage, as opposed to in a list afterwards:

(T) ++(T) {
    // Prefix
}

(T) (T)++ {
    // Suffix 
}

(T) +(T) {
    // Unary
}

(T) (T)+(T) {
    // Binary
}

The types would likely have to be parenthesized to avoid ambiguous syntax.

$\endgroup$
2
  • 1
    $\begingroup$ I really like this. Agda does something similar, where the name of a function has underscores in the places that it should expect arguments, so _++ is the postfix function's name and ++_ is the prefix one. You can also go a little bit crazy: _≡⟨_⟩_ is a real function from the Agda stdlib that is quite commonly used. $\endgroup$ May 16, 2023 at 17:39
  • $\begingroup$ "The types would likely have to be parenthesized to avoid ambiguous syntax." I think this is likely only an issue for languages with relatively expressive type systems. I can see it causing a problem if e.g. <> are used to indicate type parametrization. $\endgroup$ May 17, 2023 at 0:03
8
$\begingroup$

UpValues in Mathematica

Mathematica (the Wolfram Language) is based on pattern matching and term rewriting. Functions are defined as rules. For example, when you define a function like this:

f[x_] := x^2

It defines a rule that says "when you see f[x_], replace it with x^2". This rule is associated with the symbol f, and is called a "downvalue" of f.

Mathematica also has "upvalues", where the rule is associated with a symbol in the function's arguments instead of the function's name. For example, if you define:

f[x] ^:= x^2

Then the rule is associated with the symbol x, and is called an "upvalue" of x.

Operators in Mathematica are just syntactic sugar for built-in functions. For example, a + b is equivalent to Plus[a, b]. Downvalues for built-in functions are protected, so you can't redefine Plus directly. However, you can define upvalues for your own symbols.

For example, if you want to define your own complex number type, you can define a symbol Comp, and then define addition and multiplication for it:

Comp[a_, b_] + Comp[c_, d_] ^:= Comp[a + c, b + d]
Comp[a_, b_] * Comp[c_, d_] ^:= Comp[a c - b d, a d + b c]

These rules are associated with the symbol Comp instead of Plus and Times, so they don't conflict with the built-in rules for Plus and Times.

$\endgroup$
7
$\begingroup$

tl;dr

This is legal, runnable Scala code:

import language.postfixOps

object Foo:
  def +(other: this.type) = "binary infix"
  def +                   = "unary postfix"
  def unary_+             = "unary prefix"

Foo + Foo //=> "binary infix"

Foo +     //=> "unary postfix"

+ Foo     //=> "unary prefix"

Scala allows calling arbitrary methods with one argument using infix syntax and allows arbitrary operator characters in method names, therefore allowing arbitrary operators to be defined. Associativity and precedence, however, cannot be freely defined, they are based on the method name.

Scala 2 (Legacy)

Scala mostly doesn't deal with operators at all. There are three rules for method calls which allow Scala to support what "feels like" operators and operator overloading without actually having to deal with special support for operators and operator overloading:

  1. The . for method calls can be replaced by whitespace.
  2. When using the method calling syntax from #1, parentheses for a single argument list with a single argument are optional.
  3. A method whose name ends with : is right-associative.

So, rule #1 means that I can replace

foo.bar(baz)

with

foo bar(baz)

and rule #2 means I can replace that with

foo bar baz

There is nothing special about bar. Any method call expression with a single argument list with a single argument can be written this way.

Consequently, there is nothing special about

a + b

It is simply the same as

a.+(b)

applying rules #1 and #2 from above. + is just a legal name for a method like any other. Scala's rules for identifiers are roughly the same as for other popular languages (alphanumeric, can't start with a number, etc.), except that identifiers can also contain (and be completely composed of) operator characters. When mixing operators and alphanumerics, they need to be separated by an underscore. So, foo is legal ++--::::--++!!!**** is legal, foo_+ is legal.

The third rule means that

a +: b

is actually equivalent to

b.+:(a)

or, more precisely (observing left-to-right evaluation):

{ val __unspeakable_name__ = a; b.+:(__unspeakable_name__) }

This is primarily used to support the standard :: operator for prepending an element to a CONS list. Naturally, in a single-dispatch OO language, that must be a method of the list object, but we still want to write the elements in the order they appear in the list:

val list = 1 :: 2 :: 3 :: Nil
// is equivalent to
val list = Nil.::(3).::(2).::(1)

But what about precedence? And what about unary prefix operators? Okay, that's where my qualification from above comes in, where I wrote Scala "mostly" doesn't deal with operators.

For precedence, the rule is that the precedence is determined by the first character of the operator. I.e. all operators starting with + have the same precedence, all starting with - have the same precedence, and so on. The relative precedence between the starting characters is roughly the "standard" precedence from languages like C or Java, but with some modifications.

For unary prefix operators, Scala does not allow defining new ones. It only allows +, -, ~, and !. Unary prefix operator expressions are translated into method calls by prefixing unary_ to them, e.g.

+foo
-foo
~foo
!foo

is equivalent to

foo.unary_+
foo.unary_-
foo.unary_~
foo.unary_!

Note how the rule that identifiers mixing alphanumeric and operator characters must be separated by an underscore is observed, so these are legal identifiers and can be defined using normal method definition syntax.

Unary postfix operators used to be allowed by simply observing rule #1 from above:

foo bar

is equivalent to

foo.bar

They are not technically deprecated (yet!) but their use is heavily discouraged. In current versions of Scala, they require an explicit import (or compiler flag), and will generate a compiler warning even if explicitly enabled.

There are a couple of other rules in order to support common use cases for operator overloading:

  • Function application syntax: if there is no method named foo in scope, then foo(bar) translates to foo.apply(bar). This is used extensively for Factory methods, e.g. Scala has no literal syntax for collections, instead, all collections have companion objects with an apply method, which allows you to construct e.g. a List with List(1, 2, 3, 4). This is also used for array and map indexing, treating arrays as functions of their indices and maps as functions of their keys (in fact, arrays and maps literally inherit from Function).
  • Update syntax: foo(bar) = baz translates to foo.update(bar, baz). Not used extensively, as the Scala community eschews mutability.
  • Property syntax: If there is no mutable field (var) named bar, then foo.bar = baz translates to foo.bar_=(baz).
  • Assignment operators: +=, -=, etc. are just normal method names, so they can be implemented, i.e. a += b translates to a.+=(b). However, if there is no method named += but the expression a = a + b type checks, then that translation is chosen. This means += can be overloaded, but var n = 0; n += 1 still works as expected even though Int doesn't define +=.

There are some other "magic methods", but they are not related to operators, specifically:

  • for comprehensions de-sugar into calls to foreach, map, flatMap, and withFilter.
  • Pattern Matching de-sugars into calls to unapply and unapplySeq.

An interesting tidbit is that infix notation is also valid for type constructors. Application of a binary type constructor C[A, B] can be written as A C B. This is rarely used but can be useful in the context of type-level programming. For example, the type-constructor =:=[A, B] compiles IFF A and B are equal, and it should be obvious why being able to write it as A =:= B is desirable.

Scala 3 (Current)

Some of the rules above have been tightened up in Scala 3:

  • It is always possible to call a method using infix syntax by enclosing it in backticks, i.e. foo `bar` baz and foo `+` baz are always allowed and are equivalent to foo.bar(baz) and foo.+(baz).
  • It is still always possible to call a method whose name consists exclusively of operator characters using infix notation, i.e. a + b is still allowed.
  • Methods whose name contains alphanumeric characters can only be called using infix notation if they are explicitly marked infix at the definition site: infix def foo(bar: SomeParameterType): SomeResultType. Operator methods don't need an infix modifier.
  • For operator methods, it is encouraged to use the @targetName annotation which allows you to choose a human-readable name for the method in the actual compiled code. This is especially useful if the name is illegal in the underlying target platform (e.g. the JVM). For example, @targetName("prepend") def ++:(other: SomeParameterType): SomeResultType will be compiled into a Java method named prepend whereas without the annotation it would be something like $plus$plus$colon, which would be the name you need to use if you want to call this method from Java, Kotlin, Ruby, or any other JVM language.

But the fundamental idea of trying to, as much as possible, treat operators as methods like any other, just with funky names, remains.


Disclosure: this answer was copy&pasted to https://langdev.stackexchange.com/a/2768/854

$\endgroup$
0
6
$\begingroup$

The Operator Is An Argument

In D, a few special method names are reserved for operator overloading, including opUnary and opBinary, and they are used as templates to which the operator is passed as argument.
https://dlang.org/spec/operatoroverloading.html

This means that, for example, +x is interpreted as x.opUnary!"+", and x + y as x.opBinary!"+"(y).
This has the major advantage that multiple syntactically related operators can be implemented by a single function template.

struct S
{
    int x;
    S opBinary(string Op)(in S o) const
    {
        mixin("return S(x", Op, " o.x);");
    }
    
    unittest
    {
        const x = S(21);
        const y = S(2);
        const z = x * y;
        assert(z.x == 42);
    }
}

Another example is that of the cast operator, implemented as opCast where the type is passed as argument.
Combined with CTFE and user-defined overload resolution, it acts similarly to the previous example.

struct S
{
    int x;

    T opCast(T)() const
      if(is(int : T))
    {
        return x;
    }
    T opCast(T: string)() const
    {
        return imported!"std.conv".to!string(x);
    }

    unittest
    {
        const s = S(42);
        const x = cast(long) s;
        const str = cast(string) s;
        assert(x == 42);
        assert(str == "42");
    }
}

x++ calls ++x

What would be a good alternative syntax for operator overloading that could [...] distinguish ++x from x++ without hacks such as unused parameters?

x++ really means (in GNU C's syntax) ({ __auto_type _tmp = x; ++x; _tmp; }).
This is also how both prefix and postfix (inc|dec)rementation operator in D resolve to opUnary.
Naturally, any copy and related semantics can be optimised away via copy elision.

$\endgroup$
5
$\begingroup$

In Ruby, binary infix operators are just syntactic sugar for message sends, i.e. for all binary infix operators ω,

a ω b

is syntactic sugar for

a.ω(b)

Consequently, since operator expressions are just message sends, they invoke methods just like any other message send and just like any other message send, the name of the method being invoked is the same as the message.

So, defining an operator is just defining a method with the same name as the operator. Defining a method named plus looks like this:

module SomeModule
  def plus(other)
    # do something with `self` and `other`
  end
end

And thus, defining a method named + looks exactly identical:

module SomeModule
  def +(other)
    # do something with `self` and `other`
  end
end

There are, however, some special cases.

Firstly, not all operators can be defined. . (message sending), = (assignment), && (logical-AND), || (logical-OR), and, or, unary prefix & (unroll an object responding to to_proc into a block), and unary prefix defined? don't translate into message sends and thus cannot be defined. Unary prefix not cannot be defined on its own, but it translates into a message send of !, which means it is always identical to unary prefix ! which can be defined.

Second, this scheme obviously only works for binary infix operators. Ruby does not have unary postfix operators, but it does have some unary prefix operators. The ones which can be defined are: +, -, !, and ~.

Since there is no binary infix operator named ! or ~, these are just syntactic sugar for argument-less message sends with the same name as the operator and thus are defined with the same name, similar to binary infix operators:

not a # is same as
!a    # is same as
a.!
# is defined using
module SomeModule
  def !
    # do something with `self`
  end
end

~a # is same as
a.~
# is defined using
module SomeModule
  def ~
    # do something with `self`
  end
end

In Ruby, there can be only one method with a particular name in a particular module, so it is not possible to define unary prefix + and - as a parameterless method named + or - since that would clash with the binary infix versions. So, for these two operators, the rule is slightly modified in that they are translated to +@ and -@, respectively:

+a # is same as
a.+@
# is defined using
module SomeModule
  def +@
    # do something with `self`
  end
end

-a # is same as
a.-@
# is defined using
module SomeModule
  def -@
    # do something with `self`
  end
end

So, long answer short: the way to disambiguate operators with the same name but different arity is to give them different names when defining them.

$\endgroup$
0
4
$\begingroup$

In Python the methods become special only because of its name.

For example for +:

def __add__(...):
    ...

Python like methods names:

  • + -- __add__
  • - -- __sub__
  • * -- __mul__
  • / -- __div__
  • ...
$\endgroup$
1
$\begingroup$

There is a thing called extension methods in C#. You could add a NewMethod into an ExistingClass using this:

public static ReturnType NewMethod(this ExistingClass ParamName,...) {
    ...
}

It also has to be in a static class, which is only because they don't want functions outside classes in C#, unimportant to the discussion.

If we are borrowing this this grammar, we could come up with some straightforward ideas. In the class the operator is used upon, we could use:

class Type{
    ReturnType operator++(){
        //postfix ++
    }
    static ReturnType operator++(Type x){
        //prefix ++
        //May not be best.
        //It has ambiguity without the C# assumption that every function has a "this".
    }
    static ReturnType operator++(this, Type x){
        //prefix ++
        //Alternative, denoting "this" is empty.
    }
}

Outside a class, we could use:

ReturnType operator++(this Type x){
    //postfix ++
}
ReturnType operator++(Type x){
    //prefix ++
}

It's like we are using the prefix ++ like a standalone function and the postfix ++ like a method, imitating the order of the object and the operator in the two cases.

$\endgroup$
1
$\begingroup$

Using the attribute system to define a prefix or postfix attribute. Distinction from binary operators should and commonly happens over the type system simply by checking the operand count.

The very definition can reuse the syntax of functions replacing the identifier token with an operator token (example).

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .