Readit News logoReadit News
nneonneo · 5 days ago
Last year I ported a virtio-host network driver written in Rust, entirely changing its backend, interrupt mechanism and converting it from a library to a standalone process. This is a complex program that handles low-level memory mappings, VM interrupts, network sockets, multithreading, etc.

What’s remarkable is that (a) I have very little Rust experience overall (mostly a Python programmer), (b) very little virtio experience, and (c) essentially no experience working with any of the libraries involved. Yet, I pulled off the refactor inside of a week, because by the time the project actually compiled, it worked perfectly (with one minor Drop-related bug that was easily found and fixed).

This was aided by libraries that went out of their way to make sure you couldn’t hold them wrong, and it shows.

mmastrac · 5 days ago
I've been writing Rust code for a while and generally if it compiles, it works. There are occasional deadlocks and higher-level ordering issues from time-to-time, but modulo bugs, the compiler succeeding generally means it'll run a decent amount of your project.
jabwd · 5 days ago
Though my code complexity is far FAR from what you've been writing it has been a similar experience for me. There are a few footguns and a bug in chrono I still have to find the energy to report or fix; which has been causing a bi-yearly issue, but apart from that happy lil programmer.
AlanYx · 5 days ago
It's a lot like Haskell in that respect. Once you get something to compile, it tends to work more often than not.
shark1 · 5 days ago
Reviews like yours are increasing my interested towards Rust. Apparently the tools and ecosystem are great, build on solid concepts and foundation.
johnfn · 5 days ago
I think Rust is awesome and I agree with that part of the article.

What I disagree with is that it's the fault of Typescript that the href assignment bug is not caught. I don't think that has anything to do with Typescript. The bug is that it's counter-intuitive that setting href defers the location switch until later. You could imagine the same bug in Rust if Rust had a `set_href` function that also deferred the work:

    set_href('/foo');

    if (some_condition) {
        set_href('/bar');
    }
Of course, Rust would never do this, because this is poor library design: it doesn't make sense to take action in a setter, and it doesn't make sense that assigning to href doesn't immediately navigate you to the next page. Of course, Rust would never have such a dumb library design. Perhaps I'm splitting hairs, but that's not Rust vs TypeScript - it's Rust's standard library vs the Web Platform API. To which I would totally agree that Rust would never do something so silly.

flohofwoe · 5 days ago
Objection your honor ;)

A 'setter' should never ever cause an action to be triggered, and especially not immediately inside the setter.

At the least change the naming, like `navigate_to(href)`.

But in the browser environment it's also perfectly clear why it is not happening immediately, your entire JS code is essentially just a callback which serves the browser event loop and tells it what to do next. A function which never returns to the caller doesn't fit into the overall picture.

johnfn · 5 days ago
That’s a good point. I actually modified my comment because I assumed everyone would take for granted that no work should be done in a setter :)

> A function which never returns to the caller doesn't fit into the overall picture.

Hmm, not sure about this. On the node side, you can process.exit() out of a callback. If setting href worked like that, I think it would be less confusing.

bkolobara · 5 days ago
Thanks! I should have clarified a bit better that example.

The point I was trying to make is that Rust's ownership model would allow you to design an api where calling `window.set_href('/foo')` would take ownership of `window`. So you would not be able to call it twice. This possibility doesn't exist at all in TypeScript, because it doesn't track lifetimes.

Of course, TypeScript can't do anything here either way. Even if it had knowledge of lifetimes, the JavaScript API already existed before and it would not be possible to introduce an ownership model on top of it, because there are just too many global variables and APIs.

I wanted more to demonstrate how Rust's whole set of features neatly fits together and that it would be hard to get the same guarantees with "just types".

johnfn · 5 days ago
I'm not as familiar with Rust, but isn't there still a gap? For instance, if we modified window.set_href to have move semantics, wouldn't this still work (i.e. not produce an error)?

    let win = window.set_href("/foo")
    win.set_href("/bar")
You might say "why would you ever do that" but my point is that if it's really the lack of move semantics that cause this problem (not the deferred update), then you should never be able to cause an issue if you get the types correct. And if you do have deferred updates, maybe you do want to do something after set_href, like send analytics in a finally() block.

In fact, Typescript does have a way to solve this problem - just make `setHref` return never[1]! Then subsequent calls to `setHref`, or in fact anything else at all, will be an error. If I understand correctly, this is similar to how `!` works in Rust.

So maybe TS is not so bad after all :)

[1]: https://www.typescriptlang.org/play/?ssl=9&ssc=1&pln=9&pc=2#...

dominicrose · 5 days ago
The Rust example was interesting but the Typescript example doesn't show if TS would or would not be good for a big project.

I'm scared of Ruby because I catch bugs at runtime all the time, but here's the thing: it ends up working before a commit and it was easy enough to get there and it's satisfying to read and edit the code. Now wether I can keep going like this if the project become bigger is the question.

The location.href issue is really a javascript problem that has been inherited by TS. Because JS allows to modify attributes, the browser kind of has to take the change into account. But it's not like Ruby's exit keyword. The page is still there until the next page loads and this makes total sense once you know it.

masklinn · 5 days ago
Technically Rust could hint at the semantics, based on whether `set_href` returned `()` or `!`. However the “erroneous” usage would not be surfaced in the case of a conditional redirect indeed (for a non-conditional one you may notice that the following code is not dead).
socalgal2 · 5 days ago
That is not a repo of the code in the article. The article's code is effectively

    set_href('/foo');

    let future = doSomethingElse()
    block_on(future)

    if (some_condition) {
        set_href('/bar');
    }
This code makes the bug clearer. doSomethingElse is effectively allowing the page to exit. this would be no different in many apps, even in rust.

The browser does not start a process when you set `window.location.href`. It starts a process after your code exits and lets the event loop run other tasks. The `await` in the example code is what allow other tasks to run, including the task to load a new page, (or quit an app, etc..) That task that was added when you set `window.location.href`

If that's not clear

    // task 1
    window.location.href = '/foo' // task2 (queues task2 to load the page)

    let content = await response.json(); // adds task3 to load json
                                         // which will add task4
                                         // to continue when finished

    // task4
    if (content.onboardingDone) {
        window.location.href = "/dashboard";
    } else {
        window.location.href = "/onboarding";
    }
task2 runs after task1. task1 exits at the `await`. task2, clears out all the tasks. task3 and task4 never run.

depressedpanda · 4 days ago
No, I think you misunderstand how it works. The problem is that task 4, as you call it, runs after the navigation triggered by the redirect value.

The the author expects the side-effect -- navigation to a new page -- of the window.location.href setter to abort the code running below it. This obviously won't happen because there is no return in the first if-statement.

qalmakka · 5 days ago
Yeah, the problem is that some old web APIs have been clearly hacked up haphazardly in the '90s, probably in a hurry, and now we have to live with the consequences of that. This is not unique with the Web though, it's basically the same with the entirety of the WinAPI and most libc functions in my experience
buzzin__ · 5 days ago
So, your argument is that Rust is better because better programers use Rust.

I specifically mean this part: "Rust would never have such a dumb library design".

One could then also say that Rust programmers would never make such a cyclical argument.

johnfn · 5 days ago
If you want to be more charitable, you could say "Rust library design is superior to the Web API library design", and I'd say you were right - particularly for crufty stuff like .href which was designed decades ago.
merdaverse · 5 days ago
Code written below your line gets executed if you don't return early. More breaking news at 8.

Seriously, why would you think that assigning a value would stop your script from executing? Maybe the Typescript example is missing some context, but it seems like such a weird case to present as a "data race".

Arch-TK · 5 days ago
Assigning to `window.location.href` has a side effects. The side effect is that your browser will navigate to wherever you assigned, as if you had clicked a link. This is already a surprising behaviour, but given that this assignment is effectively loading a new page in-place, kind of like how `execve` does for a process, I can totally see how someone would think that JS execution would stop immediately after a link is clicked.

It's obviously not a good idea to rely on such assumptions when programming, and when you find yourself having such a hunch, you should generally stop and verify what the specification actually says. But in this case, the behaviour is weird, and all bets are off. I am not at all surprised that someone would fall for this.

stouset · 5 days ago
Part of the problem is that we unknowingly make a million little assumptions every day in the course of software development. Many of them are reasonable, some of them are technically unreasonable but fine in practice, and some of them are disasters waiting to happen. And it's genuinely hard to not only know which are which, but to notice even a fraction of them in the first place.

I'm sure I knew the href thing at one point. It's probably even in the documentation. But the API itself leaves a giant hole for this kind of misunderstanding, and it's almost certainly a mistake that a huge number of people have made. The more pieces of documentation we need to keep in our heads in order to avoid daily mistakes, the exponentially more likely it is we're going to make them anyway.

Good software engineering is, IMHO, about making things hard to hold the wrong way. Strong types, pure functions without side effects (when possible), immutable-by-default semantics, and other such practices can go a long way towards forming the basis of software that is hard to misuse.

dkarl · 5 days ago
> when you find yourself having such a hunch, you should generally stop and verify what the specification actually says

It greatly heartens me that we've made it to the point where someone writing Javascript for the browser is recommended to consult a spec instead of a matrix of browsers and browser versions.

However, that said, why would a person embark on research instead of making a simple change to the code so that it relies on fewer assumptions, and so that it's readable and understandable by other programmers on their team who don't know the spec by heart?

ngruhn · 5 days ago
I use JavaScript for ~15 years. I thought it worked like that.
hsbauauvhabzb · 5 days ago
Is this a JavaScript wart or a browser wart though? JavaScript is communicating to the browser via an API and rust would need to do the same.
lights0123 · 5 days ago
exit(), execve(), and friends do immediately stop execution—I could understand why you'd think a redirect would as well.
JoshTriplett · 5 days ago
Exactly. Given that JavaScript runs in the context of a page, redirecting off of the page seems like it should act like a "noreturn" function...but it doesn't. That seems like a very easy mistake to make.
sintax · 5 days ago
Until they don't. A common issue is not checking if the execve() actually worked and thinking nothing after the execve() will execute, which is an assumption that it not always true.
jacquesm · 5 days ago
See, that's what they meant about making assumptions upthread:

https://man7.org/linux/man-pages/man3/atexit.3.html

dvt · 5 days ago
The redirect is an assignment. In no language has a variable assignment ever stopped execution.
love2read · 5 days ago
It seems weird to shame someone for talking about their own experience?
Humphrey · 5 days ago
Whether you think that or not is not the issue - the fix is very obvious once pointed out to you. The arguement the author is making is that a bug like that TS issue can be very difficult and time consuming to track down and is not picked up on by the compiler.
IshKebab · 5 days ago
Whenever someone talks about a surprising paper cut like this you always see misguided "this is obvious" comments.

No shit. It's obvious because you literally just read a blog post explaining it. The point is if you sprinkle dozens of "obvious" things through a large enough code based, one of them is going to bite you sooner or later.

It's better if the language helps you avoid them.

masklinn · 5 days ago
> Seriously, why would you think that assigning a value would stop your script from executing?

This assignment has a significant side-effect of leaving the page, assuming this is immediate rather than a scheduled asynchronous action is not unfair (I’m pretty sure I assumed the same when I saw or did that).

jemiluv8 · 5 days ago
This is more a control flow issue than a data race issue. I've seen this countless times. And it is often a sign that you don't spend too much time writing JavaScript/Typescript. You get shot in the foot by this very often. And some linters will catch this - most do actyally
drdrey · 5 days ago
OP thought the redirect was synchronous, not that it would stop the script from executing
jibal · 5 days ago
No, you're mistaken. Read the other comments under the parent. If it were synchronous then it would have stopped the script from executing, much the way POSIX exec() works. If the OP didn't think that the script would stop, then why would he let execution fall through to code that should not execute ... which he fixed by not letting it fall through?
BinaryIgor · 5 days ago
Don't most of the benefits just come down to using a statically typed and thus compiled language? Be it Java, Go or C++; TypeScript is trickier, because it compiles to JavaScript and inherits some issues, but it's still fine.

I know that Rust provides some additional compile-time checks because of its stricter type system, but it doesn't come for free - it's harder to learn and arguably to read

pornel · 5 days ago
To a large extent yes, but Rust adds more dimensions to the type system: ownership, shared vs exclusive access, thread safety, mutually-exclusive fields (sum types).

Ownership/borrowing clarifies whether function arguments are given only temporarily to view during the call, or whether they're given to the function to keep and use exclusively. This ensures there won't be any surprise action at distance when the data is mutated, because it's always clear who can do that. In large programs, and when using 3rd party libraries, this is incredibly useful. Compare that to that golang, which has types for slices, but the type system has no opinion on whether data can be appended to a slice or not (what happens depends on capacity at runtime), and you can't lend a slice as a temporary read-only view (without hiding it behind an abstraction that isn't a slice type any more).

Thread safety in the type system reliably catches at compile time a class of data race errors that in other languages could be nearly impossible to find and debug, or at very least would require catching at run time under a sanitizer.

zelphirkalt · 5 days ago
What annoys me about borrowing is, that my default mode of operating is to not mutate things if I can avoid it, and I go to some length in avoiding it, but Rust then forces me to copy or clone, to be able to use things, that I won't mutate anyway, after passing them to another procedure. That creates a lot of mental and syntactical overhead. While in an FP language you are passing values and the assumption is already, that you will not mutate things you pass as arguments and as such there is no need to have extra stuff to do, in order to pass things and later still use them.

Basically, I don't need ownership, if I don't mutate things. It would be nice to have ownership as a concept, in case I do decide to mutate things, but it sucks to have to pay attention to it, when I don't mutate and to carry that around all the time in the code.

w10-1 · 5 days ago
Readers would benefit from distinguishing effects systems from type systems - error handling, async code, ownership, pointers escaping, etc. are all better understood as effects because they pertain to usage of a value/type (though the usage constraints can depend on the type properties).

Similarly, Java sidesteps many of these issues in mostly using reference types, but ends up with a different classes of errors. So the C/pointer family static analysis can be quite distinct from that for JVM languages.

Swift is roughly on par with Rust wrt exclusivity and data-race safety, and is catching up on ownership.

Rust traits and macros are really a distinguishing feature, because they enable programmer-defined constraints (instead of just compiler-defined), which makes the standard library smaller.

bpicolo · 5 days ago
I think tagged unions with exhaustive type checking and no nulls are the two killer features for correctness
mason_mpls · 5 days ago
Apologies for the non sequitur

Do you think Zig is a valid challenger to Rust for this kind of programming?

arwhatever · 5 days ago
I might suspect that if you are lumping all statically-typed languages into a single bucket without making particular distinction among them, then you might not have fully internalized the implications of union (aka Rust enum aka sum) typed data structures combined with exhaustive pattern matching.

I like to call it getting "union-pilled" and it's really hard to accept otherwise statically-typed languages once you become familiar.

JoshTriplett · 5 days ago
Or the fact that Rust's type system includes things like Send and Sync, which aren't tracked and enforced in many otherwise-statically-typed languages.

C is statically typed, but its type system tracks much less.

ModernMech · 5 days ago
enums + match expressions + tagged unions are the secret sauce of Rust.
b_e_n_t_o_n · 5 days ago
Afaik they aren't true unions but sum types, which have different implications.

And fwiw I've used unions in typescript extensively and I'm not convinced that they're a good idea. They give you a certain flexibility to writing code, yes, does that flexibility lead to good design choices, idk.

jauntywundrkind · 5 days ago
One other major factor I'd throw on the heap: traits / implementation traits. They act as both an interface system and as a sort of Extension Method system (as seen in c#).

But where-as with interfaces, typically they require you early define what your class implements. Rust gives you a late-bound-ish (still compile time but not defined in the original type) / Inversion of Control way to take whatever you've got and define new things for it. In most languages what types a thing has are defined by the library, but Rust not just allows but is built entirely around taking very simple abstract thing and constructing bigger and bigger toolkits of stuff around them. Very Non-zero sum in ways that languages rarely are.

There's a ton of similarity to Extension Methods, where more can get added to the type. But traits / impls are built much more deeply into rust, are how everything works. Extension Methods are also, afaik, just methods, where-as with Rust you really adding new types that an existing defined-elsewhere thing can express.

I find it super shocking (and not because duh) that Rust's borrow checking gets all the focus. Because the type system is such a refreshing open ended late-defined reversal of type system dogma, of defining everything ahead of time. It seems like such a superpower of Rust that you can keep adding typiness to a thing, keep expanding what a thing can do. The inversion here is, imo, one of the real largely unseen sources of glory for why Rust keeps succeeding: you don't need to fully consider the entire type system of your program ahead of time, you can layer in typing onto existing types as you please, as fits, as makes sense, and that is a far more dynamic static type system than the same old highly constrained static type dreck we've suffered for decades. Massive break forward: static, but still rather dynamic (at compile time).

pjmlp · 5 days ago
An approach that is done in Standard ML with functors.
dwaltrip · 5 days ago
Any good articles or blog posts that go deeper on this? Sounds very interesting
gf000 · 5 days ago
I mean, is that not just [1] type classes? They are not a new concept, they could be grandparents!

[1] not trying to take away anything from the designers, getting it right in combination with all the other features is a huge feat!

ModernMech · 5 days ago
> statically typed and thus compiled

Statically typed does not imply compiled. You can interpret a statically typed language, for instance. And not every compiled language is all that static.

For example, C is statically typed, but also has the ability to play pointer typecasting trickery. So how much can the compiler ever guarantee anything, really? It can't, and we've seen the result is brittle artifacts from C.

Rust is statically-typed and it has all kinds of restrictions on what you can do with those types. You can't just pointer cast one thing to another in Rust, that's going to be rejected by the compiler outright. So Rust code has to meet a higher bar of "static" than most languages that call themselves "static".

Type casting is just one way Rust does this, other ways have been mentioned. They all add up and the result is Rust artifacts are safter and more secure.

tialaramex · 5 days ago
> You can't just pointer cast one thing to another in Rust, that's going to be rejected by the compiler

You can't safely do this yourself. That is, you couldn't write safe Rust which performs this operation for two arbitrary things. But Rust of course does do this, actually quite a lot, because if we're careful it's entirely safe.

That famous Quake 3 Arena "Fast inverse square root" which involves type puns? You can just write that in safe Rust and it'll work fine. You shouldn't - on any even vaguely modern hardware the CPU can do this operation faster anyway - but if you insist it's trivial to write it, just slower.

Why can you do that? Well, on all the hardware you'd realistically run Rust on the 32-bit integer types and the 32-bit floating types are the exact same size (duh), same bit order and so on. The CPU does not actually give a shit whether this 32-bit aligned and 32-bit sized value "is" an integer or a floating point number, so "transforming" f32 to u32 or u32 to f32 emits zero CPU instructions, exactly like the rather hairier looking C. So all the Rust standard library has to do is promise that this is OK which on every supported Rust platform it is. If some day they adopted some wheezing 1980s CPU where that can't work they'd have to write custom code for that platform, but so would John Carmack under the same conditions.

1718627440 · 5 days ago
> So how much can the compiler ever guarantee anything, really?

Well, the compiler is guaranteed that no mistakes will happen. It's the programmer who looses his guarantees in this case.

marcosdumay · 5 days ago
> Don't most of the benefits just come down to using a statically typed and thus compiled language?

Doesn't have to be compiled to be statically typed... but yeah, probably.

> Be it Java, Go or C++;

Lol! No. All static type systems aren't the same.

TypeScript would be the only one of your examples that brings the same benefit. But the entire system is broken due to infinite JS Wats it has to be compatible with.

> it's harder to learn and arguably to read

It's easier to learn it properly, harder to vibe pushing something into it until it seems to works. Granted, vibe pushing code into seemingly working is a huge part of initial learning to code, so yeah, don't pick Rust as your first language.

It's absolutely not harder to read.

akkad33 · 5 days ago
No. Other languages don't prevent concurrency related bugs like the one in the article. Rust has reference aliasing rules and lifetimes and send and sync traits. These things do not exist in Java and others, so will not prevent such bugs
rvz · 5 days ago
> Don't most of the benefits just come down to using a statically typed and thus compiled language? Be it Java, Go or C++; TypeScript is trickier, because it compiles to JavaScript and inherits some issues, but it's still fine.

Yes. The type systems of these modern compiled languages are more sound than anything that Javascript and Typescript can ever provide.

Anyone using such languages that have a totally weak type system and a dynamic typing system as well is going to run into hundreds of headaches - hence why they love properly typed-systems such as Rust which actually is a well designed language.

rendaw · 5 days ago
Not all static type systems are equally expressive/safe/consistent - Java falls back to `Object` and runtime casts frequently, Go doesn't have enums, and C++ variants come with significant footguns and ergonomics issues since they were hacked on and aren't first class language features (i.e. safe access requires using `try/except` which is exclusive with other control structures).
gf000 · 5 days ago
> Java falls back to `Object` and runtime casts frequently

Is it frequently? Generics are definitely not as nice as they could be, but they are surprisingly "sufficient" for almost any library, e.g. a full on type-safe SQL DSL like JOOQ. Unsafe casts are very rare, and where you do need Object and stuff are very dynamic stuff where it's impossible to extend compile-time guarantees even theoretically (e.g. runtime code generation, dynamic code loading, etc - people often forget about these use cases, not everything can work in a closed universe)

saghm · 5 days ago
Yes, all four of them will have some checks that won't be present in a dynamic language, but the differences between them are large enough to be significant. Riding a bike and driving a car are both much faster than going on foot, but if you only view this as a "benefit that comes down to using wheels", you're missing some pretty meaningful details.
csomar · 5 days ago
Yes. What Rust adds is a better type system. Statically typed is as good as your Type system (which is why TypeScript still sucks).

The concurrency/safety/memory story is only valid in a few rare cases and I wish people didn't try to sell Rust for these features.

ViewTrick1002 · 5 days ago
Neither Go, Java or C++ would catch that concurrency bug.
Const-me · 5 days ago
C# would catch the bug at compile time, just like Rust.

https://www.rocksolidknowledge.com/articles/locking-asyncawa...

lenkite · 5 days ago
> Neither Go, Java or C++ would catch that concurrency bug.

That is incorrect. Java enforces that a monitor lock (or Lock) must be released by the same thread that acquired it. Attempting to unlock from a different thread throws IllegalMonitorStateException.

lmm · 5 days ago
> Don't most of the benefits just come down to using a statically typed and thus compiled language? Be it Java, Go or C++; TypeScript is trickier, because it compiles to JavaScript and inherits some issues, but it's still fine.

No. You have to have a certain amount of basic functionality in your type system; in particular, sum types, which surprisingly many languages still lack.

(Note that static typing does not require compilation or vice versa)

> I know that Rust provides some additional compile-time checks because of its stricter type system, but it doesn't come for free - it's harder to learn and arguably to read

ML-family languages are generally easier to learn and read if you start from them. It's just familiarity.

eptcyka · 5 days ago
The specific example used wouldn’t be caught by C++ or Java.
gf000 · 5 days ago
The redirect one would funnily be caught in Java. For the simple reason of not having properties, it would have to be a method call, where the author's wrong assumption would no longer apply.
adamnemecek · 5 days ago
Rust is way more productive than any of the listed languages.
gf000 · 5 days ago
Extraordinary claims require.. at least some kind of evidence.

There is very little research comparing PL productivity, because it is very hard to do them properly.

BobbyJo · 5 days ago
IMO, most of the terse syntax in Rust comes from the sugar they've added for error handling.
keybored · 5 days ago
You ask a question in your first paragraph which you answer in the second.
stocksinsmocks · 5 days ago
Yes, but more importantly writing a program that compiles in Rust guarantees you a front page spot on HN.
raphinou · 5 days ago
Though it's not the only benefit, I enjoy rust and fsharp's typesystems most when refactoring code. Fearless refactoring is the right expression here.
estebank · 5 days ago
The only issue with it is that Rust's aversion to half-baked code means that you can't have "partially working code" during the refactor: you either finish it or bail on it, without the possibility to have inconsistent codebase state. This is particularly annoying for exploratory code.

On the other hand, that strictness is precisely what leads people to end up with generally reasonable code.

tialaramex · 5 days ago
I find a healthy dose of todo!() is excellent for this.

    match foo {
      (3...=5, x, BLABLABLA) => easy(x),
      _ => todo!("I should actually implement this for non-trivial cases"),
    }
The nice thing about todo!() is that it type checks, obviously it always diverges so the type match is trivial, but it means this compiles and, so long as we don't cause the non-trivial case to happen, it even works at runtime.

Cthulhu_ · 5 days ago
It's a tradeoff, reminds me of Go not compiling if you have an unused variable; the strictness is a feature and basically locks out sloppy / half baked code.

I personally see Rust as an ideal "second system" language, that is, you solve a business case in a more forgiving language first, then switch (parts) to Rust if the case is proven and you need the added performance / reliability.

raphinou · 5 days ago
Fsharp's type inference is great in that regard. Function types are inferred too, in contrast with rust. I find fsharp fit for exploratory code, and its type inference is probably the enabler.
veber-alex · 5 days ago
I find the Zig example to be shocking.

It's just so brittle. How can anyone think this is a good idea?

tialaramex · 5 days ago
I assume it's a bug. However because this is an auteur language if you want the bug fixed it will be important to ensure the auteur also thinks it is a bug. If they get it into their head that it's supposed to be like this, they'll double down and regardless of how many people are annoyed they're insist on it.
ozgrakkurt · 5 days ago
Tbh you would just use ErrorTypeName.ErrorKind to check equality and not error.ErrorKind if you are worried about this.

It is a tradeoff between making some things easier. And probably compiler is not mature enough to catch this mistake yet but it will be at some point.

Zig being an auteur language is a very good thing from my perspective, for example you get this new IO approach which is amazing and probably wouldn’t happen if Andrew Kelley wasn’t in the position he is in.

I have been using Rust to write storage engines past couple years and it’s async and io systems have many performance mistakes. Whole ecosystem feels like it is basically designed for writing web servers.

An example is a file format library using Io traits everywhere and using buffered versions for w/e reason. Then you get a couple extra memcopy calls that are copying huge buffers. Combined with global allocation everywhere approach, it generates a lot of page faults which tanks performance.

Another example was, file format and just compute libraries using asyncio traits everywhere which forces everything to be send+sync+’static which makes it basically impossible to use in single thread context with local allocators.

Another example is a library using vec everywhere even if they know what size they’ll need and generating memcopies as vec is getting bigger. Language just makes it too easy.

I’m not saying Rust is bad, it is a super productive ecosystem. But it is good that Zig is able to go deeper and enable more things. Which is possible because one guy can just say “I’ll break the entire IO API so I can make it better”.

veber-alex · 5 days ago
Knowing how the Zig developers operate it's 100% not a bug and it's exactly the case you described.
gwenzek · 4 days ago
Andrew actually agrees with the general sentiment. And this will be made into a compile error.

What's happening is that compiler knows the two errors come from disjoint error set, but it promotes them both to anyerror

Details at https://github.com/ziglang/zig/issues/25046

Ericson2314 · 5 days ago
I'm smirking big time reading this :), well said

(To be clear to others, it's not even that this is 100% a bad thing, but people love to shit on "design by committee" so much, it helps to have a bit of the opposite)

jibal · 5 days ago
Why would you assume that? It's very intentional design decision.
kouteiheika · 5 days ago
Every language has questionable design decisions that lead to brittle code, although some more than others.

Like, how can anyone think that requiring the user to always remember to explicitly write `mutex.unlock()` or `defer mutex.unlock()` instead of just allowing optional explicit unlock and having it automatically unlock when it goes out of scope by default is a good idea? Both Go and Zig have this flaw. Or, how can anyone think that having a cast that can implicitly convert from any numeric type to any other in conjunction with pervasive type inference is a good idea, like Rust's terrible `as` operator? (I once spent a whole day debugging a bug due to this.)

veber-alex · 5 days ago
You are right, but it doesn't mean we can't complain about it :)

As a side note, I hate the `as` cast in Rust. It's so brittle and dangerous it doesn't even feel like a part of the language. It's like a JavaScript developer snuck in and added it without anyone noticing. I hope they get rid of it in an edition.

vjerancrnjak · 5 days ago
I would like a language where your call stack can't be bigger than two/three.

You can call functions inside your function Main, but these function can't call any functions anymore (exception being flat helper functions defined inside your function).

I think it would save a huge chunk of time by just having all programs really nice and flat. You'd naturally gravitate towards mechanisms that make programs flat.

zparky · 5 days ago
I didn't even see the error after the first glance until I read your comment.
WD-42 · 5 days ago
I saw this comment first and then read the zig and still couldn’t find the error until I read the explanation below it. This has to be a bug.
veber-alex · 5 days ago
Yeah, I had to do a double take while reading the article because I missed the error completely while looking at the code.
geysersam · 5 days ago
Can't a linter catch that you're referring to an error that doesn't exist anywhere else in your system and warn you about that and suggest you use switch instead of if?
Phil_Latio · 5 days ago
I would've assumed the error set is generated based on function signatures. Sick stuff.
suriya-ganesh · 5 days ago
I vibe with this entirely.

Rust to me makes a lot more sense. The compiler gives reasonable errors, the code structure is clean and it's obvious where everything should go.

I just can't deal with Typescript at all. There is a sense of uncertainty in TypeScript that is just unbearable. It is really hard to be confident about the code.

aeve890 · 5 days ago
>The code will compile just fine. The Zig compiler will generate a new number for each unique 'error.*'

This is wild. I assume there's at least the tooling to catch this kind of errors right?

TUSF · 5 days ago
There's no special tooling to catch this, because nobody catches an error with if-else—it's simply not idiomatic. Everyone uses switch statements in the catch block, and I've never seen anybody using anything other than switch, when catching an error.
dminik · 5 days ago
Both the Zig standard library as well as several third party projects do check errors like this.

I already commented on Zig compiler/stdlib code itself, but here's Tigerbeetle and Bun, the two biggest(?) Zig codebases:

https://github.com/search?q=repo%3Atigerbeetle%2Ftigerbeetle...

https://github.com/search?q=repo%3Aoven-sh%2Fbun%20%22%3D%3D...

veber-alex · 5 days ago
But why?

If I just need to check for 1 specific error and do something why do I need a switch?

In Rust you have both "match" (like switch) and "if let" which just pattern matches one variant but both are properly checked by the compiler to have only valid values.

arwalk · 5 days ago
The real problem is not about the if-else, its that he's comparing to the global error set, and not to the FileError error set he created specifically to define AccessDenied.
love2read · 5 days ago
“There’s no specific tooling to catch this, because nobody[…]”. So? This is a standard library/language feature, which is usually the first place people go for features in the language. To say that nobody uses it is stupid.
matklad · 5 days ago
This is compiler bug, the code should not compile (see issue #25046)
arwalk · 5 days ago
The example is a bit dubious. Sure, it compiles just fine, because the author is not using errors properly in zig. Here, he uses the global error set with `error. AccessDenid`, and as stated, it compiles just fine because when you reach the global error set, it's integers all the way down.

If the author had written `FileError.AccessDenid`, this would not have compiled, as it would be comparing with the `FileError` error set.

The global error set is pretty much never used, except when you want to allow a user to provide his own errors, so you allow the method to return `anyerror`.

dminik · 5 days ago
You say never, but even the Zig stdlib does this occasionally.

Like here in `std/tar.zig`: https://github.com/ziglang/zig/blob/50edad37ba745502174e49af...

Or here in `main.zig`: https://github.com/ziglang/zig/blob/50edad37ba745502174e49af...

And in a bunch of other places: https://github.com/search?q=repo%3Aziglang%2Fzig+%22%3D%3D+e...

empath75 · 5 days ago
All examples of this type come down to "the user made a mistake", but that is kind of the the entire point. It is not possible to make that mistake in rust.
veber-alex · 5 days ago
Why does the compiler decay the FileError and the global error set to an integer? If they were unique types, the if statement would not have compiled.