-
Notifications
You must be signed in to change notification settings - Fork 17.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: spec: operator functions #27605
Comments
@gopherbot please add go2 |
I do not like this idea. Because:
|
Of course it does, it's a new feature. All features inevatibly will do this. Although if we're worrying about complicating anything, it should be the spec. Luckily a lot of the rules remain the same, the only things that change are allowing more things to be "operated" together with arithmetic operators.
Again, so does every new feature, and I'd say the learning cost is very small. People typically don't need to write operator functions anyway, as there are meant to be used much more than they are written.
That is a valid critique, but ideally you should not be worried about what it does. It doesn't matter how 1+2 works, you care that it adds properly and you get a 3 in return. Operator functions should not do anything other than what you expect the operator to do. I'd say it actually increases readability as well. Again, look at literally anything that uses |
Also, it's defined the same place that methods are. In the same package as the type itself! So really the question "where is it defined" is a question that we already have the tools to answer. |
I would argue the opposite. The current implementation of the math/big package is confusing and very hard to use. A very big improvement would be if it would just support operators and would act like the base number types. That would make it very easy to learn and use.
The code readability is greatly improved. Again, math/big clearly illustrates that. As for "where is the code defined", that has little to do with readability, and the answer, as is right now, is of course: https://godoc.org/math/big |
Another area that may be improved with such operator functions is the current generics proposal. |
I disagree. Only a small percentage of the math/big api can be replaced with the small set of binary operators defined in the language.
… On 11 Sep 2018, at 17:47, Viktor Kojouharov ***@***.***> wrote:
It also increase the learning cost of the language.
I would argue the opposite. The current implementation of the math/big package is confusing and very hard to use. A very big improvement would be if it would just support operators and would act like the base number types. That would make it very easy to learn and use.
It decrease the readable of the code. It is much more difficult to answer the question "where is the code defined?"
The code readability is greatly improved. Again, math/big clearly illustrates that. As for "where is the code defined", that has little to do with readability, and the answer, as is right now, is of course: https://godoc.org/math/big
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub, or mute the thread.
|
I would prefer not to allocate a potentially large amount of memory for every arithmetic operation, especially if that operation looks like a normal arithmetic operation. The package |
Yes, only a small percentage can be replaced, but what about how often those are used? (Also, this really isn't only for
This is a good critique as well, but when all you want to do is to add numbers without having to worry about overflow, it could be extremely useful. There could also be a separate type defined somewhere else with a less steep learning curve. After all, there is a pretty popular issue (#19623) to make |
Just as a quick demo as to how this could be implemented outside of This could also of course be used for other mathematical constructs such as matrices, vectors, etc. Also for arbitrary-precision integers, a bigdecimals, a elliptical-point, etc. A simple modular integer implementation:
And of course the usage:
|
With operators there are 5 goals:
I propose the following. Operators should be a function calls with functions that are in general:
Comparison is special, it is the "default operator", and maps all of the operators <,>,<=,>=,==,!= Comparison is written like:
Next the user can define also the other operators:
If MyCustomAdd does not match the generic signature If the function is capitalized then the operator can be exported but must be also declared in the remote package, like so:
This satisfies point 2. All functions linked to operator will go thru additional type checking, to detect cases where you write to args and where you return something else than pointer to your own variable (this rule bans returning nil, also bans returning a and b):
Operators that passed the additional type checking, will be internally optimized to something like
Those who did not won't. Next, generic function Maximum:
Can be specially written as:
Now comes the main point: Maximum requires either plain generic function or auto-passed "unoptimized operator". Operators can be auto passed, like this:
And used inside the generic function:
And the best part: compiler takes care of the boring details. |
Could you edit to have your examples in code fences?
would be |
Maybe a simpler syntax is also okay?
Just like a normal methods, but with an operator rune as name. |
To make it sure that operators always return something, and are optimized (by writing it's result to a specific memory location using input argument as output), I revise my proposal to:
If operator is passed to a generic function, the operator function must return it's third argument: Example:
this will work:
this won't:
|
Okay, I think you're micro-optimizing a bit here. The syntax should be simple. Also, as a minor point, you don't need to dereference before using the I'd also like to add that read-only types (I'd link the proposal if I weren't on my phone) would work well with this proposal, as operator functions could require that all inputs are read-only. |
I think forcing redeclaration of operators may lead to an "operator poisoning" of sorts... It would be especially inconvenient for people who use Go for mathematics to use a matrix, vector, and arbitrary-size int, and need to redeclare 50 operators. Calling the comparison operators "default" doesn't make any sense to me. I like the rest of the first part of your proposal, though. The second part not-so-much. |
I am not merely speculating, I have a very serious intention to add operator overloading to my experimental language, named go-li. In fact I plan to do it next week, exactly how I propose. Comparisons: Calling them default is wrong you're right. IMHO Most people would want to map all of them to the same function, in the C language tradition that returns negative int when a<b, zero when a==b, and positive int when a>b. take look at this line:
How do you know that it is operator comparison, rather than parameter named op of type comparison? You don't know. Backwards compatibility is not 100% bulletproof. For this reason at least one of the words has to be reserved keyword, I propose:
or something like this:
or, alternatively, not using "op" but the "map" keyword to map operators.
alternatively, not using any arg names:
|
To prove it, here is example of code that uses generics, and would benefit from operator overloading. Fenwick tree (a data-structure that allows you to quickly sum a continuous sub-sequence of a large array.). The thing you probably disliked, is how we pass operators to generic functions. Trust me this is needed, otherwise people who don't prefer operators will have no way to call into generic functions that use operators. In this way they can, by passing the required concrete function that matches the callback signature as the parameter the way it is. If we don't do this, the use of operators would spread like a disease: |
I'm 100% okay with binding operators to existing function calls. What I don't like is redeclaring the operators in every package that's going to use them. I really would like a new "op" or "operator" TLD. It would make sure source code stays readable. It could still be backward-compatible as it is a TLD and would only be used as one. I think the best syntax would be something like
The only issue is that this would allow
Sure, you could say "just don't allow inlining here", but I feel like that'd be against Go's simplistic design. Perhaps instead preventing that should just be a |
TL;DR Long list of comments:
|
I also don't like the idea of re-declaration. If I import a package that defines operators on its types then I don't have access to those operators? That kinda makes them useless. The whole point of operator overloading is to give them to library consumers. I, as a library implementer, don't care about them and don't really need them. Also, I don't think we should look at C++. It has too much complexity that's very specific to C++ - value semantics, rvalue/lvalue, move semantics etc etc etc. It looks, frankly, ridiculous how it works rights now and committee understands that. |
Operator declaration without overloading is very useful. With this feature decimals can be made usable, complex numbers can be took out of core, even thinking of a flexible number type gets live. |
^ disgusting, error prone, entirely because I can't define +* on vectors All geometry/physics libraries are full of unreadable code because this feature is missing. |
@hydroflame Your example contains a lot of code with little context. Can you provide the example of imagined "non-disgusting, error minimizing" code where you are using the notation? |
what do you suggest to handle precedence @DeedleFake ? |
It was just a random thought, not an actual suggestion, but probably just simple left-to-right would be the least surprising. |
@cosmos72 EDIT: |
If being Operator overloading, if added, will be abused. Not adding it and also not making types that'd benefit greatly from it ( There's no avoiding the trade off. |
many say that operator overloading will be abused to the point that the code is hard to understand. |
In Go today |
My understanding is that it is common to prohibit the use of operator overloading on large projects where code complexity is a concern, for example in game development (allowed for simple implementations) and mars rover environments (strict prohibition). My experience: I have worked on several numerical libraries that deal with quat, vector and matrix operations, the whole lot of it. There has been only one case where I regretted the fact there was no operator overloading in Go, it was when I had to work with the So I'd rather not have operator overloading. It seems to be a language feature avoided by those who practice software engineering and I don't have a need for it to develop numerical libraries. There are other more pressing issues regarding Go in a numerical setting than the lack of syntactic sugar. |
All I want is generic support in operator overloading, with full support for all operations, with generics support. Let me overload any operator that is supported; Type constraints as I have seen them in go don't define these well enough because we are missing critical binary and logical operations, and are not able to do things like using constraints.Unsigned to write generic code that leverages operator overloading for binary operations. Let me do that! Let me program to interfaces so that I can write more dry and solid code by design. |
operator overloading introduces a huge complexity, either in compiler implementation and code maintaibility, which is also the root of the expansion of C++ complexity IMHO. |
I would love to be able to write code that can simply be leveraged with type sets that I've previously defined. Let me use the composition of objects to define what they are. You can't tell me that you want to enable composition over inheritance as a core design constraint, and then tell me that I can't compose objects without begging the question if that violates the design by making custom composition harder. Not to mention, it seems kind of contradictory.. and unfair. The fact is, while operator overloading for things like (+) and (-) might seem odd, for distinct sets of data and various models in some very distinct domains, it makes a lot of sense and enables separation of concerns and DRYing that leads to cleaner code. For example, I should be able to add a Money type to another Money type with (a + b). I'm possibly going to have to make sure the two currencies are the same, or I can enable functionality around exchange rates through. But the fact remains, no matter what I choose to do, it's kind of important that I have full control... if only for compliance and auditing. I fully understand that people shouldn't be messing with operators in ways that could create bad code; But the community can't even agree on what that is half the time. I'm sure we can all point to tech that just seems wrong, but trying to design against bad code will only get you so far. And in many ways, it'll force people to create bad code to get around the artificial constraint that you imposed with good intentions that got in their way while they were just trying to do their jobs. All I'm asking is that, if golang is truly about composition, that we truly go into and enable composition. Let me compose my objects. Not just to define the methods that are a part of an interface, let me define the operators that must be enabled for the interfaces used in the process/strategy I have defined, because they are effectively just function calls that the language is not letting me override right now. The current solution only enables half of the problem; Operator overloading and operator functions would enable solutions for the other half and truly allow people to Go, |
That operator overloading will never happen in GO was one of the very first design strategies. |
I apologize, but as respectfully as possible, I do not believe that opinion alone is worthy of accepting as fact. I'm bringing data and examples and asking questions; people are replying with insults and accusations. It's fine for people to have opinions, and I welcome opinions with data to back them up, but we should be willing to admit when we don't have data to back them up. I'm actually a big fan of DRY and SOLID and personally enforce these in my projects; So any incorrect assertion that this would generate bad code 100% of the time is flat out wrong. I gave a great example above: and I explicitly defined the concerned boundaries. I respectfully understand that if some disagree with me, that's one thing, But you shouldn't be making random arguments without data either, because with all respect, I'm bringing examples and data to you... and it feels like some of you are replying with opinion and insults. Where is the data? |
Looks like you have not tired the complex expressions! |
Given the power of the fmt.Stringer interface in golang, its important to consider that effectively this is another perfect example of why you would want type conversion that you can inject your own code into as a form of operator overloading; in this case it's simply of a string composed of a Strategy taking the prior typed instance as input and returning a string. What's important here is that we can inject that strategy ourselves. The value is that we have control over the code in the resulting callback; and the fact that it's used by so many things grants it even more utility. |
I propose a comprehensive package of enhancements for Go, which includes operator overloading, lambda functions, as well as getter and setter methods similar to C#. In order to control potential side effects - a prevalent concern with these features - my suggestion is to implement operator overloading, and getter/setter methods through lambda functions in Go. These lambda functions should only allow conditional expressions, effectively preventing side effects. This approach could provide a layer of control not currently available in Python due to possible side effects with lambda functions. Furthermore, within these lambda functions in Go, I would suggest disallowing function calls. We should only permit further lambda expressions and constructors. This approach would ensure the purity of the lambda function and maintain a level of simplicity and straightforwardness in the code, which is in line with Go's philosophy. This comprehensive package could open up new patterns and capabilities in Go, while effectively managing the risk of side effects. Each component must be carefully considered and implemented to ensure consistency with the existing language philosophy and objectives. I understand that any change to the language needs to be meticulously planned and executed considering its potential impact on existing code and the learning curve for the community. However, these are my initial thoughts and I'm eager to hear feedback and insights from the community on this proposal. Example: Python code: python
Equivalent Go code (hypothetical, as Go does not currently support operator overloading or lambda-style functions):
Getter Proposal:
in this proposal, Public is a getter function that acts like a public field, providing access to the private field private. You would access it directly, as if it were a public field (bar.Public), rather than using the usual method call syntax (bar.GetPrivate()). Setter Proposal:
func foo(bar *MyStructure, value int) { In this case, Public is a setter method that you're using like a field, to set the value of the underlying private field. |
Stephan Veenbeck presents some commonly expressed concerns about operator overloading: it can make the code difficult to read and hide its functions, leading to confusion and errors. These are valid concerns that should be considered in the discussion of potential enhancements for Go. A potential approach to addressing these concerns might look like this: Clarity and Readability: Use Control: Existing Practices in Other Languages: Community Feedback: It's crucial to note that introducing such a change needs to be carefully thought out and planned to avoid unwanted side effects. Ultimately, any changes in the language should aim to enhance developer productivity and satisfaction without compromising the principles of simplicity and clarity that have made Go what it is. |
There are a simpler solution to the operator implementation. Just allow more characters in the definition of the name of a function similar to what scala does it and allow for infix notation for single parameter functions and do not treat operators in any special way other than the way they are defined. currently: // Defining a + for the private field use currently as: val1.add(val2) new: type MyValue struct { // Defining a + for the private field // Defining a ++ for the private field use new as: this way anyone can add "new operators" such as: ~>() >>>() <--->() etc. it will get interesting for the * and & operators for pointer references. Also := and = for assignment. |
There is two cases in my develop experiences.
This case is simplify for developer: type Point struct {
X, Y int
}
func (p Point) Add(other Point) Point {
return Point{p.X + other.X, p.Y + other.Y}
}
func main() {
p1 := Point{1, 2}
p2 := Point{3, 4}
fmt.Println(p1 + p2) // Output: {4 6}
}
type Block struct {
PrevHash []byte
Hash []byte
}
func (b Block) Compare(other Block) bool {
return bytes.Compare(b.PrevHash, other.PrevHash) == 0 &&
bytes.Compare(b.Hash, other.Hash) == 0
}
func main() {
b1 := Block{[]byte("p0"), []byte("h1")}
b2 := Block{[]byte("p1"), []byte("h2")}
m := map[Block]struct{}{
b1: struct{}{},
b2: struct{}{},
}
} |
At least we could implement a custom optionalChaining operator even though this was dismissed with such an option. Would love to see custom operators They can be limited but having the ability would come a long way. |
I'm a little worried about whether this will turn golang into another python. For example, if we customize a symbol like |
I think it is okay if the operator symbol is not overloaded with existing ones. if you encounter a weird symbol like an infix operator is just another form of method invocation, but it is more readable.
vs
you can know that if it is not a standard operator symbol, then it must be a method NOTE: btw, it is just a matter of preference, I prefer the former, but someone maybe prefer the later |
I don't personally think this serves any good purpose that solves a specific problem. From my understanding, which might be wrong, Okay, fair enough this example from @win-t is fair
Although, I see a fair bit of prons and cons in this one where the cons are more than the pros Pros
Cons
For the fairness of all, I agree, Absolutely right it is. But for numerical types. When you're not dealing with numerical types, how are you gonna know what is that overloaded function is doing under the hood, how are you gonna differentiate easily between primary type and object type operators (no a simple tl;dr; I see 100:1 problems/what it solves on this proposal and bringing much confusion. I'm not hating the concept, I just don't see what value does it serve and how often that value is shown in actual real-life code. |
Hi, just want to clarify
the dot (.), is just an example, it doesn't need to be a dot, we can use any symbol that not distracting, but it's enough to communicate "it is not a normal operator, look up the docs", another alternative might be one may argue, that we will have multiple definitions of the
I can argue, that the correct usage of the custom operators might be not just "numerical" types, but any types that resemble mathematical sense (group, semigroup, monoid, ...). In the end, it is just a matter of using the proper "name" for what purpose: Rob also has a comment about how the Personally, the operator function is not a feature that I miss the most in golang, I can live without it. I'm neutral about this issue, just want to add some comments. But if we want to add to the language, I want it to be defined more like magma (in the mathematical sense). the operand and the result must have same type |
It seems like most of the pushback towards this feature sounds a lot like the typical arguments for and against operator overloading. However, in general, it seems that operator overloading is most beneficial for numeric data types, things like BigInt, fixed-precision numeric values (ex. currency or other uses), Vectors, Matrices), while other types don't really use overloading, except for comparison and equality. I think a good middle ground could be only allowing overloading of operators like comparison and equality, then having Go add support for numeric types like vectors, BigInt, fixed-point, etc. that would have first-class support for standard mathematical operators (though we may want some way to differentiate matrix multiplication vs element-wise, perhaps with an @ operator or some alternative), essentially locking those operators down to being available only on types that the language permits. This seems to give the simplicity, usability, and readability benefits that can come from operator overloading to the most common use case, while avoiding pitfalls of bad operator overloading. Having equality and comparison be overrideable on any struct is valuable since it would allow a single, unambiguous method for equality and comparisons/ordering of all types. The language could even enforce that the operands must be the same type for an equals operator to return true. The fact that there are two different ways to do this (function for structs, operators for primitives) presently makes it less simple than it could be. Arbitrary operator definition would most certainly complicate the compiler, and lead to possible confusion since it could easily result in operators that do not make much sense on their own. I don't see that happening considering that Go wants to keep the compiler as simple as they can. I do, however, think operator overloading is a net positive and would like to see support for overloading existing operators. It seems to me that bad operator overloading implementations generally do not get much traction, with libraries doing this poorly generally being avoided. I very rarely find myself needing to overload anything more than an equality operator on most things in languages with the feature, and I think most devs have the common sense to not needlessly throw it around. I find I end up using libraries that do it well often on the flip side, but those are largely when dealing with numeric or vector types in other languages, and they're a huge benefit to code readability. It seems to me that most code authors have learned the lesson to use operator overloading carefully, which seems to have us to the point where comparison operators are the only ones most override unless you are a numeric type, which led me to the suggestion I made earlier. |
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.
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
orType{} - t
, and pointers would benil + t
andnil - 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.
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
anduint128
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?
Considering other languages' implementations.
C++
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
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#
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
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 mutatemutableList
? Does it return a newMutableList
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?)<
, we should include==
to prevent the confusing case ofx <= y && y >= x
butx != y
.Overloading equality may be a good thing.
big.Int
suffers because the only way to test equality is witha.Cmp(b) == 0
which is not readable at all.I have left out
||
and&&
because they should be reserved exclusively forbool
or types based onbool
(has anyone ever even based a type onbool
?) 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
nil
checks in operator implementationAllowing pointer types
*big.Int
is*big.Int
everywhere else, it would be good for consistiencybig.Int
into a function that takes*big.Int
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:
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:
*big.Int
since themath/big
package is designed to be used in an efficient way, and added that read-only types would benefit this proposalThe text was updated successfully, but these errors were encountered: