Cliff Biffle writes about Rust, embedded systems, and operating system design, including the Hubris OS.
Commenters recommend his series on learning Rust from a C perspective and his posts on the typestate pattern and Hubris.
Rust marks cross-thread shared memory as immutable in the general case, and allows you to define your own shared mutability constructs out of primitives like mutexes, atomics, and UnsafeCell. As a result you don't get rope to hang yourself with by default, but atomic orderings are more than enough rope to devise incorrect synchronizations (especially with more than 2 threads or memory locations). To quote an earlier post of mine:
In terms of shared-memory threading concurrency, Send and Sync, and the distinction between &T and &Mutex<T> and &mut T, were a revelation when I first learned them. It was a principled approach to shared-memory threading, with Send/Sync banning nearly all of the confusing and buggy entangled-state codebases I've seen and continue to see in C++ (much to my frustration and exasperation), and &Mutex<T> providing a cleaner alternative design (there's an excellent article on its design at http://cliffle.com/blog/rust-mutexes/).
My favorite simple concurrent data structure is https://docs.rs/triple_buffer/latest/triple_buffer/struct.TripleBuffer.html. It beautifully demonstrates how you can achieve principled shared mutability, by defining two "handle" types (living on different threads), each carrying thread-local state (not TLS) and a pointer to shared memory, and only allowing each handle to access shared memory in a particular way. This statically prevents one thread from calling a method intended to run on another thread, or accessing fields local…
> I'd argue, that while null ref and those classes of bugs may decrease, logic errors will increase.
To some extent that argument only makes sense; if you can find a way to greatly reduce the incidence of non-logic bugs while not addressing other bugs then of course logic bugs would make up a greater proportion of what remains.
I think it's also worth considering the fact that while Rust doesn't guarantee that it'll catch all logic bugs, it (like other languages with more "advanced" type systems) gives you tools to construct systems that can catch certain kinds of logic bugs. For example, you can write lock types in a way that guarantees at compile time that you'll take locks in the correct order, avoiding deadlocks [0]. Another example is the typestate pattern [1], which can encode state machine transitions in the type system to ensure that invalid transitions and/or operations on invalid states are caught at compile time.
These, in turn, can lead to higher-order benefits as offloading some checks to the compiler means you can devote more attention to things the compiler can't check (though to be fair this does seem to be more variable among different programmers).
> Rust is not an extraordinary readable language in my opinion, especially in the kernel where the kernel has its own data structures.
The above notwithstanding, I'd imagine it's possible to think up scenarios where Rust would make some logic bugs more visible and others less so; only time will tell which…
This is good, but could go further if you're pursuing type-system leverage. For example, why does a deleted user have an `activate` associated function in the first place? It will error out! That's safe in Rust and perfectly fine in terms of control flow: it cannot be forgotten, and cannot easily be handled incorrectly (unlike, say, a bool, where a single exclamation mark can mean a nasty bug). But it's not ideal.
I've been a huge fan of the type-state pattern [1]. I don't see it mentioned often, and never outside of Rust so far. However, it's applicable to most languages, including ones you wouldn't suspect (Python). If you introduce a whole new type (not a big deal in Rust; more of a ceremony in C# et al.) for `DeletedUser`, you can simply leave off the `active` function! Any action (==state transition) on that type will be legal and possible. Methods have unit value return type, no `Result` needed. You cannot handle that incorrectly! The code won't even compile.
I am still in the process of exploring downsides to the pattern. For example, in classic OOP languages, you can create a type hierarchy, with a top-level `User`, and the different kinds inheriting from it (and then ideally be marked `final`/`sealed` or whatever). You can then treat all users the same by interacting with the top-level type. Useful for DB interaction, for example. The same in Rust would go through traits: `impl User for DeletedUser`. But it's not quite as nice, is it?
1:…
> The Rust community should be upfront about this tradeoff - it's a universal tradeoff, that is: Safety is less ergonomic.
That can be true for small programs. Not always, because Rust's type system makes for programs that can be every bit as compact as Python if the algorithm doesn't interact badly with the borrow checker. Or even if it does. For example this tutorial converts a fairly nary C program to Rust: https://cliffle.com/p/dangerust/ The C was 234 lines, the finished memory safe Rust 198 lines.
But when it comes to large programs, the ergonomics strangely tips into reverse. By "strangely tips into reverse", I mean yes it takes more tokens and thinking to produce a working Rust program, but overall it saves time. Here a "large program" means a programmer can't fit it all in his head at one time. I think Andrew Huang summed the effect up best, when he said if you start pulling on a thread in a Rust program, you always get to the end. In other languages, you often just make the knot tighter.
> First off types can't emmit values. Types don't exist at run time.
Incorrect. Please have a look at OpenCombine sources, or, say, at the Typestate Pattern in Rust:
http://cliffle.com/blog/rust-typestate/
"Typestates are a technique for moving properties of state (the dynamic information a program is processing) into the type level (the static world that the compiler can check ahead-of-time)."
> you're completely ignorant about type theory. (...) This is just fucking rude. (...) I didn't realize how stupid and rude people can actually be.
Yes, of course, naturally, you must be right, how blind could I have been?
> I'd appreciate an apology if you had any gall.
Sure, sorry about my little previous joke[1], meant no factual offense. The very best of luck to you as a programmer and a wonderfully polite human being with a great sense of humor.
[1] "Topper: I thought I saw Elvis. Block: Let it go, Topper. The King is gone. Let's head for home." ("Hot Shots!", 1991)
Not really.
For me, the number-one Rust feature I love is that, by baking monadic optionality and error return in from the beginning (Option<T> and Result<T, E>), I can trust that, unless an author abuses panic! (in which case I never trust their code again), I can see all a function's return paths in its type signature. (Without having to choke down a pure functional language like Haskell with that currying-based function call syntax that I can never get used to.)
The runners-up are how fast Rust starts compared to Python or Java or similar when writing a CLI tool and how nice PyO3 makes safely writing libraries or backends for tools that need to be in Python for some reason like "nothing but PyQt, PySide and possibly QtJambi offers memory-safe QWidget bindings... and I've never found a Java app that didn't feel laggy and sluggish on X11".
See also https://cliffle.com/blog/rust-typestate/