Hacker News new | ask | show | jobs
by softirq 874 days ago
The standard library being designed to allow freestanding binaries makes Zig a much better language for systems software such as kernels, EFI applications, etc. than Rust.

Rust's addition of operator overloading is also a perplexingly bad decision. I don't think there is a single worse design decision in C++ or Rust than allowing someone to redefine what + means on some arbitrary type.

6 comments

> Rust's addition of operator overloading is also a perplexingly bad decision. I don't think there is a single worse design decision in C++ or Rust than allowing someone to redefine what + means on some arbitrary type.

Why is that? Won’t it be more ergonomic than needing to call, say `.add`, when you want to do something clearly similar to addition. It’s just sugar, no?

In Java, there is no operator overloading, so if you want to compare 2 strings by contents rather than their memory addresses, you have to use a `.equals` method. Comparing 2 strings by their contents is the most common use case, so it not being the default is a design flaw (similar to how you need to `break` out of a `case` in most languages).

If you can’t overload the addition operator, then why have a whole language construct that can only be used on primitive integers and floats?

> Why is that? Won’t it be more ergonomic than needing to call, say `.add`, when you want to do something clearly similar to addition. It’s just sugar, no?

Especially since, in a typed language, this function would be desugared at compile time and (probably) aggressively inlined, making it hardly different from the compiler builtins for adding floats and ints. Unless, of course, you're doing something like allocating to concatenate strings.

Really, though, it's more of a developer common-sense issue than a language one.

When writing systems software like a kernel, clarity is far more important than ergonomics. The basic operators aren't functions, they're translated directly into opcodes based on the types of the operands. Operator overloading allows them to be either opcodes or functions, and if you think about the amount of work that goes into a function - saving registers, creating a stack frame, pushing addresses, multiple branches - versus a single opcode, as a kernel developer you really don't want this to be hidden behavior based on whether something has been overloaded or not. In the best case the compiler will optimistically inline multiple instructions, in the worse case it will call a function before you've even set up a stack.
SIMD intrinsics are also translated directly into opcodes. There's very little reason to have floating point ops represented by special syntax rather than ordinary functions like fadd() fsub() fmul() fdiv() fmuladd(), other than mere legacy. (And they might not even be simple opcodes in a soft-float implementation.) I mention floating point specifically because that's where even something basic like the order of operations can affect the outcome, so the extra precision actually matters.
What if your target cpu doesn't have, say, floating point operations, or integer division. Will Zig refuse to compile any code that uses these operations? Will it inline the emulation code? Or gasp generate a call out to a library function?
This is an extremely common issue to deal with when creating a kernel, which is, again, why levels of indirection in code and hiding context only makes life more difficult for a systems developer. The kernel may need to enable accelerators, switch between arm and thumb, enable an fpu, clear cache lines, etc. A lot of decisions will be made by the compiler, but based upon parameters passed in at build time, and a lot of it will be arch specific code interwoven in assembly. And there are tons of times when a compiler will generate things you don't want, forcing you to add pragmas and so forth.
Thing is, Rust has full support for hygienic macros so operator overloading could've been added as part of that. You'd just have to write, e.g. int_expr![a + b] or whatever, but that would've made the syntax fully extensible.
Macros are easy to spot, the whole point of operator overloading is that it's a trojan horse. It might do simple addition, it might do a heap allocation and talk to a printer.
Native ints support math operators because math operators are far more readable than method calls. And if you're doing something that requires a custom numeric type, like base-10 floats or fixed-width, method calls don't get any more readable than math operators there either. A language where you can't overload numeric ops is a language with a strong disincentive against using better numerics. And this is without getting into having a single source of semantic equality, whether it be `==` or otherwise; not having that is probably the single worst design decision of Go.

This complaint is always made in the context of some pathological case where a library author tries to do clever things with operator overloading; if any such libraries exist in the first place, you can guarantee that you are not using them by simply not using any libraries with fewer than 10k downloads. The horror stories that fill HN comments pretty much never make it into real code.

See my other comment about the necessity of clarity in a systems language. Also in my opinion readable code is code that is easily understood with as little context as possible. Operator overloading is the opposite, it hides behavior based on the types of the operands. Instead of thinking, that * will turn in to an smul, we now have to check for and read the implementation of * for every type. A function makes it clear that there is a call site and the types of its operands, and the name can more clearly convey the meaning of what is to be done than *.

What you are referring to as readability is actually terseness, which I think is a lousy metric to optimize for, especially for systems software where correctness is important and people will read code a lot more than they will write it.

I get where you're coming from, but I think it doesn't have to be more complicated than: "Are the two values standard Rust number types? If yes, they do simple multiplication on an asm level. Otherwise, check the relevant Mul implementation."
I disagree, consider a generic that is constrained by std::ops::Add. If you want to write generic functions with this type, you have to contend with types that might do simple addition or do allocations with potential side effects.
Why would you constrain a generic with ops::Add, if you didn't want to specifically allow for generic implementations of +? If you just want to be generic over built-in integers, it would be as easy as a "trait Integer: Add + Sub + Mul + TryFrom<i32> + ... {}" that's implemented by the standard integer types and sealed off from outside implementations.
As I understand it, generics are just a particular mechanism for interfacing with multiple types, and C manages without. For the same program, could a programmer ever be more unaware about which types are parameterized and which specific implementations are called in Rust than in C? I don't have much knowledge of C, admittedly, so this isn't a rhetorical question.
The whole point of generics is that you don’t know and don’t care what the type is.
> I don't think there is a single worse design decision in C++ or Rust than allowing someone to redefine what + means on some arbitrary type.

This has perplexed us for D. Experience with C++'s iostream's operator overloading meant running away screaming. Another terrible thing is people would code up DSL's using operator overloading, such as a Regex language. The horror there is the source code looks like ordinary C++ arithmetic, but it is actually doing Regexes.

So, how to allow operator overloading for arithmetic, but not for other porpoises?

1. Only allow overloading of arithmetic operators (i.e. no overloading of unary *) and [ ]

2. Only allow < overloadable, instead of < <= > >=. This enforces symmetry.

3. Don't allow overloading of && || ?:

4. A strong Compile Time Function Execution feature which enables DSLs in the form of string literals

5. Develop a culture of operator overloading is for arithmetic

This has worked well.

> Another terrible thing is people would code up DSL's using operator overloading, such as a Regex language.

There's nothing wrong with this if your language has proper hygienic macros. Then you can have all of math_expr![ … ], float_expr![ … ] (dangerous! order of operations may affect results), regex_expr![ … ], or even stream_concat_expr![ … ] all using the same operators while meaning completely different things and preserving complete extensibility. They would even be composable since each macro invocation would desugar its own operators and leave those in other contained macros unaltered.

Macros form their own hell, hygienic or not. The reason is inevitably these evolve into one's personal, undocumented, quirky, unmaintainable language.
This sounds great if you’re stuck inside the C-derivative mindset block. I.e. the best you can do.
> Rust's addition of operator overloading is also a perplexingly bad decision.

You know, I don't even have a side on this never ending debate... but it's perplexing that similarly intelligent people, with similar interests and backgrounds can both make so confident blank statements such as this, one way or another! IMO that is a pretty good indication that there's no right answer, it's simply a matter of preference... and the fact that people still feel like they're right and the people who disagree with them must be making "perplexing bad decisions" is, for lack of a better word, hilarious.

> Rust's addition of operator overloading is also a perplexingly bad decision.

Yes. The poor man’s custom operators.

Eh, I think operator overloads make a lot of sense in the right context. If you created a type that’s a kind of mathematical object, you want a mathematical syntax for it. However, they are horrible when abused.