I know a lot of people don't like overloading in general because they've been bitten by people abusing it, but I don't think that's where Zig is coming from. Rather, the project places a high value on being able to understand a snippet of code in isolation, particularly the performance implications of that snippet.
Especially in math-heavy domains I don't think anyone is arguing that "vec1.mul(vec2.plus(vec3))" is cleaner code in the abstract (and I know I've personally written pre-processors _entirely_ to avoid having to write that kind of garbage when doing math-heavy code in an environment unfriendly to such syntax), but the function calls make it crystal clear that something non-trivial is happening under the hood.
Do I want to to give up operator overloading in Python? Absolutely not, and to be frank I wish that portion of the language were even more dynamic. Do I care about Zig not having operator overloading? Not in the slightest. It sits at a different point in the language design space, and I'm super excited about it.
> the function calls make it crystal clear that something non-trivial is happening under the hood.
Well, it makes it crystal clear that something non-trivial may be happening under the hood. If I have a vector type which is implemented using SIMD intrinsics, I'd still call its addition operation "trivial", even if it hasn't been blessed by the language as such.
The point that we only know something non-trivial _may_ be happening is definitely fair :) I'll leave the original comment as-is since you've already responded.
While we're nit-picking, a wide vector type implemented with SIMD intrinsics would still have a non-trivial addition as far as Zig is concerned; your specific example really only holds for sufficiently primitive vectors.
My beef with operator overloading is googling symbols and when I can't inspect it with a language server / IDE. In Pycharm/Clion you can jump to the dunder definition of the operator. Haskell lets you get :info on symbols.
Google has gotten much better at handling symbols.
Basically, custom infix operators are super convenient as long as they are auditable and aren't abused.
I for one have literally never been bitten by it anywhere. I get that you could launch missiles with it but ultimately that's true of literally any structured programming.
Allow me to introduce you to C++’s new std::filesystem::path class, which overloads the divide operators "/" and "/=" to concatenate two paths. (Not to be confused with the “+=“ operator, which also concatenates paths, but with subtly different behavior.)
Why did the division symbol get chosen for a concatenation behavior, of all things? Well, I suppose because it looks kind of like how we write paths with slashes, e.g. “dir/subdir/etc”. While I understand how this might be fun aesthetically once you already know the types involved, I find this completely opposite-than-numeric behavior here to be quite ambiguous and unintuitive.
Actually, slashes in filesystem::path is one of the operator overloading uses I really like in the C++ standard library (It’s time-saving, it’s useful, it’s easy to read). What I really don’t like is the IO operators << and >>, which I think is an incredibly horrid language design mistake. (It’s slow in performance, it’s hard to add formatting options, hard to read).
I suspect it might have bitten me if I wasn’t so lucky to encounter “/=“ first, which lead me to read the documentation and discover the different kinds of concatenation (“+=“ and “/=“) and their behavior.
To someone just skimming the code though, it might not at all be obvious that “+=“ and “/=“ both exist as concatenation operators, and their behaviors are different.
But yes it’s an aesthetic preference or opinion; I tend to prefer operators only when their only possible behavior is nearly so obvious that no documentation should be necessary. When that’s not the case, I prefer named functions/methods because they permit being explicit about subtle differences.
There are actually named member functions for those; they're append and concat. Can you guess which is which? I can't. append is actually "append with directory separator", which is weird, because I'd expect append to be a string-like append to match the append function on std::string. Instead concat is a string-like append despite the fact std::string doesn't have a concat member.
OTOH / is an instant mnemonic cue for "append with separator", and + matches the + on std::string.
Yes, I 100% agree with you that “append()” vs “concat()” are certainly no less confusing in their differences than “/=“ vs “+=“ (and the latter are arguably less confusing due to the mnemonic effect you mention).
In fact, to generalize, I think we can fairly say that operator overloads are akin to extremely short function names; in some cases they may work out great when there is not much ambiguity implicit in the underlying problem, but in other cases a longer and more explicitly descriptive name is required to disambiguate. In this case, “append” and “concat” seem quite poor names for different functions, given that they are virtually synonymous and therefore do nothing to describe or distinguish their differences of behavior.
So my claim is that we can do significantly better at resolving ambiguity with carefully named functions (or other approaches) than operators, or tersely/ambiguously named functions (like “append” and “concat”). Of course, this does come at the cost of code verbosity. Just where we should draw the line between too ambiguous vs too verbose, is of course a difficult subjective matter.
I suppose you're comparing against std::format, but the question is why overloading an operator (cout << "foo") would have been necessary vs a regular member function (cout.put("foo")).
Because it had to replace people using a comma inside printf. The member function could only accept one argument cleanly so you would've to chain them like cout.put.put.put
I was once trying to diagnose a performance issue in an algorithm written in Ocaml. Someone had overloaded ** to be (IIRC) 64-bit multiply. I had a momentary “gotcha in 2 seconds!” moment before realising what was happening with that one.
Funny you should mention OCaml, with doesn't have operator overloading! You can only have one function with a given name in scope at any point, that includes operators. Of course you can redefine with `let (+) a b = ...`, but then you have to explicitly open that module where you want to use that redefined operator. That makes it even more clear what's going on.
Operator overloading is hated among programmers who never use math; in that community all uses of "+" are to do something other than addition, usually something that's not even remotely related to addition. Operators end up as one-character infix method names.
I don’t think I’ve ever seen a library overload math operators when they didn’t do what you’d expect. And just because a language feature is abused doesn’t mean it’s bad. Typescript’s generics are Turing Complete, but they’re still great.
Look at it this way - let's say you're a programmer who never uses math. As a result, 100% of your exposure to operator overloading will be, by definition, to cases that have nothing to do with math. While a programmer that uses math will see 1,000,000 good uses and 10 bad uses, a programmer who never uses math will see 0 good uses and 10 bad uses. I saw a similar effect in the Go community where they were debating whether or not the complex type should be removed. It's absolutely crucial for signal processing, but to the rank and file REST-ist, it's just another annoying case to type out in a generic-less language.
I'm using math in a colloquial sense to refer to "mathy math," not "computer science math." So linear algebra would be a leading example, along with stuff like vectors and complex numbers. Accounting math would be another example, because it requires a bignum implementation that doesn't drop decimals like float can.
I thought his post was pretty clear tbh. For example he mentions complex types so that should be an indicator that he's not talking about the kind of high school maths that the average developer can coast along with.
1) Most of (micro-)benchmarking in computing. Of course you want a tiny bit more analysis in that case.
2) Slightly more real-world: Measuring things when you only have approximate rulers, have difficult things to measure (odd surfaces, etc.), or have to calculate from 2nd hand measurements (pictures with rulers, etc.)
By real world I mean code running in production and making someone money. Or a popular open source package. And I'd be interested in which company or project that is and if they have a writeup.
So for those use cases where you will be using a library with custom types anyway, choose one that doesn't overload the operators for those types? This very much seems like a problem on the library- and not the language-level.
Also, in Rust if you don't want to use the overloading, you can still write your code to directly use function calls `a.add(b)`, where `a` is a type that implements the `Add` trait.
I get the truth in that statement. But as a game programmer, I find it interesting the contrast that linear algebra is precisely what is commonly used.
I encountered something similar when I was trying out Nim. In Nim - ^ is the exponentiationoperator, so x^2 means x squared. The way operator precedence is implemented in Nim is that the unary negate (-) takes precedence over the ^, so - 2^2=4. It threw me off quite a bit (coming from a physical science background I assumed it should always be - 4,took a while in my debugging to figure this out). I went to their discord (?) channel and asked about this, some of the people there could not understand how it would be any other way, which just shows that the way someone who largely translates math into code (like myself) things about code very differently then someone who never does this.
Ah, the memories: I once cut the compile time of a multi-million line C++ codebase by over 20%, just by removing one single-line use of Boost Spirit and replacing it with atoi.
I'd rather solve this with code standards around function naming conventions (including symbols as names for functions) than remove a language feature.
I don't override operators often, but when I do it's useful because the operation I'm describing is a really close parallel to other uses and properties of that operation. As an example, it's why I dislike + for list append, + is normally commutative (which list appending definitely isn't) and subtraction is pretty bonkers as a reflected operator on lists.
FWIW, I advocate ~ as array append (a la perl6/raku strings) (and also as wedge product on vectors, * being dot product). It works well with x^y as exponent/repeat, since the rhs is qualitatively different anyway[0], and also gives you (with +) a sum-of-products/dioid/semiring structure for concatenation and alternation on regular expressions. Admittedly, you still don't have a sensible semantics for division, but that's true of plenty of other product operators (even int is iffy - either not closed (div-as-float/rational/etc) or not a multiplicative inverse (divmod)).
0: In particular, exponentiation by natural numbers (or positive integers for things like nonempty lists that deliberately exclude the multiplicative identity) is almost always well defined[1], even if the thing you're exponentiating bears no resemblance to a natural number.
1: Although note than with vectors and dimensional quantities (eg meters), x^2/x^3/etc are all different types: area/volume, bivector, etc.
I get your point. But even in non-mathy business software, data structure access is so much more convenient in languages with an overloadable index operator:
That is a very good point that I hadn't considered, because I was thinking about Go. In Go, the builtin data structures are compiler special-cases, so you get the index behavior on the map structure. Of course, it starts looking like Java for anything more advanced, but the ethos of Go is to not use anything more advanced.
There are more operators than the infix mathematical ones, like increment/decrement (cursors, iterators, custom counters), dereferencing (used for some clever pointer types), etc.
Sometimes there are good reasons to mask complexity.
I think this is the main reason. Outside of math libraries (and maybe strings for concatenation?) operator overloading rarely makes sense. If you're working in a math heavy domain with custom types it's a godsend though. I wouldn't want to work in a language without operator overloading (I've done enough work with Processing and a Quake derived game engine to know that it sucks), but others might have seen it horribly abused
I have been thinking recently that everything should (or rather could, in a special language) be overloadable. And every operator should be treated as a non-first class citizen of a language, '=', '+', and all. The inelegance is granting those operators any privileged status. So the parsing can be in every case dependent on the arguments being parsed: if you're adding numbers, number addition, if you're adding vectors; even more complex behavior could be added for special parsing cases (which I don't even know what could be), say for creating certain algebraic operators with special conditions, maybe something like knuth's up-arrow.
Of course, thought must be employed on the scoping of these parsing changes but since this behavior would be conditional on the argument properties, it is very difficult to see any problems. For example, while adding vectors overloads '+', it is not going to cause problems in other cases (when the arguments aren't vectors), and when dealing with vectors 'vec1+vec2' the programmer will essentially always be thinking about the overloaded operation anyway, it seems absurd a confusion would occur. I should be able to write 'x=5!+3' to mean 'x=factorial(5)+3'.
Allowing contextual meaning (evaluation) of operators, functions, even syntax, allows more compact, expressive language, because we can reuse words, associate their slightly different applications and adapt syntactical behavior to the problem at hand.
Take the usages of the word 'slow' in natural language: it can be a description of current velocity (variable) of an object (context-dependent) "the car is slow", it can be a description of a property of an object (low typical/maximum velocity) "slugs are slow", it can be a verb "please slow down", and so on. Creating new words for each use case is inefficient and disregards the natural close association of their (contextual and algorithmic) meaning.
The benefit is that you don't have to be concerned about hidden function calls. You don't get unexpected function calls or performance hits this way.
If you want your code to run fast, you need to ensure that you know what your program is doing and you're in control, at all times. Manual memory management enables that.
I believe the rationale against operator overloading is not a question of whether the programmer should or shouldn't be trusted, but rather simply about optimizing for code readability.
Ehh??? It's the first point there.
And I don't like all this hate for operator overloading. I prefer to write "vec1 * (vec2 + vec3)" instead of "vec1.mul(vec2.plus(vec3))".