Hacker News new | ask | show | jobs
by no_wizard 423 days ago
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

5 comments

>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

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.

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.

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.

>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.

> 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.

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.
> 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.

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.
Writing correct code did not start after the introduction of the rust programming language
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)

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.

> 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.

Thank you for the answer! Do you have an example? I'm having a fish-doesn't-know-water problem.
Basically anything that involves objects mutually referencing each other.
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.
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.
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.

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)
And those index values are just pointers by another name!
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).
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.
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

Austra's concept are interesting but the introduction doesn't show how to handle correctly errors in this language..
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

> 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.

There is Mojo and Vale (which was created by a now Mojo core contributor)
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.
The first and last blog post was in 2021. Looks like it’s still active on Github, though?
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.
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/

Agree, been waiting for it for almost a decade.
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

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.

I like Zig as a replacement for C, but not C++ due to its lack of RAII. Rust on the other hand is a great replacement for C++. I see Zig as filling a small niche where allocation failures are paramount - very constrained embedded devices, etc... Otherwise, I think you just get a lot more with Rust.
Compile times and painful to refactor codebase are rust’s main drawbacks for me though.

It’s totally subjective but I find the language boring to use. For side projects I like having fun thus I picked zig.

To each his own of course.

> refactor codebase are rust’s main drawbacks

Hard disagree about refactoring. Rust is one of the few languages where you can actually do refactoring rather safely without having tons of tests that just exist to catch issues if code changes.

Lifetimes and generic tend to leak so you have to modify your code all around the place when you touch them though.
Explicit lifetimes are not super common, and I haven't had any trouble with them causing refactor headaches. Generics obviously do leak, the same way that C++ templates do.
Even better than RAII would be linear types, but it would require a borrow checker to track the lifetimes of objects. Then you would get a compiler error if you forget to call a .destroy() method
no you just need analysis with a dependent type system (which linear types are a subset of). it doesn't have to be in the compiler. there was a proof of concept here a few months ago:

https://news.ycombinator.com/item?id=42923829

https://news.ycombinator.com/item?id=43199265

in principle it should be doable, possibly not in the language/compiler itself, there was this POC a few months ago:

https://github.com/ityonemo/clr

I wish for “strict” mode as well. My current thinking:

TypeScript is to JavaScript

as

Zig is to C

I am a huge TS fan.

Is Zig aiming to extend C or extinguish it? The embrace story is well-established at this point but the remainder is often unclear in the messaging from the community.
It's improved C.

C interop is very important, and very valuable. However, by removing undefined behaviours, replacing macros that do weird things with well thought-through comptime, and making sure that the zig compiler is also a c compiler, you get a nice balance across lots of factors.

It's a great language, I encourage people to dig into it.

Zig is open source, so the analogy to Microsoft's EEE [0] seems misplaced.

[0] https://en.m.wikipedia.org/wiki/Embrace,_extend,_and_extingu...

Open source or not isn't the point. The point is the mission and the ecosystem. Some of the Zig proponents laud the C compatibility. Others are seeking out the "pure Zig" ecosystem. Curious onlookers want to know if the Zig ecosystem and community will be as hostile to the decades of C libraries as the Rust zealots have been.

To be fair, I don't believe there is a centralized and stated mission with Zig but it does feel like the story has moved beyond the "Incrementally improve your C/C++/Zig codebase" moniker.

> Curious onlookers want to know if the Zig ecosystem and community will be as hostile to the decades of C libraries as the Rust zealots have been.

Definitely not the case in Zig. From my experience, the relationship with C libraries amounts to "if it works, use it".

Are you referring to static linking? Dynamic linking? Importing/inclusion? How does this translate (no pun intended) when the LLVM backend work is completed? Does this extend to reproducible builds? Hermetic builds?

And the relationship with C libraries certainly feels like a placeholder, akin to before the compiler was self-hosted. While I have seen some novel projects in Zig, there are certainly more than a few "pure Zig" rewrites of C libraries. Ultimately, this is free will. I just wonder if the Zig community is teeing up for a repeat of Rust's actix-web drama but rather than being because of the use of unsafe, it would be due to the use of C libraries instead of the all-Zig counterparts (assuming some level of maturity with the latter). While Zig's community appears healthier and more pragmatic, hype and ego have a way of ruining everything.

zig's C compat is being lowered from 'comptime' equivalent status to 'zig build'-time equivalent status. When you'll need to put 'extern "C"' annotations on any import/export to C, it'll have gone full-circle to C++ C compat, and thus be none the wiser.

andrewrk's wording towards C and its main ecosystem (POSIX) is very hostile, if that is something you'd like to go by.

The goal rather explicitly seems to be to extinguish it - the idea being that if you've got Zig, there should be no reason to need to write new code in C, because literally anything possible in C should be possible (and ideally done better) in Zig.

Whether that ends up happening is obviously yet to be seen; as it stands there are plenty of Zig codebases with C in the mix. The idea, though, is that there shouldn't be anything stopping a programmer from replacing that C with Zig, and the two languages only coexist for the purpose of allowing that replacement to be gradual.

Most of Zig's safety was already available in 1978's Modula-2, but apparently languages have to come in curly brackets for adoption.
languages have to come in curly brackets for adoption

Python and Ruby are two very popular counterexamples.

Not really, Ruby has plenty of curly brackets, e.g. 5.times { puts "hello!" }.

In both cases, while it wasn't curly brackets that drove their adoption, it was unavoidable frameworks.

Most people only use Ruby when they have Rails projects, and what made Python originally interesting was Zope CMS.

And nowadays AI/ML frameworks, that are actually written in C, C++ and Fortran, making Python relevant because scientists decided on picking Python for their library bindings, it could have been Tcl just as well, as choices go.

So yeah, maybe not always curly brackets, but definitly something that makes it unavoidable, sadly Modula-2 lacked that, an OS vendor pushing it no matter what, FAANG style.

Which AI/ML frameworks are written in Fortran?
Probably none, it was more a kind of expression, given the tradition of "Python" libraries, that are actually bindings to C, C++, Fortran libraries.