I know operator overloading is considered out of Zig scope.
But no issue seems to make the following case so here it is.
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?
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.
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.
How should this integrate with error handling and resource management?
Error handling: how do you write x = a + b + c when + is overloaded and fallible?
Example: fn +(a: &const BigInt, b: &const BigInt) !BigInt - i.e., either a new BigInt or an error like error.OutOfMemory.
Resource management: zig doesn't have destructors or linear or affine types. What cleans up the intermediate values in x = a + b + c?
Error aware overloaded operators can either:
x=a+b+c)The choice between these should be left to the user.
Resource management should be left to the creativity of users.
The two extremes are:
immediate destruction
When x=a+b+c is executed
b+c returns a new value that has the attribute temporary=True a + (b+c) returns a new value and destroys the right operandx = (a+b+x) copy the right operand in x, then destroys the right operandtape style
tapex=a+b+c, any temporaries register itself with the tapetape.clear() to destroy all temporariesThe points raised by @bnoordhuis are good.
If we look at a biased history of programming languages:
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.
Zig provides one way to overload operators, no suprise.
The rest will be inevitably wild, it's normal and needed.
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:
//2*x*y + 3*z + 5y - 8*x
var p = sub(add(add(mul(2, mul(x,y)), mul(3,z)), mul(5,y)), mul(8,x))
Could be rewritten to be more readable using type-namespaced fns:
var p = x.mul(y);
p = p.mul(2);
p = p.add( z.mul(3) );
p = p.add( y.mul(5) );
p = p.sub( x.mul(8) );
which stays pretty clean with current error handling:
var p = try x.mul(y);
p = try p.mul(2);
p =try p.add( try z.mul(3) );
p = try p.add( y.mul(5) catch y );
p = try p.sub( x.mul(8) catch x );
That said, I can see reasons why DSLs in general are desirable. Something like this might be possible to write:
fn ctBigMath(comptime str: []const u8, args: ...) BigInt {
//...
}
//...
var p = ctBigMath( "2*[0]*y + 3*[2] + 5[1] - 8*[0]" , x, y, z);
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:
fn comptime MathDSL(ast: AST) AST { ... }
var p = MathDSL(2*x*y + 3*z + 5y - 8*x) ;
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 x and y then the code could figure out what operations to do to transform the equation into something approaching the try version above.
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.
My personal feeling is that operator overloading is too far into the "hidden behavior" territory to mesh well with zig.
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.
https://github.com/zig-lang/zig/wiki/Why-Zig-When-There-is-Already-CPP%2C-D%2C-and-Rust%3F
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:
const Foo = struct {
x: i32,
z: float,
extend Bar {
pub fn add(parent: *const Foo, self: *const Bar) {
...
}
}
// Foo.Bar.add(foo, bar) <=> foo + bar
Not the world's most elegant syntax and not really ready to go as is (what about foo * 2 for example, if Foo.float.add( ... ) is defined?), and not something you'd want extensively used but maybe there is a way to make it without adding overloading to everything (after all, this is like a poor man's function overloading)
Just something to get the discussion started.
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
(MathStruct in the above example), where of course this namespace could delegate the actual calculations, but the entry point would be non-ambiguous.
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 opoverload block as part of the config passed to the block.
The config could be used to select which groups of operators would be overloaded [(+ - / *) vs (| & ~) vs (== >= <=) etc ], whether to fold from left or right in expressions like (a + b + c), and so on.
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
const Float3 = struct {
x: f32,
y: f32,
z: f32,
};
const Integer2 = struct {
x: i32,
y: i32,
};
const ArrayType = [4]u32;
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 Integer2 case this would mean:
const a = Integer2{.x=1, .y=2} + Integer2{.x=3, .y=5};
std.testing.expectEqual(Integer2{.x=4, .y=7}, a);
const b = Integer2{.x=1, .y=2} * Integer2{.x=3, .y=5};
std.testing.expectEqual(Integer2{.x=3, .y=10}, b);
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 u32 types. This could be solved by indicating in the definition of the struct that you want these arithmetic operators.
How about something like this?
0) An infixop must have two parameters and a return value.
1) They are enforced to be of type @This().
2) Only the parameters are in scope. No userspace definitions from outside the block are accessible. If @This() uses a non-core definition internally, it is a compile error.
3) All @core definitions are accessible, except @import, and @cImport.
4) No mutation of parameters is allowed.
5) <opname>= is automatically defined.
6) <opname> must be one of +, -, *, /, %, +%, -%, *%.
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.
0) A prefixop must have one parameter and a return value.
1) They are enforced to be of type @This().
2) Only the parameter is in scope. No userspace definitions from outside the block are accessible. If @This() uses a non-core definition internally, it is a compile error.
3) All @core definitions are accessible, except @import, and @cImport.
4) No mutation of the parameter is allowed.
5) <opname> must be -.
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 + read from a file, as the type information would not be imported inside the infixop/prefixop's scope.
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
Only the parameters are in scope. No external definitions are accessible.
Can you specify a bit more? mycustomconstant is not available, but @sqrt would be?
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 const or var, imported or otherwise. As @import and function definitions are/(will be) expressions, this should sufficiently sandbox the operators to non-importing @core definitions within the infixop / prefixop blocks. Combined with readonly parameters, we've got enough to do math with, and I _think_ nothing else. I've edited the post to clarify.
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. :thinking: )
Most helpful comment
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.