-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Operator Overloading #871
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
Comments
How should this integrate with error handling and resource management?
|
1. Error handlingError aware overloaded operators can either:
The choice between these should be left to the user. 2. Resource managementResource management should be left to the creativity of users.
Additional CommentsThe points raised by @bnoordhuis are good.
Supporting operator overloading in Zig is one step toward this history. Will there be too many ways to do the same thing? This is a non-issue, because we are talking about userland. |
x = a + b + c should be equivalent to x = add(add(a, b), c) How does zig deal with error handling and resource management in that case? PS: I am new to zig and I am also one of those keen on operator overloading. ;) |
My personal feeling is that operator overloading is too far into the "hidden behavior" territory to mesh well with zig. Some thoughts:
Could be rewritten to be more readable using type-namespaced fns:
which stays pretty clean with current error handling:
That said, I can see reasons why DSLs in general are desirable. Something like this might be possible to write:
where ctBigMath, at compile time, performs the parsing of the DSL and produces code equivalent to any of the above examples. And if so, then providing functions in the std library to make it easy to write comptime DSL parsers could be a solution to a lot of things like this without adding anything to the language itself. |
@logzero, @tgschultz I have been playing with some ideas around extending the use of comptime to include an AST rather than just functions and arguments:
I had not proposed an enhancement since I do not have this figured out. The idea would be that, somehow (this is the TBD part!), you'd get an AST and be able to descend on it, modify it and return a valid AST. This would allow almost any rewriting. If you could get the types of Note that this takes an AST, not a string, and could perform full type checking on the elements. It is unclear how to tell the compiler to pass in the AST rather than the argument expression result. |
I guess I am used to seeing operators as nothing more than syntactic sugar for function calls. If it is not a zig thing, so be it. :) |
I know we are all busy, but the rational behind rejection may help future projects. Zig is intended to replace C, but Zig borrows from C++, D and Rust (all of which have operator overloading). Zig doc saying that operator overloading is in the 'hidden territory' is not really an explanation. |
This wiki link articulates a bit more on some core tenets which conflict with operator overloading. These would first need to change before I think we could consider operator overloading. Summarising the key headings which are applicable:
I'll let @andrewrk chime in if there is anything extra worth mentioning. |
Related #427 |
If there are Struct extensions (#1170), could we imagine twice namespaced struct functions in order to avoid overloading? So that "a * b" with a = struct Foo, b = struct Bar tries to match a function with the actual name of Foo.Bar.add(a, b). So:
Not the world's most elegant syntax and not really ready to go as is (what about Just something to get the discussion started. |
Scope limited operator overloading:One approach I haven't seen being explored yet is "scope limited" operator overloading. I imagine it could look like this: pub fn main() void {
const MathStruct = struct{ //
fn add(...) ..
fn sub(...) ...
fn mul(...) ...
fn mul_2(...) ...
// etc
// following naming convention to map to operators.
};
const config = OperatorOverloadConfig.initArithmethic(); // Default init of builtin struct definition
// define x,y,z ...
// notice the `try`, in case any overloading functions throw an error
try opoverload(MathStruct, config) {
const a1 = z*(x + y);
// within the current scope, the above is equivalent to:
const a1 = MathStruct.mul(MathStruct.add(x,y),z);
}
} The problems with operator overloading all arise from the fact that functions are called behind the scenes (creating uncertainty around performance, allocations, errors, infinite loops, ..). With scope limitation like above, at least the reader would get a clear warning to what might be going on while still getting to enjoy the readability and ergonomic benefits of operator overloading. Another benefit here, is that all the overload definitions would be located in a single namespace The main remaining issue here is allocation. I don't know how to approach that yet, but perhaps an allocator could be accessed within the The I also believe the AST could contain information on which function is substituted for each operator, so that IDEs could provide "hovering" information within the code editor, and "go to definition". |
I'm not a big fan of operator overloading but I could see it work if it is only defined for structs/arrays which only have one arithmetic type. Such as these
Note, I didn't include slices as their length is not compile time known and require loops and more elaborate 'magic'. Then the operators would just work element-wise, so for the
Comparison operators don't work with this obviously, as they result in a different type and it's non-trivial to do anything with that. This would be able to automagically translate to SIMD instructions as anything with 4 floats would could be done with SSE for example and AVX for 8, etc. And clang usually manages to do this on it's own. Though I don't know what it'll do for non power of two sizes. The main advantage is that there are still no function calls or user defined functionality with these overloaded operators. One issue I can see with this is that it'll also be 'enabled' for structs for which arithmetic operators may not make much sense. Such as config structs which just happen to contain only |
How about something like this?
const Self = @This();
pub const @'*' = infixop (self, other) Self {
// do whatever via self and other, return something.
}
// 'a * b' and 'b *= a' are now usable.
const Self = @This();
pub const @'-' = prefixop (self) Self {
// do whatever via self, return something.
}
// 'b = -a' is now usable. AFAIKT, this has the following desirable properties:
If I understand correctly, this would even protect from stupidity like having The most I can imagine this being abused is within embedded systems by writing directly to IO memory, but that moves operator overloading from "being a footgun" to "being a hobby". |
@floopfloopfloopfloopfloop
Can you specify a bit more? Two additions: 1: It might be possible to add the following to your list of desirable properties:
2: I think your idea goes well with the scope restricted operator overloading opoverload{ // yes. in this scope, operators WILL call user defined "functions". deal with it
_ = a + b; // call infixop function
_ = 4 + 2; // error. only overloaded operators in this scope (or maybe too restrictive?)
}
_ = 4 + 2; // back to "normal" Not saying that these two additions are aligned with what you had in mind, but these additions together with the points you made would make operator overloading so transparent and controlled that IMO the drawbacks normally associated with operator overloading would not be valid anymore. It would be interesting to see a more real world example though, like complex numbers or matrices. I also think your suggestion on operator overloading would go well with typedef (#5132), because these infixop/prefixop functions could be put in typedef namespaces, making it possible to define custom operators for arrays for example. Typedef operator overloading would HAVE to be scope restricted though, as you could typedef a primitive type that supports the overloaded operator to begin with. |
@user00e00 By "no external definitions are available", I mean no userspace-defined expressions are accessible, i.e. anything assigned to a I don't know if it's possible to have comptime known execution 'time'/ticks. With the above restrictions, I don't see why it would help. (..unless you're trying to comptime-detect infinite loops. That'd be useful generally. 🤔 ) |
The docs say "Zig's SIMD abilities are just beginning to be fleshed out." But can this proposal be done using SIMD |
Why not just make operator overload behave the exact same as functions? This way we would solve all issues related to error handling. If const a = try add(try add(x, y), z); So why not just make this valid: const a = try (try x + y) + z; This is more or less how Rust does operator overloading works with its error handling system, except it uses Edit: |
I absolutely second @williamhCode sentiment. I believe that "no hidden control flow" is then a good thing, if ensure that the code reads the way it executes; it is immediately obvious under which conditions statements may get skipped. I don't see how that would preclude operator overloading. Seeing a If code-flow hidden in the implementation of an operation where a reason to concern, why do we allow the runtime to supply such "hidden" control flow? Depending on the processor, some operations may be instructions or they may be library code supplied by the runtime. Or is it that we make special exceptions for runtime and compiler, but ban the user from such code-flow? Why do we allow users to write functions then? The reason why we allow functions is because abstraction is a good thing. It allows to encapsulate complex operations behind a relatively simple name, and simple pre- and post-conditions. This way, the function user can focus on what's relevant: the "what" of the function call, not the "how". The same is true for operator overloading. If it helps encapsulate details that are not relevant to the caller of the operation, then it's a good thing. I don't see why that should be restricted to an arbitrary limited set of operations. I believe a language should make it easy to write good/correct code while difficult to write "bad" code. There are few areas where operations are as well-defined as those where we use the language of mathematics to specify properties. That language uses operators. IMHO, it would be stupid not to allow to use that language to describe those properties in code. For example, "physical quantities" is a distinct concept of a number (and one that is very dear to me). Many languages are slowly developing tools to map that concept into their space, often via libraries - because users of such libraries know how difficult it is to write correct code that needs to track physical quantities as numbers with a hidden, implicit physical unit. Mapping that concept into the language frees you from having that hidden, implicit unit by making it explicit, and tracking it through the mathematical operations on them. It enormously simplifies writing correct code. Sadly, Zig does ban such a tool outright, on the grounds of an argument that I consider inconsistent in it self, as I outlined above. |
@burnpanck I think the main reason people dislike operator overloading is simply because of such practices as in C++, where they are not used for actually implementing for example arithmetics on a custom types, but to implement various kinds of implicit and/or unexpected behaviour. The canonical example being C++ using |
So the stance of Zig is "if we cannot prevent most/all bad use of a feature, it should be banned outright"? Shouldn't we ban "writing code" then? Which overloads are reasonable and which aren't is indeed a domain-specific question, so if the language tries to enforce that, it basically provides a white-list of "acceptable" domains which will always be very limiting. Why not give the user the tools to define what operations are reasonable? Sure, the feature can be misused, but you cannot prevent users from writing illogical code anyway. Has anyone ever made a list of actual examples of cases where operator overloading has been "misused" and it has become mainstream enough that one has to use those operators? There is of course C++'s |
@burnpanck, no no, I don't speak for Zig at all. I simply offer a possible explanation of how "no hidden control flow / allocations / simplicity" might tie into a rejection of operator overloading. |
Why we can't add operators like |
You should take a look at what Odin lang did. It also doesn't allow operator overloading, but common operations on vectors and matrix types are built in as are those types. It's philosophy is that since it already provides things that 99% of people use operator overloading for there is no need for operator overloading. |
@igor84 zig already supports math ops for https://ziglang.org/documentation/0.13.0/#Vectors
|
No comparison? |
I think the domains that Zig excels in overlap strongly with domains that use vectors. Vector operations are syntactically-challenging without operator overloading. Examples:
This overlap turns the operator overloading omission into something that sounds superficial into a significant downside. |
The current implementations of If hidden control flow is the crux of the issue, then I think having a collection of "function-call" operators that are distinct from the standard arithmetic ones would make for a good compromise, e.g. |
I know operator overloading is considered out of Zig scope.
But no issue seems to make the following case so here it is.
Operator overloading matter in scientific computing
Consider writing polynomial addition with function
p = 2*x*y + 3*z + 5y - 8*x;
assume x, y and z are some other complex polynomials.
With functions, this becomes
p = sub(add(add(mul(2, mul(x,y)), mul(3,z)), mul(5,y)), mul(8,y))
Is the intent clear in the previous line?
Isn't forcing the previous line on user breaking Zig simplicity principles?
Zig promises to make scientific computing easier and fun
When writing numerical code in C++, memory leak is a pain.
With Zig focus on no 'undefined behavior', writing fast numerical can be made much easier.
C++ is currently a big entry barrier in high performance numerical computing.
Why 'printf' and not '+'
Zig brings a new paradigm where function previously hidden in the compiler internal are now in userland.
Why limit this paradigm to standard function call?
Zig offers an all encompassing approach that offers a build system and a package manager.
There are many domain where operator overloading is crucial.
For example, in Fortran '+' is overloaded in the compiler to support vector addition.
If Zig doesn't offer operator overloading, users will either:
These two solutions break many Zig principles.
The text was updated successfully, but these errors were encountered: