Most active commenters
  • the__alchemist(4)
  • Ygg2(4)
  • int_19h(3)
  • estebank(3)

←back to thread

Things Zig comptime won't do

(matklad.github.io)
458 points JadedBlueEyes | 41 comments | | HN request time: 1.914s | source | bottom
Show context
no_wizard ◴[] No.43744932[source]
I like the Zig language and tooling. I do wish there was a safety mode that give the same guarantees as Rust, but it’s a huge step above C/C++. I am also extremely impressed with the Zig compiler.

Perhaps the safety is the tradeoff with the comparative ease of using the language compared to Rust, but I’d love the best of both worlds if it were possible

replies(5): >>43744960 #>>43745201 #>>43745418 #>>43745573 #>>43749228 #
1. ksec ◴[] No.43745418[source]
>but I’d love the best of both worlds if it were possible

I am just going to quote what pcwalton said the other day that perhaps answer your question.

>> I’d be much more excited about that promise [memory safety in Rust] if the compiler provided that safety, rather than asking the programmer to do an extraordinary amount of extra work to conform to syntactically enforced safety rules. Put the complexity in the compiler, dudes.

> That exists; it's called garbage collection.

>If you don't want the performance characteristics of garbage collection, something has to give. Either you sacrifice memory safety or you accept a more restrictive paradigm than GC'd languages give you. For some reason, programming language enthusiasts think that if you think really hard, every issue has some solution out there without any drawbacks at all just waiting to be found. But in fact, creating a system that has zero runtime overhead and unlimited aliasing with a mutable heap is as impossible as finding two even numbers whose sum is odd.

[1] https://news.ycombinator.com/item?id=43726315

replies(4): >>43745462 #>>43745760 #>>43745791 #>>43746930 #
2. skybrian ◴[] No.43745462[source]
Yes, but I’m not hoping for that. I’m hoping for something like a scripting language with simpler lifetime annotations. Is Rust going to be the last popular language to be invented that explores that space? I hope not.
replies(3): >>43745665 #>>43745858 #>>43745947 #
3. hyperbrainer ◴[] No.43745665[source]
I was quite impressed with Austral[0], which used Linear Types and avoids the whole Rust-like implementation in favour of a more easily understandable system, albeit slightly more verbose.

[0]https://borretti.me/article/introducing-austral

replies(1): >>43746855 #
4. the__alchemist ◴[] No.43745760[source]
Maybe this is a bad place to ask, but: Those experienced in manual-memory langs: What in particular do you find cumbersome about the borrow system? I've hit some annoyances like when splitting up struct fields into params where more than one is mutable, but that's the only friction point that comes to mind.

I ask because I am obvious blind to other cases - that's what I'm curious about! I generally find the &s to be a net help even without mem safety ... They make it easier to reason about structure, and when things mutate.

replies(4): >>43745891 #>>43745963 #>>43746263 #>>43747347 #
5. spullara ◴[] No.43745791[source]
With Java ZGC the performance aspect has been fixed (<1ms pause times and real world throughput improvement). Memory usage though will always be strictly worse with no obvious way to improve it without sacrificing the performance gained.
replies(1): >>43747793 #
6. Philpax ◴[] No.43745858[source]
You may be interested in https://dada-lang.org/, which is not ready for public consumption, but is a language by one of Rust's designers that aims to be higher-level while still keeping much of the goodness from Rust.
replies(1): >>43745902 #
7. rc00 ◴[] No.43745891[source]
> What in particular do you find cumbersome about the borrow system?

The refusal to accept code that the developer knows is correct, simply because it does not fit how the borrow checker wants to see it implemented. That kind of heavy-handed and opinionated supervision is overhead to productivity. (In recent times, others have taken to saying that Rust is less "fun.")

When the purpose of writing code is to solve a problem and not engage in some pedantic or academic exercise, there are much better tools for the job. There are also times when memory safety is not a paramount concern. That makes the overhead of Rust not only unnecessary but also unwelcome.

replies(3): >>43745915 #>>43746004 #>>43747477 #
8. skybrian ◴[] No.43745902{3}[source]
The first and last blog post was in 2021. Looks like it’s still active on Github, though?
9. the__alchemist ◴[] No.43745915{3}[source]
Thank you for the answer! Do you have an example? I'm having a fish-doesn't-know-water problem.
replies(1): >>43746854 #
10. Ygg2 ◴[] No.43745947[source]
> Is Rust going to be the last popular language to be invented that explores that space? I hope not.

Seeing how most people hate the lifetime annotations, yes. For the foreseeable future.

People want unlimited freedom. Unlimited freedom rhymes with unlimited footguns.

replies(1): >>43746350 #
11. sgeisenh ◴[] No.43745963[source]
Lifetime annotations can be burdensome when trying to avoid extraneous copies and they feel contagious (when you add a lifetime annotation to a frequently used type, it bubbles out to anything that uses that type unless you're willing to use unsafe to extend lifetimes). The solutions to this problem (tracking indices instead of references) lose a lot of benefits that the borrow checker provides.

The aliasing rules in Rust are also pretty strict. There are plenty of single-threaded programs where I want to be able to occasionally read a piece of information through an immutable reference, but that information can be modified by a different piece of code. This usually indicates a design issue in your program but sometimes you just want to throw together some code to solve an immediate problem. The extra friction from the borrow checker makes it less attractive to use Rust for these kinds of programs.

replies(1): >>43746402 #
12. Ygg2 ◴[] No.43746004{3}[source]
> The refusal to accept code that the developer knows is correct,

How do you know it is correct? Did you prove it with pre-condition, invariants and post-condition? Or did you assume based on prior experience.

replies(3): >>43746128 #>>43746298 #>>43749589 #
13. yohannesk ◴[] No.43746128{4}[source]
Writing correct code did not start after the introduction of the rust programming language
replies(1): >>43746376 #
14. Starlevel004 ◴[] No.43746263[source]
Lifetimes add an impending sense of doom to writing any sort of deeply nested code. You get this deep without writing a lifetime... uh oh, this struct needs a reference, and now you need to add a generic parameter to everything everywhere you've ever written and it feels miserable. Doubly so when you've accidentally omitted a lifetime generic somewhere and it compiles now but then you do some refactoring and it won't work anymore and you need to go back and re-add the generic parameter everywhere.
replies(2): >>43746507 #>>43746586 #
15. edflsafoiewq ◴[] No.43746298{4}[source]
One example is a function call that doesn't compile, but will if you inline the function body. Compilation is prevented only by the insufficient expressiveness of the function signature.
16. xmorse ◴[] No.43746350{3}[source]
There is Mojo and Vale (which was created by a now Mojo core contributor)
17. Ygg2 ◴[] No.43746376{5}[source]
Nope, but claims of knowing to write correct code (especially C code) without borrow checker sure did spike with its introduction. Hence, my question.

How do you know you haven't been writing unsafe code for years, when C unsafe guidelines have like 200 entries[1].

[1]https://www.dii.uchile.cl/~daespino/files/Iso_C_1999_definit... (Annex J.2 page 490)

replies(1): >>43746850 #
18. bogdanoff_2 ◴[] No.43746402{3}[source]
>There are plenty of single-threaded programs where I want to be able to occasionally read a piece of information through an immutable reference, but that information can be modified by a different piece of code.

You could do that using Cell or RefCell. I agree that it makes it more cumbersome.

19. the__alchemist ◴[] No.43746507{3}[source]
I guess the dodge on this one is not using refs in structs. This opens you up to index errors though because it presumably means indexing arrays etc. Is this the tradeoff. (I write loads of rusts in a variety of domains, and rarely need a manual lifetime)
replies(1): >>43747105 #
20. pornel ◴[] No.43746586{3}[source]
There is a stark contrast in usability of self-contained/owning types vs types that are temporary views bound by a lifetime of the place they are borrowing from. But this is an inherent problem for all non-GC languages that allow saving pointers to data on the stack (Rust doesn't need lifetimes for by-reference heap types). In languages without lifetimes you just don't get any compiler help in finding places that may be affected by dangling pointers.

This is similar to creating a broadly-used data structure and realizing that some field has to be optional. Option<T> will require you to change everything touching it, and virally spread through all the code that wanted to use that field unconditionally. However, that's not the fault of the Option syntax, it's the fault of semantics of optionality. In languages that don't make this "miserable" at compile time, this problem manifests with a whack-a-mole of NullPointerExceptions at run time.

With experience, I don't get this "oh no, now there's a lifetime popping up everywhere" surprise in Rust any more. Whether something is going to be a temporary view or permanent storage can be known ahead of time, and if it can be both, it can be designed with Cow-like types.

I also got a sense for when using a temporary loan is a premature optimization. All data has to be stored somewhere (you can't have a reference to data that hasn't been stored). Designs that try to be ultra-efficient by allowing only temporary references often force data to be stored in a temporary location first, and then borrowed, which doesn't avoid any allocations, only adds dependencies on external storage. Instead, the design can support moving or collecting data into owned (non-temporary) storage directly. It can then keep it for an arbirary lifetime without lifetime annotations, and hand out temporary references to it whenever needed. The run-time cost can be the same, but the semantics are much easier to work with.

21. int_19h ◴[] No.43746850{6}[source]
It's not difficult to write a provably correct implementation of doubly linked list in C, but it is very painful to do in Rust because the borrow checker really hates this kind of mutually referential objects.
replies(1): >>43749579 #
22. int_19h ◴[] No.43746854{4}[source]
Basically anything that involves objects mutually referencing each other.
replies(1): >>43746946 #
23. renox ◴[] No.43746855{3}[source]
Austra's concept are interesting but the introduction doesn't show how to handle correctly errors in this language..
replies(1): >>43748794 #
24. no_wizard ◴[] No.43746930[source]
I have zero issue with needing runtime GC or equivalent like ARC.

My issue is with ergonomics and performance. In my experience with a range of languages, the most performant way of writing the code is not the way you would idiomatically write it. They make good performance more complicated than it should be.

This holds true to me for my work with Java, Python, C# and JavaScript.

What I suppose I’m looking for is a better compromise between having some form of managed runtime vs non managed

And yes, I’ve also tried Go, and it’s DX is its own type of pain for me. I should try it again now that it has generics

replies(1): >>43747394 #
25. the__alchemist ◴[] No.43746946{5}[source]
Oh, that does sound tough in rust! I'm not even sure how to approach it; good to know it's a useful pattern in other langs.
replies(1): >>43747522 #
26. quotemstr ◴[] No.43747105{4}[source]
And those index values are just pointers by another name!
replies(1): >>43747764 #
27. dzaima ◴[] No.43747347[source]
I imagine a large part is just how one is used to doing stuff. Not being forced to be explicit about mutability and lifetimes allows a bunch of neat stuff that does not translate well to Rust, even if the desired thing in question might not be hard to do in another way. (but that other way might involve more copies / indirections, which users of manually-memory langs would (perhaps rightfully, perhaps pointlessly) desire to avoid if possible, but Rust users might just be comfortable with)

This separation is also why it is basically impossible to make apples-to-apples comparisons between languages.

Messy things I've hit (from ~5KLoC of Rust; I'm a Rust beginner, I primarily do C) are: cyclical references; a large structure that needs efficient single-threaded mutation while referenced from multiple places (i.e. must use some form of cell) at first, but needs to be sharable multithreaded after all the mutating is done; self-referential structures are roughly impossible to move around (namely, an object holding &-s to objects allocated by a bump allocator, movable around as a pair, but that's not a thing (without libraries that I couldn't figure out at least)); and refactoring mutability/lifetimes is also rather messy.

28. neonsunset ◴[] No.43747394[source]
Using spans, structs, object and array pools is considered fairly idiomatic C# if you care about performance (and many methods now default to just spans even outside that).

What kind of idiomatic or unidiomatic C# do you have in mind?

I’d say if you are okay with GC side effects, achieving good performance targets is way easier than if you care about P99/999.

29. charlotte-fyi ◴[] No.43747477{3}[source]
Isn't the persistent failure of developers to "know" that their code is correct the entire point? Unless you have mechanical proof, in the aggregate and working on any project of non-trivial size "knowing" is really just "assuming." This isn't academic or pedantic, it's a basic epistemological claim with regard to what writing software actually looks like in practice. You, in fact, do not know, and your insistence that you do is precisely the reason that you are at greater risk of creating memory safety vulnerabilities.
30. int_19h ◴[] No.43747522{6}[source]
Well, one can always write unsafe Rust.

Although the more usual pattern here is to ditch pointers and instead have a giant array of objects referring to each other via indices into said array. But this is effectively working around the borrow checker - those indices are semantically unchecked references, and although out-of-bounds checks will prevent memory corruption, it is possible to store index to some object only for that object to be replaced with something else entirely later.

replies(2): >>43747732 #>>43748790 #
31. estebank ◴[] No.43747732{7}[source]
> it is possible to store index to some object only for that object to be replaced with something else entirely later.

That's what generational arenas are for, at the cost of having to check for index validity on every access. But that cost is only in comparison to "keep a pointer in a field" with no additional logic, which is bug-prone.

32. estebank ◴[] No.43747764{5}[source]
It's not "just pointers", because they can have additional semantics and assurances beyond "give me the bits at this address". The index value can be tied to a specific container (using new types for indexing so tha you can't make the mistake of getting value 1 from container A when it represents an index from container B), can prevent use after free (by embedding data about the value's "generation" in the key), and makes the index resistant to relocation of the values (because of the additional level of indirection of the index to the value's location).
replies(1): >>43748492 #
33. estebank ◴[] No.43747793[source]
IMO the best chance Java has to close the gap on memory utilisation is Project Valhalla[1] which brings value types to the JVM, but the specifics will matter. If it requires backwards incompatible opt-in ceremony, the adoption in the Java ecosystem is going to be an uphill battle, so the wins will remain theoretical and be unrealised. If it is transparent, then it might reduce the memory pressure of Java applications overnight. Last I heard was that the project was ongoing, but production readiness remained far in the future. I hope they pull it off.

1: https://openjdk.org/projects/valhalla/

replies(1): >>43748268 #
34. spullara ◴[] No.43748268{3}[source]
Agree, been waiting for it for almost a decade.
35. quotemstr ◴[] No.43748492{6}[source]
Yes, but like raw pointers, they lack lifetime guarantees and invite use after free vulnerabilities
36. Cloudef ◴[] No.43748790{7}[source]
>unsafe rust Which is worse than C
37. hyperbrainer ◴[] No.43748794{4}[source]
Austral's specification is one of the most beautiful and well-written pieces of documentation I have ever found. It's section on error handling in Austral[0] cover everything from rationale and alternatives to concrete examples of how exceptions should be handled in conjunction with linear types.

https://austral-lang.org/spec/spec.html#rationale-errors

38. Ygg2 ◴[] No.43749579{7}[source]
Hard part of writing actually provable code isn't the code. It's the proof. What are invariants of double linked list that guarantee safety?

Writing provable anything is hard because it forces you to think carefully about that. You can no longer reason by going into flow mode, letting fast and incorrect part of the brain take over.

39. lelanthran ◴[] No.43749589{4}[source]
Rust prevents classes of bugs by preventing specific patterns.

This means it rejects, by definition alone, bug-free code because that bug free code uses a pattern that is not acceptable.

IOW, while Rust rejects code with bugs, it also rejects code without bugs.

It's part of the deal when choosing Rust, and people who choose Rust know this upfront and are okay with it.

replies(1): >>43754844 #
40. bigstrat2003 ◴[] No.43754844{5}[source]
> This means it rejects, by definition alone, bug-free code because that bug free code uses a pattern that is not acceptable.

That is not true by definition alone. It is only true if you add the corollary that the patterns which rustc prevents are sometimes bug-free code.

replies(1): >>43756056 #
41. lelanthran ◴[] No.43756056{6}[source]
> That is not true by definition alone. It is only true if you add the corollary that the patterns which rustc prevents are sometimes bug-free code.

That corollary is only required in the cases that a pattern is unable to produce bug-free code.

In practice, there isn't a pattern that reliably, 100% of the time and deterministically produces a bug.