Description
The only issue that I could find about operator overloading currently #19770, although it's currently closed and doesn't have many details.
Goal
Operator overloading should be used to create datatypes that represent things that already exist in Go. They should not represent anything else, and should ideally have no other function.
New operators should not be allowed to defined, we should only be able to define currently existing operators. If you look at languages that let you define your own operators (Scala, looking at you!) the code becomes extremely messy and hard to read. It almost requires an IDE because operator overloading is very heavily used.
Using an operator for anything other than it's purpose is a bad idea. We should not be making data structures with fancy looking +
operators to add things to the structure. Overloading the +
operator in Go should only be used for defining addition/concatenation operators for non-builtin types. Also, as per Go spec, binary operators should only be used for operating on two values of the same type.
Operators should only operate on and return a single type. This keeps things consistent with how operators currently work in Go. We shouldn't allow any type1 + string -> type1
stuff.
Operators should only be defined in the same package as the type they are defined on. Same rule as methods. You can't define methods for structs outside your package, and you shouldn't be able to do this with operators either.
And last but not least, operators should never mutate their operands. This should be a contract that should be listed in the Go spec. This makes operator functions predictable, which is how they should be.
Unary operators should not need to be overloaded.
+x is 0 + x
-x negation is 0 - x
^x bitwise complement is m ^ x with m = "all bits set to 1" for unsigned x
and m = -1 for signed x
This part of the spec should always remain true, and should also remain true for anything using these operators. Perhaps ^x
may need to have it's own, as there's no good way to define "all bits set to 1" for an arbitrary type, although defining a .Invert()
function is no less readable IMO.
Unary operations on structs would then therefore be Type{} + t
or Type{} - t
, and pointers would be nil + t
and nil - t
. These may have to be special cases in the implementation of operator functions on pointers to types.
Assignment operators should also never be overloaded.
An assignment operation x op= y where op is a binary arithmetic operator is equivalent to x = x op (y) but evaluates x only once. The op= construct is a single token.
This should remain the same just as unary operators.
If we do not permit overloading the ^x
unary operator, this means that we only need to define binary operations.
Issues/Projects aided by operator overloading
#19787 - Decimal floating point (IEEE 754-2008)
#26699 - Same proposal, more detail
#19623 - Changing int
to be arbitrary precision
#9455 - Adding int128
and uint128
this code - Seriously it's gross
really anything that uses math/big that isn't micro-optimized
If I went searching for longer, there'd probably be a few more that pop up
Syntax
What's a proposal without proposed syntaxes?
// A modular integer.
type Int struct {
Val int
Mod int
}
// ==============
// each of the functions would have the following function body:
//
// if a == Int{} { // handle unary +
// a.Mod = b.Mod
// }
//
// checkMod(a, b)
// nextInt = Int{Val: a.Val + b.Val, Mod: a.Mod}
// nextInt.Reduce()
//
// return nextInt
//
// ==============
// In all of these, it would result in a compile error if the types of the arguments
// do not match each other and the return type.
// My new favorite. This makes for a simple grammar. It allows
// people who prefer function calls can instead use the `Add` function.
operator (Int + Int) Add
func Add(a, b Int) Int { ... }
// My old favorite. Abandoning the `func` construct clarifies
// that these should not be used like a standard function, and is much
// more clear that all arguments and the return type must be equal.
op(Int) (a + b) { ... }
operator(Int) (a + b) { ... } // <- I like this better
// My old second favorite, although this looks a lot like a standard method definition.
// Maybe a good thing?
func (a + b Int) Int { ... }
// It can be fixed with adding an "op" to signify it's an operator function, although
// I do not like it because it just reads wrong. Also, looks like we're defining a package-level
// function named `op`... which is not what we are doing.
func op (a + b Int) Int { ... }
// Although at the same time, I don't like having words
// before the `func`... I feel that all function declarations should begin with `func`
op func (a + b Int) Int { ... }
// Another idea could just be to define a method named "Plus", although this
// would cause confusion between functions like `big.Int.Plus` vs `big.Int.Add`.
// We probably need to preserve `big.Int.Add` for microoptimization purposes.
func (a Int) Plus(b Int) Int { ... }
Considering other languages' implementations.
C++
// there's several ways to declare, but we'll use this one
Type operator+(Type a, Type b)
I think C++ isn't a bad language, but there are a lot of new programmers who use it and think it's "super cool" to implement operator functions for everything they make, including stuff like overloading the =
operator (which I have seen before).
I also have a couple friends from college who really enjoyed defining operator functions for everything... no bueno.
It gives too much power to the programmer to do everything that they want to do. Doing this creates messy code.
Swift
static func +(a: Type, b: Type) -> Type
Note that custom operators may be defined, and you can define stuff like the operator's precedence. I have not looked much into how these operators end up being used, though.
C#
public static Type operator+ (Type a, Type b)
Operator functions in C# end up being massively overused in my experience. People define all of the operators for all of their data structures. Might just be a consequence of using a language with many features, though.
Kotlin
operator fun plus(b: Type): Type // use "this" for left hand side
https://kotlinlang.org/docs/reference/operator-overloading.html, actually a really nice read.
Operator functions get used everywhere, and even the standard library is littered with them. Using them leads to unreadable code. For instance, what does mutableList + elem
mean? Does it mutate mutableList
? Does it return a new MutableList
instance? No one knows without looking at the documentation.
Also, defining it as a method instead of a separate function just begs it to mutate this
. We do not want to encourage this.
Open Questions
Which operators should be allowed to be overridden?
So, the mathematical operators +
, -
, *
, /
, %
are pretty clear that if this gets implemented, we'd want to overload these operators.
List of remaining operators that should be considered for overloading:
<<
and>>
(I do not think this is a good idea)|
,&
, and&^
(also do not think this is a good idea)<
,>
,<=
,>=
,==
,!=
(maybe a good idea?)- If we include
<
, we should include==
to prevent the confusing case ofx <= y && y >= x
butx != y
.
- If we include
Overloading equality may be a good thing. big.Int
suffers because the only way to test equality is with a.Cmp(b) == 0
which is not readable at all.
I have left out ||
and &&
because they should be reserved exclusively for bool
or types based on bool
(has anyone ever even based a type on bool
?) and see no reason to override them.
Should we even allow operator overloading on pointer types?
Allowing operator overloading on a pointer type means the possibility of mutating, which we do not want. On the other hand, allowing pointer types means less copying, especially for large structures such as matrices. This question would be resolved if the read only types proposal is accepted.
Disallowing pointer types
- Does not allow mutation
- No need for
nil
checks in operator implementation
Allowing pointer types
- Allows operators to be consistent with the rest of the type's methods.
- ie
*big.Int
is*big.Int
everywhere else, it would be good for consistiency
- ie
- Since it's consistent, it makes it easier to pass into other functions.
- ie Can't pass
big.Int
into a function that takes*big.Int
- ie Can't pass
Perhaps it should be a compile-time error to mutate a pointer in an operator function. If read-only types were added then we could require the parameters to be read-only.
Should it reference/dereference as needed?
Methods currently do this with their receivers. For instance:
// Because the receiver for `NewInt` is `*big.Int`,
// the second line is equivalent to `(&num).NewInt(....)`
var num big.Int
num.NewInt(5000000000000000000)
So should the same logic apply to operator functions?
I'm aware that this will probably not be added to Go 2, but I figured it would be a good thing to make an issue for, since the current issue for Operator Functions is quite small and, well, it's closed.
Edits:
- Added proposal: spec: add support for int128 and uint128 #9455 to list of proposals which could be solved using this instead
- Fixed a clarity issue
- Changed examples to use a modular integer rather than a
*big.Int
since themath/big
package is designed to be used in an efficient way, and added that read-only types would benefit this proposal