I write production Rust code that becomes critical infra for our customers. I got tired of nil checks in Go and became a squeaky wheel in incident retros, where I finally got the chance to rewrite parts of our system in Rust during a refactor.
I admit the skill issue on my part, but I genuinely struggled to follow the concepts in this article. Working alongside peers who push Rust's bleeding edge, I dread reviewing their code and especially inheriting "legacy" implementations. It's like having a conversation with someone who expresses simple thoughts with ornate vocabulary. Reasoning about code written this way makes me experience profound fatigue and possess an overwhelming desire to return to my domicile; Or simply put, I get tired and want to go home.
Rust's safety guardrails are valuable until the language becomes so complex that reading and reasoning about _business_ logic gets harder, not easier. It reminds me of the kid in "A Christmas Story" bundled so heavily in winter gear he cant put his arms down[0]. At some point, over-engineered safety becomes its own kind of risk even though it is technically safer in some regards. Sometimes you need to just implement a dang state machine and stop throwing complexity at poorly thought-through solutions. End old-man rant.
Look, included in the concepts the article discusses are features that people have been wanting for a very long time.
Like, the ability to have multiple mut references to a struct as long as you access disjoint fields? That's amazing!!! A redo of Pin that actually composes with the rest of the language? That's pretty awesome too.
I think you're getting tied up because the the author is describing these features in a very formal way. But someone has to think about these things, especially if they are going to implement them.
Ultimately, these are features that will make Rust's safety features (which really, are Rust's reason for existing) more ergonomic and easier to use. That's the opposite of what you fear.
> I genuinely struggled to follow the concepts in this article
I read the HN comments before I read the OP, which made me worry that the post was going to be some hifalutin type wonkiness. But instead the post is basically completely milquetoast, ordinary and accessible. I'm no type theorist--I cannot tell you what a monad is--and I confess that I don't understand how anyone could be intimidated by this article.
I feel you, but hear me out. OP is right. I've wanted pretty much everything he's talking about here for years, I just never thought of all of this in as quite a formal way as he has. We need the ability to say "this piece of code can't panic". It's super important in the domains I work in. We also need the ability to say "this piece of code can't be non-deterministic". It's also super important in the domains I work in. Having language level support for something like this where I add an extra word to my function def and the compiler guarantees the above would be groundbreaking
I have no argument against using the right tool for a job. Decorating a function with a keyword to have more compile-time guarantees does sound great, but I bet it comes with strings attached that affect how it can be used which will lead to strange business logic. Anecdotally, I have not (perhaps yet) run into a situation where I needed more language features, I felt rust had enough primatives that I could adapt the current feature set to my needs. Yes, at times I had to scrap what I was working on to rewrite it another way so I can have compile-time guarantees. Yes, here language features offer speed in implementing.
Could you share a situation where the behavior is necessary? I am curious if I could work around it with the current feature set.
Perhaps I take issue with peers that throw bleeding edge features in situations that don't warrant them. Last old-man anecdote: as a hobbyist woodworker it pains me to see people buying expensive tools to accomplish something. They almost lack creativity to use their current tools they have. "If I had xyz tool I would build something so magnificent" they say. This amounts to having many, low-quality single-purpose built tools where a single high-quality table saw could fit the need. FYI, a table-saw could suit 90% of your cutting/shaping needs with a right jig. I don't want this to happen in rust.
> Could you share a situation where the behavior is necessary?
The effects mentioned in the article are not too uncommon in embedded systems, particularly if they are subject to more stringent standards (e.g., hard realtime, safety-critical, etc.). In such situations predictability is paramount, and that tends to correspond to proving the absence of the effects in the OP.
> Could you share a situation where the behavior is necessary? I am curious if I could work around it with the current feature set.
But this kinda isn't about "behavior" of your code; it's about how the compiler (and humans) can confidently reason about your code?
IMO rust started at this from the wrong direction. Comparing to something like zig which just cannot panic unless the developer wrote the thing that does the panic, cannot allocate unless the developer wrote the allocation, etc.
Rust instead has all these implicit things that just happen, and now needs ways to specify that in particular cases, it doesn't.
The problem isn't implicit things happening.
He's talking about this problem. Can this code panic?
You can't easily answer that in Rust or Zig. In both cases you have to walk the entire call graph of the function (which could be arbitrarily large) and check for panics. It's not feasible to do by hand. The compiler could do it though."Panic-free" labels are so difficult to ascribe without being misleading because temporal memory effects can cause panics. Pusher too much onto your stack because the function happened to be preceded by a ton of other stack allocations? Crash. Heap too full and malloc failed? Crash. These things can happen from user input, so labelling a function no_panic just because it doesn't do any unchecked indexing can dangerously mislead readers into thinking code can't crash when it can.
There's plenty of independent interest in properly bounding stack usage because this would open up new use cases in deep embedded and Rust-on-the-GPU. Basically, if you statically exclude unbounded stack use, you don't even need memory protection to implement guard pages (or similar) for your call stack usage, which Rust now requires. But this probably requires work on the LLVM side, not just on Rust itself.
Failable memory allocations are already needed for Rust-on-Linux, so that also has independent interest.
What about doing something that Java does with the throws keyword? Would that make the checking easier?
I think that's exactly what's being asked for here (via the "panic effect" that the article refers to)
although, I think i'd prefer a "doesn't panic" effect just to keep backwards compatibility (allowing functions to panic by default)
Or effect aliases. But given that it's strictly a syntactic transformation it seems like make the wrong default today, fix it in the next edition. (Editions come with tools to update syntax changes)
Huh? It seems to me that in these respects the two languages are almost identical. If I tell the program to panic, it panics, and if I divide an integer by zero it... panics and either those are both "the developer wrote the thing" or neither is.
In Zig, dividing by 0 does not panic unless you decide that it should or go out of your way to use unsafe primitives [1]. Same for trying to allocate more memory than is available. The general difference is as follows (IMO):
Rust tries to prevent developers from doing bad things, then has to include ways to avoid these checks for cases where it cannot prove that bad things are actually OK. Zig (and many others such as Odin, Jai, etc.) allow anything by default, but surface the fact that issues can occur in its API design. In practice the result is the same, but Rust needs to be much more complex both to do the proving and to allow the developers to ignore its rules.
[1]: https://ziglang.org/documentation/0.15.2/std/#std.math.divEx...
Could you clarify what's going on in the Zig docs[0], then? My reading of them is that Zig definitely allows you to try to divide by 0 in a way the compiler doesn't catch, and this results in a panic at runtime.
I'd be interested if this weren't true, since the only feasible compiler solutions to preventing division-by-0 errors are either: defining the behaviour, which always ends up surprising people later on, or; incredibly cumbersome or underperformant type systems/analyses which ensure that denominators are never 0.
It doesn't look like Zig does either of these.
[0]: https://ziglang.org/documentation/master/#Division-by-Zero
> the only feasible compiler solutions to preventing division-by-0 errors are either: defining the behaviour, which always ends up surprising people later on, or; incredibly cumbersome or underperformant type systems/analyses which ensure that denominators are never 0.
I don't think it's very cumbersome if the compiler checks if the divisor could be zero. Some programming languages (Kotlin, Swift, Rust, Typescript...) already do something similar for possible null pointer access: they require that you add a check "if s == null" before the access. The same can be done for division (and remainder / modulo). In my own programming language, this is what I do: you can not have a division by zero at runtime, because the compiler does not allow it [1]. In my experience, integer division by a variable is not all that common in reality. (And floating point division does not panic, and integer division by a non-zero constant doesn't panic either). If needed, one could use a static function that returns 0 or panics or whatever is best.
[1] https://github.com/thomasmueller/bau-lang/blob/main/README.m...
More specifically, Zig will return an error type from the division and if this isn't handled THEN it will panic, kind of like an exception except it can be handled with proper pattern matching.
I can't find anything related to division returning an error type. Looking at std.math.divExact, rem, mod, add, sub, etc. it looks to me like you're expected to use these if you don't want to panic.
Actually you're right, I was going by the source code which was in the link of the comment you replied to, but I missed that that was specifically for divExact and not just primitive division.
> Comparing to something like zig which just cannot panic unless the developer wrote the thing that does the panic
The zig compiler can’t possibly guarantee this without knowing which parts of the code were written by you and which by other people (which is impossible).
So really it’s not “the developer” wrote the thing that does the panic, it’s “some developer” wrote it. And how is that different from rust?
Perhaps there are similarities to Scala, from my anecdotal observation. Coming from Java and doing the Scala coursera course years ago, it feels like arriving in a candy shop. All the wonderful language features are there, true power yours to wield. And then you bump into the code lines crafted by the experts, and they are line for line so 'smart' they take a real long time to figure out how the heck it all fits together.
People say "Rust is more complex to onboard to, but it is worth it", but a lot of the onboarding hurdle is the extra complexity added in by experts being smart. And it may be a reason why a language doesn't get the adoption that the creators hoped for (Scala?). Rust does not have issues with popularity, and the high onboarding barrier, may have positive impact eventually where "Just rewrite it in Rust" is no more, and people only choose Rust where it is most appropriate. Use the right language for the tool.
The complexity of Rust made me check out Gleam [0], a language designed for simplicity, ease of use, and developer experience. A wholly different design philosophy. But not less powerful, as a BEAM language that compiles to Erlang, but also compiles to Javascript if you want to do regular web stuff.
[0] https://gleam.run
At least from what I’ve seen around me professionally, the issue with most Scala projects was that developers started new projects in Scala while also still learning Scala through a Coursera course, without having a FP background and therefore lacking intuition/experience on which technique to apply when and where. The result was that you could see “more advanced” Scala (as per the course progression) being used in newer code of the projects. Then older code was never refactored resulting in a hodgepodge of different techniques.
This can happen in any language and is more indicative of not having a strong lead safeguarding the consistency of the codebase. Now Scala has had the added handicap of being able to express the same thing in multiple ways, all made possible in later iterations of Scala, and finally homogenised in Scala 3.
I honestly just don't believe that Rust is more complex to onboard to compared to languages like Python. It just does not match my experience at all. I've been a professional rust developer for about three years. Every time I look at python code, it's doing something insane where the function argument definition basically looks like line noise with args and kwargs, with no types, so it's impossible to guess what the parameeters will be for any given function. Every python developer I know makes heavy use of the repl just to figure out what methods they can call on some return value of some underdocumented method of a library they're using. The first time I read pandas code, I saw something along the lines of df[df["age"] < 3] and thought I was having a stroke. Yet python has a reputation for being easy to learn and use. We have a python developer on our team and it probably took me about a day to onboard him to rust and get him able to make changes to our (fairly complicated) Rust codebase.
Don't get me wrong, rust has plenty of "weird" features too, for example higher rank trait bounds have a ridiculous syntax and are going to be hard for most people to understand. But, almost no one will ever have to use a higher rank trait bound. I encounter such things much more rarely in rust than in almost any other mainstream language.
> I honestly just don't believe that Rust is more complex to onboard to compared to languages like Python.
Most people conflate "complexity" and "difficulty". Rust is a less complex language than Python (yes, it's true), but it's also much more difficult, because it requires you to do all the hard work up-front, while giving you enormously more runtime guarantees.
Doing the hard work up front is easier than doing it while debugging a non-trivial system. And there are boilerplate patterns in Rust that allow you to skip the hard work while doing throwaway exploratory programming, just like in "easier" languages. Except that then you can refactor the boilerplate away and end up with a proper high-quality system.
The language itself is not more complex to onboard. For Scala also not. It feels great to have all these language features to ones proposal. The added complexity is in the way how expert code is written. The experts are empowered and productive, but heightens the barrier of entry for newcomers by their practices. Note that they also might expertly write more accessible code to avoid the issue, and then I agree with (though I can't compare to Python, never used it).
Hm, you claim that Rust and Scala are not more complex to onboard than Python... but then you say you never used Python? If that's the case, how do you know? Having used both, I do think Rust is harder to onboard, just because there is more syntax that you need to learn. And Rust is a lot more verbose. And that's before you are exposed to the borrow checker.
> And then you bump into the code lines crafted by the experts, and they are line for line so 'smart' they take a real long time to figure out how the heck it all fits together.
Thing is, the alternative to "smart" code that packs a lot into a single line is code where that line turns into multiple pages of code, which is in fact worse for understanding. At least with PL features, you only have to put in the work once and you can grok how they're meant to be used anywhere.
You can see:
* no-panic: https://docs.rs/no-panic/latest/no_panic/
* Safe Rust has no undefined behavior: https://news.ycombinator.com/item?id=39564755
I've been on both sides of the fence here - I've bounced between two camps:
1. Go with a better type system. A compiled language, that has sum types, no-nil, and generics.
2. A widely used, production, systems language that implements PL-theory up until the year ~2000. (Effects, as described in this article, was a research topic in 1999).
I started with (1), but as I started to get more and more exposed to (2), you start looking back on times when you fought with the type system and how some of these PL-nerds have a point. I think my first foray into Higher-Kinded Types was trying to rewrite a dynamic python dispatch system into Rust while keeping types at compile time.
The problem is, many of these PL-nerd concepts are rare and kind of hard to grok at first, and I can easily see them scaring people off from the language. However I think once you understand how they work, and the PL-nerds dumb down the language, I think most people will come around to them. Concepts like "sum types" and "monads", are IMO easy to understand concepts with dumb names, and even more complex standard definitions.
Yeah, (non sarcastically) give those things dumb names like borrow checker and you'll get people swooning over it.
> 1. Go with a better type system. A compiled language, that has sum types, no-nil, and generics.
I was looking for something like that and eventually found Crystal (https://crystal-lang.org) as a closest match: LLVM compiled, strong static typing with explicit nulls and very good type inference, stackfull coroutines, channels etc.
Crystal is a typed Ruby. The closer to Go language would probably be Odin.
Odin is more of an alternative C than Go. V is inspired by Go has like nearly the same syntax as Go and alot of Goodies. Like Error Types, Sum Types and more.
Crystal’s syntax is similar to Ruby’s, but AFAIK the similarity more-or-less ends there.
A state machine is a perfect example of a case where you would benefit from linear types.
Some things just need precise terminology so humans can communicate about them to humans without ambiguity. It doesn't mean they're inherently complex: the article provides simple definitions. It's the same for most engineering, science and language. One of the most valuable skills I've learned in my career is to differentiate between expressions, statements, items, etc. - how often have you heard that the hardest problem in software development is coordinating with other developers? If you learn proper terminology, you can say exactly what you mean. Simple language doesn't mean more clear.
I wasn't born knowing Rust, I had to learn it. So I'm always surprised by complaints about Rust being too complex directed at the many unremarkable people who have undergone this process without issues. What does it say, really? That you're not as good as them at learning things? In what other context do software people on the internet so freely share self-doubt?
I also wonder about their career plans for the future. LLMs are already capable of understanding these concepts. The tide is rising.
> I got tired of nil checks in Go and became a squeaky wheel in incident retros, where I finally got the chance to rewrite parts of our system in Rust during a refactor.
At a new job, I am writing my first microservice in golang. Used to be a Rust/C++ (kernel) and Python/PHP/JS dev (fullstack). Rust is allowed by team is heavily invested in go already.. I don't think I'll be able to convince them to learn rust! Lol
At the risk of sounding like a language zealot, have you ever looked at Ada? It was explicitly designed to be very readable, and for use in safety-critical systems. Ada isn't perfect in all the ways Rust isn't, and it might not be the right choice for your system, but if you're writing systems software it's worth a look. If you're writing a web backend on the other hand, it's not worth a look at all.
Unless you are going back to CGI, web backends are systems.
I don't think this is an old man rant, I think you made a reasonable argument. Rust is certainly at risk of becoming just as complex as c++.
I would love to introduce more rust at work, but I dread that someone is going to ask about for<'a>, use<'a>, differences between impl X vs Box<dyn X>, or Pin/Unpin, and I don't have proper answers either.
> Reasoning about code written this way makes me experience profound fatigue and possess an overwhelming desire to return to my domicile;
I didn't understand that you were making fun of verbosity until the word 'domicile'. I must be one of those insufferable people who expresses simple thoughts with ornate vocabulary...
The article was comprehensible to me, and the additional function colorings sound like exciting constraints I can impose to prevent my future self from making mistakes rather than heavy winter gear. I guess I'm closer to the target audience?
Can we get a version of Rust that swaps lifetimes and ownership for a GC and a JS-style event loop? I love the DX of the language, but I don't always need to squeeze out every microsecond of performance at the cost of fighting the borrow checker.
I mean, you're asking for a fundamentally different directing for the language with different tradeoffs? Why are you commenting on an article about Rust? Not everyone wants the same tradeoffs that you do.
https://ocaml.org/
ReasonML if you want a slightly more Rustic syntax.
https://gleam.run
Then why are you using rust for these tasks?
I experienced the same kind of fatigue when I read "there are three directions of development which I find particularly interesting: [three things I never heard about nor particularly want to get familiar with]" in the article.
And later, when I read "Because though we’re not doing too bad, we’re no Ada/SPARK yet" I couldn't help thinking that there must be a reason why those languages never became mainstream, and if Rust gets more of these exciting esoteric features, it's probably headed the same way...
I think the misunderstanding here is that the article was not intended to users but to other language designers.
As a user, using a feature such as pattern types will be natural if you know the rest of the language.
Do you have a function that accepts an enum `MyEnum` but has an `unreachable!()` for some variant that you know is impossible to have at that point?
Then you can accept a `MyEnum is MyEnum::Variant | MyEnum::OtherVariant` instead of `MyEnum` to tell which are the accepted variants, and the pattern match will not require that `unreachable!()` anymore.
The fact someone does not know this is called "refinement types" does not limit their ability to use the feature effectively.
> the article was not intended to users but to other language designers.
That might be true, but it shows that the direction that Rust is talking: put in the kitchen sink, just like C++ and Scala did. And _that_ is very much important for users.
I really think that golang makes it easy to read code, rust makes it easy to write code. If Golang had sum types it would be a much nicer language to write complex applications with
I find Go code mind numbing to read. There's just _so much of it_ that the parts of the code that should jump out at me for requiring greater attention get lost in the noise. Interfaces also make reading Go more difficult than it could be without LSP - there's no `impl Xyz for` to grep for.
It's the complete opposite for me. Rust code, especially async Rust code is just full of noise the only purpose of which is to make the borrow checker shut up
Go makes it easy to read each line of code, not necessarily to understand what the system as a whole is doing.
Golang makes easy-to-skim code, with all the `if err != nil` after every function call.
Rust requires actual reading, like Typescript, only more detailed.
Go does have sum types — but the syntax is awkward and a bit transparent, so many don't recognize it as being there, and those that do don't love using it.
Can you enlighten us to what you’re talking about instead of vagueposting like this? What’s the supposed way of simulating sum types in go?
I am not sure how would you would simulate them. With enums and structs, I suppose?
However, it also has true sum types:
But as you can see the syntax leaves a lot to be desired and may not be all that obvious to those who are hung up thinking in other languages.interfaces in go aren’t types, so no, that’s not a sum type, it’s just an interface.
The set of objects that can fulfill that interface is not just string and int, it’s anything in the world that someone might decide to write an isSumType function for.
> it’s anything in the world that someone might decide to write an isSumType function for.
No. Notice the lowercase tag name. It is impossible for anyone else to add an arbitrary type to the closed set.
Unless your argument is that sum types fundamentally cannot exist? Obviously given a more traditional syntax like,
...one can come along and add C just the same. I guess that is true in some natural properties of the universe way. It is a poor take in context, however.Nothing can really save you from architecture astronauts, except possibly Go, but I hear there are people templating Go with preprocessors, so who knows. This is a human problem. On the other hand I hear you. The moment Rust gets proper async traits an entire world of hexagonal pain will open up for the victims of the astronauts. So it will get even worse, basically. I think if we could solve the problem of bored smart people sabotaging projects it'd be amazing.