This list may paint Rust as a weird language that can't do anything (not even a linked list!?), but it is a good advice.
Rust is easy when you do things "the Rust way" (with more overly-cautious thread-safety, immutability, and tree-shaped data than you may like). You can protest and insist that you do things your way, or you can learn the language. You first have to learn the language as it is, to understand it can and can't do, so you don't get stuck trying to force a solution that it just won't allow.
As for linked lists and graph data — Rust can do it. The problem is people try to use references for them, and that can't work. Rust references are not general-purpose pointers. I find the naming very unfortunate, because it sounds too close to holding objects "by reference" in other languages, but Rust references can't store data! Rust references semantically closer to being mutexes evaluated at compile-time. Your first attempts at a linked list out of circularly dependent mutexes is going to be total nonsense.
Graph data structures are almost always better represented as some sort of array, with indices for links, than as loose objects on the heap connected with pointers. (This is true in any language except Lisp.) Often the links should themselves be entries in another array, and not just indices, because in the real world you probably want to be able to annotate the links, too. Sometimes only the links.
Heap-allocated graph nodes are really only for CS classes.
At the opposite extremum, use an NxN bit map, with a 1 at a[n,m] to indicate a link between n and m. Or a regular NxN array, with a value V indicating a weight between n and m. Again, no pointers.
I've been thinking along similar lines for a little while. For me, it's not about mutation, but unrestricted mutation.
When a unique reference (&mut T) exists then the only possible way to even look at the data is through that unique reference. In that situation, there's no reason to have any restrictions on mutation, because nothing else can observe it.
With shared references (&T), multiple parts of the program can access the data at the same time. If you mutate in that situation you can end up with memory corruption due to data races, so mutation has to be restricted to require runtime synchronization through RefCells, Mutexs, etc.
This is indeed interesting. I usually start to explain "mut" as meaning "exclusive", and I hadn't seen the now obvious relation with "mutex". "mut is for mutex".
> trust the input of your fellow functions, and don't lose too much time looking for assertions to add.
Rust makes it so easy to add assertions that it's well worth doing IMHO, and the assertions make it easy to detect corner cases such as with lengths, collections, comparisons, unexpected inputs, etc.
Rust comes with a handful of assertions, and there are crates that provide many more, as well as runtime equivalents for function input validation. I wrote the "assertables" crate to help with these: https://github.com/sixarm/assertables-rust-crate
Returning a `Result<>` for corner cases is so much better than random asserts because it forces callers to handle them.
Personally I reject anything that looks like `assert(...)` found outside of tests and ask they be replaced with a proper error or the idiomatic way to express a fatal/unrecoverable error in your language. In Rust that's `unreachable!` and `panic!`, not `assert!`.
It depends on what contract you have with the function caller.
For example, assertions/panics are fine for reporting programmer's errors (i.e. situations that happen due to a bug in the software, not due to runtime environment). There's no point to force programmers to write error-handling branches that will never be used in a properly written program.
This is the reason why methods like `split_at` just panic if you give them a wrong index. You're not supposed to make invalid indices in the first place.
I think an appropriate assert_eq! is more idiomatic than an equivalent panic!, since it automatically reports the offending value in the panic message.
> But at some point in Rust, and it may come soon, you'll encounter a higher step and if you don't fight it with concentration and dedication, you risk not overcome it.
Can any rustaceans give me an example of a "higher step" in rust would be?
At some point, maybe not immediately, depending on what you're building, you'll have the borrow checker tell you that no, you can't borrow there, or that some construct that you feel safe isn't possible. And it will probably come with a big lot of such errors and you'll have to look for another design pattern.
Until it clicks and starts to be natural, there might be a quite rough period.
Until you know the essential traits, how to find them, and you're used to clauses on generics, you may also have some difficulties making sense of some generic code or making your own generic code work.
I've passed that hill behind me and I rarely have to fight the borrow checker. If you're familiar with the ownership model and learn the few scoping quirks that's not that hard.
What I find way harder to cope with is the gazillions of type parameters with gazillions of guards, send, sync, clone or not clone, then impl Foo for Bar vs impl Foo for Arc<Bar> etc etc; and tons of features in cargo crates, also in private workspace projects, .... And the compiler slowness oh man the compiler slowness.
EDIT: my life improved a bit since I learned about the cargo hakari plugin
To expand on this, I think one of the biggest challenges of Rust right now is that the borrow checker and compiler enforce constraints at a "tree"-level that are indicative of "forest"-level design issues with your code. Once you learn to construct forests in a way that harmonizes well with these constraints, you'll very rarely run into these kinds of tree-level issues.
But there needs to be more in the way of education that helps you understand the types of designs that the compiler is trying to push you towards.
You can generally get past things by simply `.unwrap()`ing and `.clone()`ing your way past compiler issues, but reaching for them too often prevents you from ever learning those (important) lessons given the current lack of better alternatives to learning them.
> So before you dive, have an overview of the book (or books). Maybe don't read it fully, but make sure to have read all titles so that you know when to come back to it later.
Could someone familiar with Solana smart contract development highlight which titles from 'the book' are most relevant? I imagine developing smart contracts is much more scoped down than most rust projects.
error[E0308]: mismatched types
--> src/lib.rs:10:19
|
10 | &self[span.start..=span.end]
| ^^^^^^^^^^^^^^^^^^^^^ expected struct `Span`, found struct `RangeInclusive`
|
= note: expected struct `Span`
found struct `RangeInclusive<usize>`
For more information about this error, try `rustc --explain E0308
This is weird, because you can definitely index into a `&str` with `RangeInclusive`.
What you actually need to do is implement it for `str` instead of `&'a str`. This is a little peculiarity with how the `str` type works (to date, this is the only time I've ever needed the raw `str` type and not &str! it almost never is required) and the generic implementation for Index<I> for str combined with how Deref works.
It's a small one line change to fix the code, but it's not obvious from the error message how to do it, and a bit tricky to solve unless you knew a little bit more about how Rust's operators get parsed/implemented.
In my experience this is a common class of problems when you deal with generic and trait programming in Rust. It's ultimately the front end to an advanced constraint solver, and due to the explosion of complexity when you start making generic things more/less generic you will hit the wall with weirdness and need to understand more about the type system rather than letting the type system prevent you from doing stupid things.
Frankly the memory management issue is not a problem once you buy into ownership.
The linked list debate should really be improved. From a practical perspective there are many reasons why one may wish to model a cyclic, linked datastructure.
Simple examples:
You have a social networking site that you are prototyping. You choose to start your data model with a simple Person struct which contains a list of Person called friends.
You have an e-commerce site and choose a data model which has a User struct which contains a Cart reference, and a Cart struct containing a User reference for its owning user.
You have any problem which involves a graph data-structure.
In Rust the best way to work around these issues is to have explicit lookup's by id rather than direct traversable references, or use unsafe. There are no other mechanisms. It's a missing feature that really should be highlighted early in the docs or even as a compiler error such as "cyclic ownership detected in Struct X -> References Y -> References Z in main.rs#128 consider removing the cycle or using Rc, Arena, or unsafe allocations"
As it stands if you create a reference cycle in rust you'll get a ton of compiler errors that force you to learn the rest of the ownership rules just to find out that this is something you can never do.
I don't enjoy writing Rust but its perspective on this is at least internally consistent. What are the alternatives for managing cyclic, linked data structures in other languages?
In languages with manual memory management you can do it, but it's incredibly easy to mess up. You either have to maintain the entire mental model of who owns what data and what data has been initialized in your head or write it down and risk that becoming out of date.
In languages with a GC, the implementors have assumed this complexity for you. Depending on what style of GC your language uses the exact strategy will be different, but on a GC is a comparatively complex piece of code, especially one which handles reference cycles well like we're talking about here.
Either way, the complexity is there somewhere. If you manage memory yourself, you deal with the complexity yourself in a hard-to-debug way. If you can accept the tradeoffs of a GC, then the complexity is abstracted behind the GC. Because Rust is designed for use-cases where you can't accept the tradeoffs of a GC, it has to surface that inherent complexity somewhere. It decides to surface it with ownership semantics, which at least make the rules you're following in manual languages explicit and (largely) unbreakable.
Yeah it's complex, but the underlying problem is complex. I can use GC languages, so I do, but you'd be no better off in C.
The challenge is that rust is not explicit about what it can't do. There are GCs for rust that don't really work, and the borrow checker disallows reference cycles.
The rust docs try to pretend this is never a problem and that people writing such structures are doing it wrong which is imho a problem.
You might be confusing mean goal and end goal. Nobody needs to write a linked list, except when interviewing.
What you need (a feature, a performance improvement, etc.) is usually better served with other structures, especially in languages which don't heavily favor heap allocations (in a GC based language you've already paid the allocation cost when creating the object anyway).
I think there's now some evidence that Rust prevents developers neither from making efficient applications nor from efficiently making them.
This is certainly valid for some number of uses, however there isn't a good way of mapping all reasonable uses of linked structures to unlinked structures except via associative array.
There are many linked structures such as an Linked hashmap or LRU cache that can’t be efficiently represented in safe rust. There are engineers who need to code and use such structures outside of an interview context, these engineers must use unsafe to code valid programs.
I honestly wouldn’t mind if the docs were simply upfront on the need to use unsafe and the insufficiency of any alternative for these situations. Long ago it was thought that users would simply use something along the lines of GC<Type> to handle these situations, but none of the rust GC projects materialized.
I want to write a function to increment the value of every Node. This is trivial with recursion, but that risks blowing the stack. So I try an iterative version using an explicit stack, but the borrow checker doesn't understand explicit stacks, only the C stack, so it rejects my code.
There's no inherent underlying complexity to transforming a recursive function to an iterative one, yet it is legitimately hard in Rust.
What I meant in this article is that designing the ways to deal with graph-like structures should be done after the ownership model has been understood. If you start your first program directly on this kind of problem, you'll be only fighting the borrow checker and not really enjoying Rust.
I should probably have taken my own experience with broot rather than the linked list example (which hurts many fans of its theoretical beauty).
I like how Bryan Cantrill in his youtube talk talks about needing to get some wins under your belt with Rust before your first big fight with the borrow checker. Mine was trying to do a simple Leetcode tree problem that resulted in more than an hour of frustration. If that had been my only experience I probably would have quit.
Rust is easy when you do things "the Rust way" (with more overly-cautious thread-safety, immutability, and tree-shaped data than you may like). You can protest and insist that you do things your way, or you can learn the language. You first have to learn the language as it is, to understand it can and can't do, so you don't get stuck trying to force a solution that it just won't allow.
As for linked lists and graph data — Rust can do it. The problem is people try to use references for them, and that can't work. Rust references are not general-purpose pointers. I find the naming very unfortunate, because it sounds too close to holding objects "by reference" in other languages, but Rust references can't store data! Rust references semantically closer to being mutexes evaluated at compile-time. Your first attempts at a linked list out of circularly dependent mutexes is going to be total nonsense.