Remix.run Logo
▲ pron 2 days ago

Also, Zig is as at least as expressive as C++ (but without the implicitness and general language complexity that makes reviewing some sensitive code, where you need to see exactly what operations are performed, hard), and it supports arenas better than C/C++/Rust.

▲positive-spite 2 days ago | parent | next [-]

To anyone interested in arena allocators.

Rust just stabilized its `Allocator` API in version 1.100.0. Definitely worth checking out!

▲Bigpet 2 days ago | parent | next [-]

Seems like a long way to walk though. Retrofitting this will take time (if it'll ever complete). Zig made it a core part of the language and library. I'll assume Rust will make it farther than C++ ever made it, but just take a look at how retrofitting custom allocators is going there.

Seems like it will be useful for use-cases like embedded where you're restricted to certain parts of the library anyway. But the battle is kind of already lost for support at the level of Zig or Odin. Even if they retrofitted support for the whole standard library, they'll never be able retrofit all crates or make people even bother adding support for all new crates.

▲treyd 2 days ago | parent | next [-]

Rust's Allocator APIs have been around since 2016, less than a year after 1.0, they're just now stabilized.

The stdlib types that allocate have been generic (with default generic parameters) over the underlying Allocator used for a very long time. All widely-used data structure libraries where it's feasible/reasonable to use other allocators also are already generic over the Allocator.

It won't "take time to retrofit" because the language and ecosystem have already been prepared for it for 10 years. For context, async syntax was introduced a few years after that and if you just started learning the language today you wouldn't notice it wasn't there in 1.0.

▲Bigpet 2 days ago | parent | next [-]

Seems like I've misread the recent Rust version announcement:

> This release brings about the stabilization of the core parts of allocator_api, enabling library authors to define custom allocators that can be used together with certain standard library types - currently Vec and Box. These are enabled through the newly-stabilized Allocator trait.

That made it sound like it was rather limited currently.

I asked an LLM (because it's outside my expertise) and it said:

> It is fair to say that the heaviest architectural lifting is already done: adding a defaulted type parameter (A: Allocator = Global) preserves backward compatibility for existing code, and the core data structures already store and thread the allocator internally. However, plumbing that generic parameter through String, HashMap's std wrapper, and hundreds of standard library methods still represents substantial retrofitting work rather than a completed state.

Looks like The patch notes lead me a bit astray. But it's also not at an 'already all there' state either.

▲tialaramex 2 days ago | parent | prev [-]

Although it's true that all of Rust's standard library collections are generic over Allocator, Rust 1.100 only stabilizes useful allocator APIs for Box and Vec

If you want to use your MyCustomAllocator with a HashMap or a BinaryHeap or you've lost your mind and want a LinkedList none of that was stabilized yet.

Personally I am slightly interested in HashMap, VecDeque and String but I think Vec is much more heavily used than any of those and will illustrate just how thin the pickings are in this space for real.

▲dnautics 2 days ago | parent | next [-]

Don't diss linked lists. Sometimes they are the best data structure. In my language's interpreter, I maintain "contextual layers" and the best data structure for this is a linked list (two different layers might be applied to the same previous layers)

▲atiedebee 2 days ago | parent | prev [-]

Perhaps custom allocators could be interesting for BTreeMap and BTreeSet? I am not familiar with their internal implementation, but as I understand it those should allocate fixed-size blocks, which would benefit from a specialized fixed-size allocator.

▲tialaramex 2 days ago | parent [-]

Sure, in principle all of the standard library collections and indeed most 3rd party collections which allocate could benefit from providing a good Allocator API.

If there's a particular 3rd party collection you know would benefit, you could ask them, or write a PR to make use of Allocator. The standard library collections of course know people are interested, you can think of this as an MVP.

I just picked a few I personally want to see.

▲tialaramex 2 days ago | parent | prev [-]

> Seems like it will be useful for use-cases like embedded where you're restricted to certain parts of the library anyway. But the battle is kind of already lost for support at the level of Zig or Odin.

Odin in particular does a simply terrible job here. Take the most common need for allocations, a growable array type, in Rust Vec<T> and in Odin dynamic[T]

In Rust we only pay extra to store the allocator metadata if there is allocator metadata. If we're content using the general purpose allocator Global with global metadata it's free. In Odin we always need to pay to store an "allocator proc" function pointer and a (possibly unused) metadata pointer, so that's 2x machine word worse for Odin. We pay this even if we never allocate, and indeed even if we can't allocate, whereas of course in Rust that's free.

But then when we do need to allocate, in Rust that's a static dispatch unless our allocator specifically wants dynamic dispatch. So again for Global it's just a static dispatch. But in Odin it's always a slower dynamic dispatch.

The upside? To the extent there is one it's probably the New Jersey style, ie it was simpler to implement Odin. Which is a benefit to Ginger Bill, but not to you as an Odin programmer.

▲Bigpet 2 days ago | parent [-]

Sounds like the Odin implementation isn't particularly optimal. My point wasn't about speed or good implementations. My point was about which languages make it practical to consistenly use custom allocators as a user of the language. Not just across your own code, but also code from the standard library and external packages.

I have no practical Odin experience, I just looked at the core concepts. I tried a bit of Zig and there's significant discipline in the standard library to enable allocator use. But in 3rd Party libraries this discipline is hard to enforce because it also makes it harder to design easy to use interfaces.

I'm interested where on the spectrum of allocator usability Rust will end up at. I'd expect it somewhere between Zig and C++

▲Gibbon1 2 days ago | parent | prev [-]

I've been using an arena allocator with embedded C code for a while.

That C doesn't support an arena allocator as part of the standard library points you to everything that's wrong with C. The standards working group.

Also the Intel 0x86 and 32bit architectures caused a lot of issues with C because the paltry number of registers.

▲coolThingsFirst 2 days ago | parent | prev [-]

show me operator overloading in Zig

▲pron 2 days ago | parent | next [-]

In critical low-level code, any kind of overloading - operator or otherwise - is something you'd rather not have.

By expressivity I don't mean what characters you use in the conrecte syntax, but the number of lines needed to express an algorithm (up to a small constant). C doesn't have C++'s expressivity because it lacks templates, and while you can sort of express that kind of thing with macros, it's not all the way there. Zig, however, gets you all the way there (you might need to write matrix_plus instead of +, but the expressivity is the same).

▲Ygg2 2 days ago | parent | next [-]

BS. Using operator overloading with SIMD makes it easier to translate algorithms into code. I wrote my own U8x32 wrappers and a + b is way better than a.add(b).

▲pron 2 days ago | parent | next [-]

I'm not disputing that some people like user-controlled operator overloading (Zig does have + etc. for vectors), but clearly others prefer no user-controlled overloading of any kind (don't know what the LLMs prefer, if they have any preferences at all). In the safety-critical space where I once worked, where people could get hurt or die if there's a stack overflow, the more common preference was against (user-controlled) overloads or any implicitness. That's why we need options.

▲dnautics 2 days ago | parent | next [-]

I think zig would do to have "custom binary operators" which is not operator overloading, but basically give people what they want. It would suffice to be syntactic sugar elaborated by the parser. For example:

    const @<+> = @import("op.zig").special_add;

    const c = (a <+> b);
    //syntactic sugar for @<+>(a, b)
▲pron a day ago | parent [-]

Maybe it will add that. But just how important that is, though, I'm not so sure. Languages like Java, C, and Go don't currently offer custom infix operators, and they are more popular than many languages that do.

Note that there are some complexities, though, to infix operators regarding the order of operations (e.g. + vs * in ordinary mathematical notation) and associativity (e.g. is `(x <+> y) <+> z` equal to `x <+> (y <+> z)`?). They can be dealt with, of course, but not without adding complexity that may or may not be worth it.

▲clhodapp 2 days ago | parent | prev [-]

Fair, but don't conflate custom operators with overloading just because C++ happened to have one very specific way of implementing custom operators.

▲ 2 days ago | parent [-]
[deleted]
▲bunderbunder 2 days ago | parent | prev | next [-]

The example you choose is rather telling, though. It indicates that the use case you have in mind is not at all the one that Zig is targeting.

A more humble critique would be to just acknowledge that you don’t seem to be a member of Zig’s target audience, and move on.

▲Ygg2 2 days ago | parent [-]

I am in the target audience. I do want low level systems programming.

Saying "you don't want operator overloading" in such cases is a reductionist view of how systems programming should look.

▲bunderbunder 2 days ago | parent | next [-]

I’ll admit I interpreted that “you” in pron’s sentence to be a colloquial “you” and not an imperative one. So, more of a statement of their own preference than a strong statement of how things should be.

The point is, it’s a tradeoff. With overloading, exactly which code executes on a specific line becomes a bit more implicit because it depends on extra information from the surrounding context: the types of the arguments.

That comes with the usual tradeoffs. On the one hand, you could argue that the compiler is taking care of it for you, which saves the programmer from having to manually deal with it, and removes some visual clutter from the code.

But you can also argue that letting the choice of function be implicit means that a programmer who wants/needs to keep track of exactly what every line of code is doing needs to carry all that contextual information in their own head, too. For this programmer, it’s adding to the mental burden.

I don’t think it’s a coincidence that Kelley constantly brings up how Zig was inspired by difficulties writing a digital audio workstation with very hard real-time requirements. Part of what he’s doing there is giving a motivating case for why he wants to be an absolute control freak about, e.g., micro-managing exactly when memory is allocated and deallocated. That’s something only a subset of systems programmers need, because not all systems programming is dealing with things like hard real-time.

▲Ygg2 2 days ago | parent [-]

> The point is, it’s a tradeoff. If it's a trade off it's highly squewed.

>But you can also argue that letting the choice of function be implicit means that a programmer who wants/needs to keep track of exactly what every line of code is doing

Use LSP/IDE then ctrl+click operator. Boom. You see what it's executing.

Idea is to create abstraction that make it easier to operate on a more complex level. If you need that level of micro-managing just stick to assembly.

▲pron a day ago | parent [-]

Neither operators nor overloading are abstractions, and they offer no operation at a higher level. An abstraction is something that creates an equivalence class among multiple implementations, and that's not the case here.

First, operators are clearly not an abstraction because they offer a 1-1 correspondence: Instead of `x + y` you write `plus(x, y)`. It's just a different notation.

Overloading also isn't an abstraction because, as you pointed out, the resolution is statically known, and again, there's a 1-1 relationship. Indeed, it's just name mangling. Instead of writing `plus_int_int(x, y)` you write `plus(x,y)` and the compiler adds the `int_int` for you.

There's no doubt operators and/or overloading have some convenience advantages, but there's also no doubt they have some disadvantages, which is why some languages disallow one or both, regardless of how high or low-level they are. But Zig also has some special reasons to prevent overloading, because the language is built around compile-time introspection. So you can have just a single `plus` for multiple types, but the static dispatch is explicitly controlled by the implementation of `plus`. This means that all resolution is always local or uses unique names.

I'm not saying you have to personally like it. Zig and Rust both aim at different preferences so that people may prefer one over the other. But there is no difference in expressivity (but there very much is between assembly or even C and either one of Zig or Rust). Rust, like C++, chooses to favour implicitness over explicitness, and Zig chooses the opposite. Both have pros and cons, and neither is universally superior to the other.

▲Ygg2 a day ago | parent [-]

> Neither operators nor overloading are

Ok. Show me machine code for operator overloading. It's all abstraction. Assembly, LLIR, programming language.

Abstraction is all about collecting complexity and presenting a clearer interface.

▲pron a day ago | parent [-]

They don't add any abstraction or, if you will, they don't abstract over code that's not with operators and/or overloading. There is only ever a single correspondence. That operators don't add any expressive power is, I hope, easy to see, as they're just notation. That overloading doesn't add abstraction you can see from the fact that there is only one "implementation" of resolution at any call site (if there were more than one, then it wouldn't be static dispatch). Overloads offer shorthand, not abstraction.

Another way to see what's abstraction and what's not is to think what you can change in one place in the program without changing another (because that's what abstraction offers: indifference to the different elements in the equivalence class it creates). But there is no change you can make in a program with overloads that will require fewer changes in a program without them.

▲hitekker 2 days ago | parent | prev [-]

To be fair, the impression is that you want to do low level systems programming in Rust. It's a fine preference but it does put you in a different audience

▲Ygg2 2 days ago | parent [-]

I'm doing it in Rust because it's the closest to my ideal language. Not saying Rust is ideal.

▲pron a day ago | parent [-]

This is fine, but surely you recognise that Rust is very far from an ideal language for many other people, because there are no universal rules here. There's nothing more obvious than the fact that every programming language aims for an audience with specific preferences and tastes who like it, while others, with different preferences and tastes don't like it. This is how programming languages work when there are no universals: they make choices that appeal to some but not others.

For example, I find Rust a particularly unappealing language because it's too complicated as a high-level language (it elevates too many low-level details and offers poor abstraction compared to high-level languages) while offering me few advantages, and it's too complicated for a low-level language, as it favours implicitness where I want explicitness. Clearly, there are many people who think like me, and many people who don't, and that is as it should be. It's perfectly fine for you and others to like Rust so much - it appeals to your particular tastes - just as it's perfectly fine for me and others to dislike it, because I tend to like simpler, smaller, and more focused languages. This is why I prefer Java over C#, Clojure over Scala, Haskell over Idris, and TLA+ over Lean, while being aware that many have similar preferences and many others have the opposite ones. Your preferences, like mine, are all valid and all far from universal. No language even tries to have a universal appeal, and none comes close.

▲cornstalks 2 days ago | parent | prev [-]

You can do a+b in Zig with SIMD vectors. No operator overloading required

▲ogogmad 2 days ago | parent | next [-]

Sure. But doesn't it still lack autodiff, quaternions, etc?

▲cornstalks 2 days ago | parent [-]

Yes, there are definitely other use cases that Zig doesn't support (which some may view as a good or bad thing). I just wanted to respond to the comment about basic SIMD since the example was U8x32 and operator+, which Zig can do.

▲Ygg2 2 days ago | parent [-]

Point wasn't about SIMD, but defining my wrapper for it and giving it operations I needed.

Which seems to be something Zig can't do.

Hell, you can define operators for generic length SIMD lane.

▲pron 2 days ago | parent [-]

It's not that Zig can't do it; Zig has been carefully designed to prevent it from being done as one of its core values, which is something that's important to some people just as the ability to do it is important to some other people. Supporting it or preventing it both have pros and cons for different users and different uses.

▲Ygg2 a day ago | parent [-]

Sure Zig is pre-1.0 it can do anything but it's not going to do anything (e.g. add a borrow checker that closes the possible memory unsafety issues. They will add a runtime checker via Fil-C though)

The issue is the mindset, "I (Zig creator) don't need it nobody else does."

▲pron a day ago | parent [-]

That's not what I wrote. I said Zig is carefully designed to disallow overloading because that's what some of us want (and there are Zig-specific reasons to want that even more). You, however, seem to be of the mindset that if some set of people want a feature, every language should have it, but that is clearly untrue. Something many of us value (especially in low-level programming, but not just there) is simplicity, which is the one feature you can never add to a language once you lose it. In every product, including programming languages, what features you don't allow in is just as important as the features you have. What made the iPhone the iPhone wasn't that it had a touchscreen, but that it didn't have a physical keypad.

In the case of Zig this is especially glaring and a core aspect of its design. In my low-level work, partial evaluation (or "compile-time code generation") is very important, and both C++ and Rust have it, albeit inelegantly. The problem is that they both bring with them the complexity I try to avoid. Zig is special because it offers all that power in a simple language, which is what many people want.

Different programmers want contradictory things out of programming languages, and it's impossible to satisfy all of them. Some people want to use a language with operator overloading, while others want to use a language that doesn't offer operator overloading. They can't be using the same language.

▲Ygg2 a day ago | parent [-]

> Something many of us value is simplicity

Simplicity is a good metric but a lousy goal. If simplicity was a good goal, Brainfuck dialect would be the most used language.

> Different programmers want contradictory things out of programming languages

True to an extent but there are universal qualities every good language shares.

Ease of use; good performance even in naive cases; good safety/security even in naive cases; expressivity; fast compile times; conciseness.

▲pron a day ago | parent [-]

> Simplicity is a good metric but a lousy goal. If simplicity was a good goal, Brainfuck dialect would be the most used language.

No, because simplicity is not the only goal.

> True to an extent but there are universal qualities every good language shares.

Yes, there are some, but none of them are relevant here.

> Ease of use; good performance even in naive cases; good safety/security even in naive cases; expressivity; fast compile times; conciseness.

Obviously, conciseness is not some ultimate goal, but a tradeoff, often one that contrasts with readability/understandability. APL is probably the language most famous for its conciseness, but it was never very popular. The same goes for some of the others: these are all good things but come with the caveat of "all else being equal", i.e. if two languages were identical in every respect but one was clearly better on one of these fronts it could be universally recognised as superior. This rarely happens, and usually to gain more points in some areas a language has to sacrifice others.

Even safety is not as simple as it may seem. I've worked in formal methods and on safety-critical software, correctness is very important to me, and safety serves correctness. As you may know, while both safe Rust and safe Zig offer spatial memory safety, unlike Rust, Zig doesn't offer temporal memory safety. You'd think that someone who cares so much about correctness would always prefer more safety, but again we run into the problem of "... all things being equal," and that's not the case here. For one, to achieve its temporal memory safety, Rust sacrifices simplicity and explicitness, which are also important for correctness in some important situations (while Zig compensates not only in simplicity and explicitness, but also in other ways). For another, Rust makes you give up much of its safety precisely when you reach for a low-level language, which is when you want precise control. So the point is that things aren't so black-and-white (even Rust accepts that; e.g. there are languages safer than Rust, some existed before it was invented).

Anyway, you will simply need to accept that even among the best and most experienced language designers in the world and even among the best and most experienced programmers in the world, there's often a lack of consensus, and in this particular case, some prefer Zig to Rust and some vice versa. They're both decent, they're both interesting, they're different from each other and make different choices when there's no universal superiority, and they're both liked by some and disliked by others.

▲Ygg2 2 days ago | parent | prev [-]

Same goes for any bitwise operation <<, >>, &, |, %.

Can I write my SIMD/number abstraction and have it work?

Let's say I need rotors. With six dimensions. Can I overload it's basic numeric operations?

▲pron a day ago | parent [-]

You need to understand that the things you want to do are the very things that others want their language to ensure cannot be done. For example, the question I'm asking is, can your preferred language ensure that operators are not overloaded? You want a language with an operator overloading feature, and I want a language with an operator-overloading-prevention feature. My preferred language lacks the feature you want, and your preferred language lacks the feature I want. The same language cannot give us both what we want, because what we want is contradictory. This is why we prefer different languages; some cater to your preferences and others to mine.

▲tialaramex 2 days ago | parent | prev [-]

C++ destroyed the good will here by simultaneously insisting that you're only supposed to use this feature to give types their natural operators (makes sense) and then inventing implausible new "natural" operators so that Bjarne can show off his new toy with the I/O streams.

I actually don't like Rust's choice to implement + for Strings but most people seem to accept that, however C++ goes ludicrously far beyond "I don't like it but some people do" and straight off the cliff into "Nobody thinks that's OK" leaving you to wonder why there was effort to implement this bad idea. For example C++ can overload the short-circuiting boolean operators... but the overloaded versions don't short circuit. So now you've made things dramatically worse!

▲pron 2 days ago | parent [-]

Note that it's not that Zig is opposed to (user-controlled) operator overloading; it's opposed to (user-controlled) overloading of all symbols: The resolution of `foo` in `foo(x)` should not depend on the type of `x`. The issue is not the operators but the overloading, and Zig wants to prevent (user-controlled) overloading of any kind. This makes some things less convenient and other things more convenient.

▲tialaramex 2 days ago | parent [-]

> The resolution of `foo` in `foo(x)` should not depend on the type of `x`

This depends on how you're squinting at this word "resolution".

Remember on a modern CPU an XOR R1 R1 does not result in an ALU operation, the decoder understands this idiom and just renames a zero'd register as R1, whereas of course it can't do that for say XOR R2 R3 because that's an actual XOR operation. So if "resolution" means "exactly what happens on the CPU" apparently there is no such facility provided even in assembler.

So apparently we mean only that it should have, in some sense, the same effect. I think the fact Rust is OK with

  assert!("Goose".contains('o'))
  assert!("Goose".contains("Goo"))
  assert!("Goose".contains(&['a', 'e', 'i', 'o', 'u']))
.. meets this criterion, whereas presumably you do not agree.
▲pron 2 days ago | parent [-]

It's not about okay or not okay. Zig is designed to prevent any user code from being implicitly called, or for the resolution of user code to be done implicitly. It's not about what the CPU does internally, but whether you're able to tell, from reading the code of a single subroutine, which operations could be done, whether stack frames can be created etc.. This has pros and cons, and for some people the pros outweigh the cons and for others, the other way around, and that's why different programming languages need to prioritise different things when they target different users. Just as Rust clearly isn't supposed to be best for all uses, neither is Zig, or any other language for that matter. Every language is meant to appeal to some and not others.

▲melodyogonna 2 days ago | parent | prev | next [-]

Many people are against operator overloading, imo my only issue with C++ operator overloading is that you're allowed to overload the the assignment operator. From what I can see this is the root of many issues

▲bunderbunder 2 days ago | parent | next [-]

I’ve also experienced some unnecessary shenanigans related to blurring the distinction between primitive operations, and stuff that happens dynamically at run time such as method calls.

For a somewhat different example of the same general idea, look at how Java handles primitives and their boxed counterparts. = on two ints always compares their value. = on two Integers compares their pointers. But it uses flyweight instances for values up to 256, so in Java, whether two Integers with the same numeric value are equal according to the = operator depends on what that value is.

Then throw autoboxing and generics into the mix, and it suddenly becomes very easy to do refactorings that work fine in the unit tests (which tend to use small numbers) but fails in confusing ways in production.

This isn’t an example of operator overloading, of course. But I hope it more simply illustrates why, at least in certain problem domains, there’s value in being able to visually tell when surprising runtime shenanigans are and are not possible. And also to make it impossible to cross that boundary without touching all code that falls within the blast radius of that change.

▲wpollock 2 days ago | parent [-]

> But it uses flyweight instances for values up to 256, so in Java, whether two Integers with the same numeric value are equal according to the = operator depends on what that value is. ...

I believe Java caches Integer objects for values -128..+128, not 256. But that may be an implementation detail, and not something to count on.

▲bunderbunder 2 days ago | parent | next [-]

No, you’re right. I misremembered.

▲wpollock 2 days ago | parent [-]

> No, you’re right. I misremembered.

We both misremembered. I found where this is documented and the range is -128..127 inclusive: <https://docs.oracle.com/en/java/javase/27/docs/api/java.base...>

▲gf000 2 days ago | parent | prev [-]

And since java 28's value types, == will work identically for all Integers, finally.

▲wpollock 2 days ago | parent [-]

Note that value types will still be a "preview" feature in Java 28.

I'm looking forward to structured concurrency in Java 28. I already thought of a title for my article "Java Concurrency - Past, Present, and Futures".

▲pjmlp 2 days ago | parent | prev | next [-]

In functional programming languages basically any unicode character can be used as function name, those with overloading issues in C++ better never come close to FP.

▲vnorilo 2 days ago | parent | prev [-]

overloading the comma is also a special flavor. everyone should try it once.

▲ogogmad 2 days ago | parent | prev | next [-]

Is there a way to support varying the meaning of "+" etc without introducing a lot of complexity? I suppose if Zig had nested functions, and the ability to define a nested function called "+", then you could have name shadowing instead of "overloading".

OCaml basically gives you operator overloading this way without actually doing overloading. I'm wondering why we can't have these OCaml-isms in other languages - without needing to use OCaml - which sadly comes with other commitments.

▲dnautics 2 days ago | parent [-]

I always thought custom infix operators would be easy and basically what people want, e.g:

    const @<+> = @import("op.zig").special_add;

    const c = (a <+> b);
    //syntactic sugar for @<+>(a, b)
▲u8080 2 days ago | parent | prev | next [-]

please no

▲nvme0n1p1 2 days ago | parent | prev [-]

Yes! And exceptions and inheritance and RTTI too. Bring back all the mistakes! Let's party like it's c++99