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.
veber-alex · 1h ago
I find the Zig example to be shocking.
It's just so brittle. How can anyone think this is a good idea?
tialaramex · 8m 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.
zparky · 1h ago
I didn't even see the error after the first glance until I read your comment.
veber-alex · 52m ago
Yeah, I had to do a double take while reading the article because I missed the error completely while looking at the code.
aeve890 · 59m 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?
arwalk · 24m 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`.
veber-alex · 17m 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.
TUSF · 39m 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.
arwalk · 22m 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.
veber-alex · 32m 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.
BinaryIgor · 32m 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
ViewTrick1002 · 15m ago
Neither Go, Java or C++ would catch that concurrency bug.
koakuma-chan · 1h ago
I encourage every one to at least stop writing code in Python.
jvanderbot · 1h ago
The benefits realized can be mostly attributed to strong type checking.
I'm a rust dev full time. And I agree with everything here. But I also want people to realize it's not "Just Rust" that does this.
In case anyone gets FOMO.
koakuma-chan · 1h ago
Do you know a language other than Rust that has alternative for docs.rs? In JavaScript and Python they never bother to have any documentation or reference, one notable example that gets me frustrated these days is the openai SDK for TypeScript. There is no documentation for it. I have to go look at the source code to figure out what the hell goes on.
9question1 · 1h ago
These days isn't the solution to this just "ask <insert LLM of choice here>" to read the code and write the documentation"?
koakuma-chan · 45m ago
Yes, you can have Claude Code go through the code and make an .md file with documentation for all the public APIs. I do that for everything that doesn't provide llms.txt.
veber-alex · 1h ago
Here is some actual useful advice:
Use a type checker! Pyright can get you like 80% of Rust's type safety.
deathanatos · 54m ago
I've not tried Pyright, but mypy on any realistic, real-world codebase I've thrown at it emits ~80k errors. It's hard to get started with that.
mypy's output is, AFAICT, also non-deterministic, and doesn't support a programmatic format that I know of. This makes it next to impossible to write a wrapper script to diff the errors to, for example, show only errors introduced by the change one is making.
Relying on my devs to manually trawl through 80k lines of errors for ones they might be adding in is a lost cause.
Our codebase also uses SQLAlchemy extensively, which does not play well with typecheckers. (There is an extension to aid in this, but it regrettably SIGSEGVs.)
> There is an extension to aid in this, but it regrettably SIGSEGVs.
Love this.
koakuma-chan · 1h ago
I don't agree that Python can be saved by a type checker. The language itself is flawed irreversibly, as well as its ecosystem. It's a big big mess. Can't build reliable software in Python.
grep_it · 1h ago
Is this rage-bait? A language alone doesn't dictate reliability. There are tons of large scale systems out there running on Python. As for the language being, "flawed irreversibly", I'd be curious to hear you expand on that with examples.
dkdcio · 1h ago
you can and this is a juvenile position. there is reliable software in any language as popular and widespread as Python. every language is flawed
ninetyninenine · 54m ago
It can't be 100% saved, but like the OP said it's 80% saved.
It's not true you can't build reliable software in python. People have. There's proof of it everywhere. Tons of examples of reliable software written in python which is not the safest language.
I think the real thing here is more of a skill issue. You don't know how to build reliable software in a language that doesn't have full type coverage. That's just your lack of ability.
I'm not trying to be insulting here. Just stating the logic:
A. You claim python can't build reliable software.
B. Reliable Software for python actually exists, therefore your claim is incorrect
C. You therefore must not have experience with building any software with python and must have your hand held and be baby-sitted by rusts type checker.
Just spitting facts.
koakuma-chan · 31m ago
If you know some secret behind building reliable software in a programming language without types, with nulls, and with runtime exceptions, I'm all ears. I admit that a blanket statement "can't build reliable software" is going overboard, but the intention was to be dramatic, not factually correct. You can probably build reliable software in Python if you write everything from scratch, but I wouldn't want to do that to myself. I would rather use a programming language that has a type system, etc, and a better cultured ecosystem.
kragen · 1h ago
What are the major pros and cons of Pyright and Mypy relative to one another?
guitarbill · 1h ago
Sorry, it's not even close to Rust. Or even C# or Java. It can't provide the same "fearless refactoring". It is better than being blind/having to infer everything manually. That's not saying much.
And that's assuming the codebase and all dependencies have correct type annotations.
nilslindemann · 1h ago
People who recommend that other people stop using one of the best documented languages on the planet with a huge library ecosystem, a friendly user base, a clean syntax, excellent reference documentation, intuitive function names, readable tracebacks, superb editor and AI support.
loeg · 1h ago
The first save is from a failure of Rust's own making: async Rust. It's an awful footgun of a concept that is made vaguely bearable by the rest of the type system.
whazor · 58m ago
I agree with the premise of more sophisticated compilers give you a productivity boost. But compiling with cargo can be very slow, and having a strong workstation can be worth it.
pjmlp · 1h ago
Most of these productivity gains are achievable in any Standard ML influenced type system.
bryanlarsen · 1h ago
The main difference between Rust and other languages with a Standard ML influenced type system is that Rust has features that can let you get executive sign off for switching languages.
pjmlp · 1h ago
Not really, at my job Scala, F#, Swift and Kotlin are possible, and most likely will never do Rust, other than using JavaScript tools written using Rust, just because.
There is nothing in our domain of distributed systems based on SaaS products, mobile OSes, and managed cloud environments, that would profit from a borrow checker.
binary132 · 1h ago
what is the main advantage of Rust over OCaml for most applications in this respect?
wk_end · 1h ago
The example that the article gives relies on the borrow checker, which is not part of the usual Standard ML type system.
pjmlp · 59m ago
It is called affine type system, and there are ML descendents with it.
You can even go more crazy with linear types, effects, formal proofs or dependent types.
What Rust has achieved, was definitely make these ideas more mainstream.
user____name · 59m ago
Can the type system catch such things across translation unit boundaries? I know this is a big limit of C like compilers without whole program compilation.
tialaramex · 30m ago
Ultimately the answer is just "Yes". You know how an array of six integers is a different type from an array of three integers even though they're both arrays of integers ? If you're unclear on that it's worth a moment to go refresh, if your only experience is in C it's a deep dive - no trouble it's valuable knowledge.
In Rust lifetimes for references are part of the type, so &'a str and &'b str could be different types, even though they're both string slice references.
Beyond that, Rust tracks two important "thread safety" properties called Sync and Send, and so if your Thing ends up needing to be Send (because another thread gets given this type) but it's not Send, that's a type error just as surely as if it lacked some other needed property needed for whatever you do with the Thing, like it's not totally ordered (Ord) or it can't be turned into an iterator (IntoIterator)
0x1ceb00da · 1h ago
The typescript/javascript example is a little dishonest. Nothing will save you if you don't know how the runtime/environment/domain semantics work.
9question1 · 1h ago
I love Typescript but I think I disagree with this. The point of the post seems to be that features of the Rust compiler help enforce that you use certain runtime / environment / domain semantics in ways that eliminate common classes of errors. That's never going to prevent all errors, but preventing large groups of common errors so that you only have to manually remember a smaller set of runtime/environment/domain semantics could have some value.
0x1ceb00da · 42m ago
It isn't typescript's fault. Borrow checker won't save you from bugs in your SQL queries that you send to the DBMS. Typescript doesn't care about the browser just like rust doesn't care about SQL
bryanlarsen · 33m ago
SeaORM and similar crates in Rust will catch some common SQL bugs through the type system.
gedy · 55m ago
> Assigning a value to 'window.location.href' doesn't immediately redirect you, like I thought it would.
That's not a "Typescript" or language issue, that's a DOM/browser API weirdness
gavmor · 3m ago
Jeeze, why should attribute assignment have side-effects, anyway? That'd be gross!
Spivak · 1h ago
How do you encode the locking issue in the type system, it seems magical? Can you just never hold any locks when calling await, is it smart enough to know that this scheduler might move work between threads?
vlovich123 · 1h ago
Presumably the author is using tokio which requires the future constructed (e.g the async function) to be Send (either because of the rules of Rust or annotated as Send) since tokio is a work-stealing runtime and any thread might end up executing a given future (or even start executing and then during a pause move it for completion on another thread). std::sync::MutexGuard intentionally isn't annotated with Send because there are platforms that require the acquiring thread be the one to unlock the mutex.
One caveat though - using a normal std Mutex within an async environment is an antipattern and should not be done - you can cause all sorts of issues & I believe even deadlock your entire code. You should be using tokio sync primitives (e.g. tokio Mutex) which can yield to the reactor when it needs to block. Otherwise the thread that's running the future blocks forever waiting for that mutex and that reactor never does anything else which isn't how tokio is designed).
So the compiler is warning about 1 problem, but you also have to know to be careful to know not to call blocking functions in an async function.
maplant · 1h ago
> using a normal std Mutex within an async environment is an antipattern and should not be done
This is simply not true, and the tokio documentation says as much:
"Contrary to popular belief, it is ok and often preferred to use the ordinary Mutex from the standard library in asynchronous code."
It can be an issue, but only if you have long enough contention periods for the mutex to matter. A Mutex which is only held for short time periouds can work perfectly fine in an async context. And it's only going to cause deadlocks if it would cause deadlocks in a threaded context, since if Mutexes can only be held between yield points then it'll only ever be running tasks that are contending for them.
benmmurphy · 32m ago
original poster was 'lucky' that he was using a work stealing engine. if the engine was not moving tasks between threads then i think the compiler would have been happy and he could have had the fun of debugging what happens when the same thread tries to lock the same mutex twice. the rust compiler won't save you from this kind of bug.
> The exact behavior on locking a mutex in the thread which already holds the lock is left unspecified. However, this function will not return on the second call (it might panic or deadlock, for example).
sunshowers · 1h ago
Using a Tokio mutex is even more of an antipattern :) come to my RustConf talk about async cancellation next week to find out why!
dilawar · 1h ago
> One caveat though - using a normal std Mutex within an async environment is an antipattern and should not be done - you can cause all sorts of issues & I believe even deadlock your entire code.
True. I used std::mutex with tokio and after a few days my API would not respond unless I restarted the container. I was under the impression that if it compiles, it's gonna just work (fearless concurrency) which is usually the case.
maplant · 12m ago
It sounds like you have a deadlock somewhere in your code and it's unlikely that the choice of Mutex really fixed that
Spivak · 1h ago
Thank you! It seems so simple
in hindsight to have a type that means "can be moved to another thread safely." But the fact that using a Mutex in your function changes the "type" is really novel. It becoming Send once the lock is released before await is just fantastic.
NobodyNada · 46m ago
The way that this fully works together is:
- The return type of Mutex::lock() is a MutexGuard, which is a smart pointer type that 1) implements Deref so it can be dereferenced to access the underlying data, 2) implements Drop to unlock the mutex when the guard goes out of scope, and 3) implements !Send so the compiler knows it is unsafe to send between threads: https://doc.rust-lang.org/std/sync/struct.MutexGuard.html
- Rust's implementation of async/await works by transforming an async function into a state machine object implementing the Future trait. The compiler generates an enum that stores the current state of the state machine and all the local variables that need to live across yield points, with a poll function that (synchronously) advances the coroutine to the next yield point: https://doc.rust-lang.org/std/future/trait.Future.html
- In Rust, a composite type like a struct or enum automatically implements Send if all of its members implement Send.
- An async runtime that can move tasks between threads requires task futures to implement Send.
So, in the example here: because the author held a lock across an await point, the compiler must store the MutexGuard smart pointer as a field of the Future state machine object. Since MutexGuard is !Send, the future also is !Send, which means it cannot be used with an async runtime that moves tasks between threads.
If the author releases the lock (i.e. drops the lock guard) before awaiting, then the guard does not live across yield points and thus does not need to be persisted as part of the state machine object -- it will be created and destroyed entirely within the span of one call to Future::poll(). Thus, the future object can be Send, meaning the task can be migrated between threads.
VWWHFSfQ · 57m ago
Rust's marker traits (Send, Sync, etc.) + RAII is really what makes it so magical to me.
mrtracy · 1h ago
Rust uses the traits “Send” and “Sync” to encode this information, there is a lot of special tooling in the compiler around these.
A type is “Send” if it can be moved from one thread to another, it is “Sync” if it can be simultaneously accessed from multiple threads.
These traits are automatically applied whenever the compiler knows it is safe to do so. In cases where automatic application is not possible, the developer can explicitly declare a type to have these traits, but doing so is unsafe (requires the ‘unsafe’ keyword and everything that entails).
Yes Rust has ways of verifying single access to locks through the borrow checker and lifetimes.
Spivak · 1h ago
Well yes but this seems
more complicated. If the code was executed on a single thread it would be correct. The compiler somehow knows that await might move the work to another thread in which the code would still be correct if it weren't for the pesky undefined behavior. At all points it seems like it would be single access and yet it catches this.
veber-alex · 55m ago
The compiler doesn't know anything about threads.
The compiler knows the Future doesn't implement the Send trait because MutexGuard is not Send and it crosses await points.
Then, tokio the aysnc runtime requires that futures that it runs are Send because it can move them to another thread.
This is how Rust safety works. The internals of std, tokio and other low level libraries are unsafe but they expose interfaces that are impossible to misuse.
0x1ceb00da · 1h ago
An async function call returns a future which is an object holding the state of the async computation. Multithreaded runtimes like tokio require this future to be `Send`, which means it can be moved from one thread to another. The future generated by the compiler is not Send if there is a local variable crossing an await boundary that is not `Send`.
ViewTrick1002 · 56m ago
That comes from the where the tokio task is spawned annotating the future with send.
More the latter. The lock guard is not `Send`, so holding it across the await point makes the `impl Future` returned by the async function also not `Send`. Therefore it can't be passed to a scheduler which does work stealing, since that scheduler is typed to require futures to be `Send`.
bkolobara · 1h ago
Yes, if you use a scheduler that doesn't move work between threads, it will not require the task to be Send, and the example code would compile.
Rust can use that type information and lifetimes to figure out when it's safe and when not.
It's just so brittle. How can anyone think this is a good idea?
This is wild. I assume there's at least the tooling to catch this kind of errors right?
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`.
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.
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
I'm a rust dev full time. And I agree with everything here. But I also want people to realize it's not "Just Rust" that does this.
In case anyone gets FOMO.
Use a type checker! Pyright can get you like 80% of Rust's type safety.
mypy's output is, AFAICT, also non-deterministic, and doesn't support a programmatic format that I know of. This makes it next to impossible to write a wrapper script to diff the errors to, for example, show only errors introduced by the change one is making.
Relying on my devs to manually trawl through 80k lines of errors for ones they might be adding in is a lost cause.
Our codebase also uses SQLAlchemy extensively, which does not play well with typecheckers. (There is an extension to aid in this, but it regrettably SIGSEGVs.)
Also this took me forever to understand:
That will get you:Regarding the ~80k errors. Yeah, nothing to do here besides slowly grinding away and adding type annotations and fixes until it's resolved.
For the code example pyright gives some hint towards variance but yes it can be confusing.
https://pyright-play.net/?pyrightVersion=1.1.403&code=GYJw9g...
Love this.
It's not true you can't build reliable software in python. People have. There's proof of it everywhere. Tons of examples of reliable software written in python which is not the safest language.
I think the real thing here is more of a skill issue. You don't know how to build reliable software in a language that doesn't have full type coverage. That's just your lack of ability.
I'm not trying to be insulting here. Just stating the logic:
Just spitting facts.And that's assuming the codebase and all dependencies have correct type annotations.
There is nothing in our domain of distributed systems based on SaaS products, mobile OSes, and managed cloud environments, that would profit from a borrow checker.
You can even go more crazy with linear types, effects, formal proofs or dependent types.
What Rust has achieved, was definitely make these ideas more mainstream.
In Rust lifetimes for references are part of the type, so &'a str and &'b str could be different types, even though they're both string slice references.
Beyond that, Rust tracks two important "thread safety" properties called Sync and Send, and so if your Thing ends up needing to be Send (because another thread gets given this type) but it's not Send, that's a type error just as surely as if it lacked some other needed property needed for whatever you do with the Thing, like it's not totally ordered (Ord) or it can't be turned into an iterator (IntoIterator)
That's not a "Typescript" or language issue, that's a DOM/browser API weirdness
One caveat though - using a normal std Mutex within an async environment is an antipattern and should not be done - you can cause all sorts of issues & I believe even deadlock your entire code. You should be using tokio sync primitives (e.g. tokio Mutex) which can yield to the reactor when it needs to block. Otherwise the thread that's running the future blocks forever waiting for that mutex and that reactor never does anything else which isn't how tokio is designed).
So the compiler is warning about 1 problem, but you also have to know to be careful to know not to call blocking functions in an async function.
This is simply not true, and the tokio documentation says as much:
"Contrary to popular belief, it is ok and often preferred to use the ordinary Mutex from the standard library in asynchronous code."
https://docs.rs/tokio/latest/tokio/sync/struct.Mutex.html#wh...
No comments yet
> The exact behavior on locking a mutex in the thread which already holds the lock is left unspecified. However, this function will not return on the second call (it might panic or deadlock, for example).
True. I used std::mutex with tokio and after a few days my API would not respond unless I restarted the container. I was under the impression that if it compiles, it's gonna just work (fearless concurrency) which is usually the case.
- The return type of Mutex::lock() is a MutexGuard, which is a smart pointer type that 1) implements Deref so it can be dereferenced to access the underlying data, 2) implements Drop to unlock the mutex when the guard goes out of scope, and 3) implements !Send so the compiler knows it is unsafe to send between threads: https://doc.rust-lang.org/std/sync/struct.MutexGuard.html
- Rust's implementation of async/await works by transforming an async function into a state machine object implementing the Future trait. The compiler generates an enum that stores the current state of the state machine and all the local variables that need to live across yield points, with a poll function that (synchronously) advances the coroutine to the next yield point: https://doc.rust-lang.org/std/future/trait.Future.html
- In Rust, a composite type like a struct or enum automatically implements Send if all of its members implement Send.
- An async runtime that can move tasks between threads requires task futures to implement Send.
So, in the example here: because the author held a lock across an await point, the compiler must store the MutexGuard smart pointer as a field of the Future state machine object. Since MutexGuard is !Send, the future also is !Send, which means it cannot be used with an async runtime that moves tasks between threads.
If the author releases the lock (i.e. drops the lock guard) before awaiting, then the guard does not live across yield points and thus does not need to be persisted as part of the state machine object -- it will be created and destroyed entirely within the span of one call to Future::poll(). Thus, the future object can be Send, meaning the task can be migrated between threads.
A type is “Send” if it can be moved from one thread to another, it is “Sync” if it can be simultaneously accessed from multiple threads.
These traits are automatically applied whenever the compiler knows it is safe to do so. In cases where automatic application is not possible, the developer can explicitly declare a type to have these traits, but doing so is unsafe (requires the ‘unsafe’ keyword and everything that entails).
You can read more at rustinomicon, if you are interested: https://doc.rust-lang.org/nomicon/send-and-sync.html
The compiler knows the Future doesn't implement the Send trait because MutexGuard is not Send and it crosses await points.
Then, tokio the aysnc runtime requires that futures that it runs are Send because it can move them to another thread.
This is how Rust safety works. The internals of std, tokio and other low level libraries are unsafe but they expose interfaces that are impossible to misuse.
https://docs.rs/tokio/latest/tokio/task/fn.spawn.html
If you want to run everything on the same thread then localset enables that. See how the spawn function does not include the send bound.
https://docs.rs/tokio/latest/tokio/task/struct.LocalSet.html
Rust can use that type information and lifetimes to figure out when it's safe and when not.