1. 20

  2. 14

    I got a few notes on this. Way back I’d started writing a series of introductory HOWTOs/tutorials on some aspects of Rust that I found problematic to learn from other sources. Unfortunately, I never had the time to finish them, and I’m afraid they might also be getting stale by now. I, uh, rewrote them for this post, the originals were a little snarky but that’s just because how I write things for my own use when I’m frustrated with something.

    I wanna preface this with a) the disclaimer that I don’t think their authors should be faulted for producing the material they did: much of it was started a long time ago, and their crystal globe is just as bad as anyone else’s when it comes to predicting what will be hard, which is what they had to do, since they had very little precedent to build on. It’s orders of magnitude better than what most languages had when they were as young as Rust – the few exceptions, like Ada, didn’t have better docs because they had better writers, but because they had standardisation and better funding. I also suspect a lot of the major sources of documentation were initially meant to overcome the lack of practical material, and their authors did an excellent job at that; however, they also set the tone of later efforts, so there’s an unfortunate lack of more foundational material, not because the authors of early material sucked at it (they didn’t – despite some bad things I say about The Rust Programming Language below, I use it every day!) but because the community didn’t follow up with equally good material. And b) the disclaimer that this is all re-distiled in a haste so it’s likely of questionable quality, and may reflect some early misunderstandings of my own, but maybe you can pick something useful out of it.

    IMHO some bigger challenges with learning Rust are as follows.

    Official sources, and many unofficial sources, don’t follow a constructive approach to writing Rust programs. Virtually every material I’ve ever read goes something like this:

    1. I have this problem that I want to solve.
    2. This is how we could probably solve it…
    3. …oh, it doesn’t compile, here’s how we fix the borrow-checker error.
    4. Here are four more examples that also fail to compile, here are the four fixes.
    5. Finally, this is what compiles.

    For one thing, this has very little pedagogical value: you can usually stumble your way into fixing errors with practically zero understanding of what’s triggering them (syntactic errors aside, you can fix almost everything with clone()s and Arcs).

    But more importantly, it fails to teach the exact thing it’s supposed to teach: how to leverage Rust’s safety properties to build safe programs.

    Instead of guiding you through the reasoning process involved in coming up with a solid design, lots of sources guide you through the less rewarding process of sneaking a questionable design past the compiler. Sometimes that doesn’t (fully) work, which devolves into the “Rust forces you to design your program correctly” discussion. That’s wrong in a lot of ways but what I think is relevant here is just how discouraging it is for beginners: if Rust experts can’t write a thirty-line program that even compiles, let alone works, who the f%ck can?

    It also builds the wrong mindset in the first place. Instead of encouraging people to approach designing things in terms of “how can I make this as solid/safe/fast/whatever your objectie is as possible”, it builds up a “how can I design it so it compiles?” mindset. This results in suboptimal designs, and is also very off-puting, because lots of material reads like fanboy stuff that explains how to bend yourself backwards to write X in Rust instead of how to write a better X and how to leverage whatever Rust gives you in order to make it better.

    Finally, it makes documents really, really difficult to follow. You can barely read ten paragraphs of The Rust Programming Language without running into a snippet of code that doesn’t compile. I think this comes from a misguided attempt to treat a symptom, rather than the problem: it makes sense to show off all these cases because they result in verbose (and oftentimes very weird) error messages, so you gotta train beginners into associating each of them with the right problem in their code. This is the symptom. The problem is that readers don’t understand “the frame of mind” within which the compiler “reasons” about their code, so they cannot write code that compiles in the first place.

    I think one of the reasons why this happens is that…

    …Official sources fail to build a substantial understanding of many difficult concepts. The Rust Programming Language book makes it really hard to connect the dots. A few examples:

    1. Ownership and references are discussed in Chapter 4, but then the book discusses boxes, Rcs, and RefCells only in chapter 15. The discussion of of all these concepts is essentially disjoint from the discussion of traits, even though essential differences between the various species of references and smart pointers can be easily explained in terms of which data-related (Move, Copy etc.) traits they implement and how.

    This makes it difficult to understand – and to remember! – all the useful properties of various smart pointer types, and people are left trying to find other ways to do it, like making imperfect analogies with their C++ counterparts, for example.

    To illustrate this: almost all of the content of Chapter 15.1 (which describes the Box type) can be explained by noting that Box doesn’t implement Copy. Boxes derive almost all of their useful properties from that (obviously, as long as you stick to safe Rust):

    • Why is box ownership exclusive? Because Box is not Copy: you can’t get a copy of “the address that a Box points to” – there’s only one copy of it, and everyone has to pass it around.
    • Why are boxes immune to double-freeing? Because, not being Copy, you can never get two boxes in the same scope to point to the same data on the heap: the allocator (hopefully) won’t give you the same region twice, and once you have one in a box, you can’t give it in another box and keep a copy. So whenever a box is destroyed – e.g. because it goes out of scope – it’s guaranteed to be the only one that references a particular address.
    • Why are dangling boxes not a problem? Because not just manipulating boxes, but also manipulating the data inside it – i.e. dereferencing boxes – follows the usual Rust ownership rules: if the inner data type is Copy, it gets copied, otherwise it gets moved out of the box. So if you’ve dereferenced a box, the stuff in the box was either copied (so you can access or destroy the data safely through the box – the data is still there, and the box is still its sole owner) or it was moved (and the compiler won’t let you access it through the box anymore, there’s nothing to get destroyed when the box gets out of scope etc.). That’s also why boxes have that nice cushy feeling to them (like other Rust smart pointer types): if the compiler lets you access it, it’s safe to access it.

    I suspect that this, coupled with the unconstructive approach I mentioned above, is the source of a lot of common questions we see in Rust forums. E.g. people try to build a tree or a linked list structure, using boxes as inner data types because they want trees of (pointers to) big chunks of data allocated on the heap. Then they try to do stuff with their list, but the compiler yells that they can’t do them because Box doesn’t implement Copy. So they show up and ask how they can implement Copy for the Box type, failing to realize that Box works the way it does not because of some advanced compiler magic, but precisely because it doesn’t implement Copy. Then we tell them they should do something completely different instead, and the cycle begins anew – they try to do it, the compiler yells at them and so on and so forth – instead of equiping them with what they need to come up with a correct design in the first place.

    1. A related example: the discussion of Data Types in chapter 3 doesn’t even mention DSTs. Lots of things in the book make zero sense without it – since we just talked about boxes, that includes the very first thing on the list of what boxes are used for: “when you have a type whose size can’t be known at compile time and you want to use a value of that type in a context that requires an exact size”.

    Unfortunately, there’s no reasonably self-contained explanation for what those contexts are, and why, until late in chapter 19, when DSTs are finally discussed. Even trait objects are discussed before DSTs (and the discussion obviously needs to handwave some details away). So, unless you’re armed with either good intuition or a good understanding of modern language design principles, you spend most of the book figuring out if you need to box something or not based on whether the compiler yells at you.

    There’s lots of “what”, very little “why”, which I suspect is also the source of very much Rust hate, as it makes a lot of things seem completely arbitrary and made-up (why can’t I just have a plain reference to heap objects? Why can’t I just copy a String?)

    Ultimately, I think a lot of Rust’s “steep” learning curve stems from the fact that the process of building a lot of knowledge is uselessly replicated in detail, across several “derived” concepts, just because the more fundamental concepts on which they’re based are not discussed in sufficient detail, or aren’t applied.

    Also, lots of sources focus so much on evangelism that it’s really gross. This isn’t just upsetting on a subjective level, although it is that, too – even Microsoft docs from the High Microsoft era of the late 90s look tame compared to some of this, and those were gross. I think it builds a bad frame of mind for documentation authors, who tend to be quick to illustrate the strength of whatever it is they’re discussing that they jump into hands-on demonstrations before building a sufficient understanding of the subject, and discuss trade-offs only as an afterthought. This results in the same problem as above: lots of “what”, very little “why”.

    1. 12

      I don’t know how to read documentation for rust packages. If I am able to figure something out it is usually by accident or because I happened to Ctrl+f the correct term and not because I knew where I could find the information I needed. It’s probably because I am dumb and mostly use Python so I am used to reading docs for a high level language which usually take much less brain power.

      1. 9

        This was a huge problem for me in the beginning until I tried to write my own crate. Then I realized that crates are organized a certain way and then everything clicked. But coming from Ruby, it was hard to get over looking for methods contained within modules or classes. Crates offer you modules, traits, structs, and functions, and if you can’t find what you’re looking for at the top level, it may be:

        1. On an implementation of a trait for a struct
        2. In a macro somewhere
        3. Any of the above within a nested module

        I generally end up using the search function regardless, but if you do have a mental model of the layout of a Rust crate it helps a lot for understanding the results of those searches.

      2. 9

        Rust’s standard library is really big. The “core language” is billed as small and that may be true, but to do anything with the language, you need to use the standard library…and it is quite big and not always easily discoverable, in my experience.

        Not sure where I’m going with this, just an observation.

        1. 7

          Interesting, I would probably say that from learnability perspective, Rust stdlib is perfectly sized: it has the basic things (hashmaps, subprocesses, TCP), but doesn’t have anything extra (CLI argument parsing, JSON, HTTP). I’d say that going in either direction would make stuff harder to learn.

          But year, learning stdlib is always hard, because that’s just a lot of stuff. What worked for me is just reading the docs for all stdlib modules. It’s not that big of a pile of text, and, if you glanced through everything, it becomes much easier to find stuff later.

          1. 4

            Rust’s standard library is really big.

            Compared to what? When I look at the usual suspects in the industry (C++, C#, etc.), then Rust seems slim. If you cut things out of Rust’s standard library, you will have a mess of conflicting crates that try to patch the holes in the standard library. This is not without precedent, there’s already a mess with time and chrono [1].

            [1] https://www.reddit.com/r/rust/comments/ts84n4/chrono_or_time_03/

          2. 6

            This prompted me to formulate a grand theory of learning a programming language thoroughly:

            Step 0: learn to program; Learning a specific language, and learning how to program in general are very different activities. You generally want as little language as possible while learning how to code.

            Step 1: learn the big “why” of the language, how it works in broad strokes, it’s design philosophy and relation to other languages. If you understand why, you can predict how the language works before learning it. It’s much more efficient, learning wise, to narrow the gap between “this is how I think it should work” and “this is how it actually works” than to just learn the latter from nothing. Understanding why is super hard for the first few languages, and mostly trivial after you know a few. The direct way to learn is to look at the official docs, or some early presentations, as that’s the thing they are centered around. But the hard bit here is not finding info, but rather viscerally understanding it. The best advice here is probably to just learn other languages.

            Step 2: learn how to solve specific problems. If you want to, eg, “print stuff to stderr”, how’d you figure out how to do that in a language? That’s mostly generic skill of “googling”, but you need to have one, be it literal googling, chatting with gpt or with (hopefully) humans on IRC.

            Step 3: architect and write a moderately sized (couple of weekends) program from scratch. Don’t drill small exercises (advent), don’t go contributing to someone else awesome open-source project. Write a thing for yourself, with the aim to learn stuff, and not with the aim to create a great thing.

            In terms of my own experience/Rust.

            Step 0: stuff like advent is good for learning programming! Though, I personally find advent to fiddly, as is mostly about parsing input. If you are a cranky holidays-hater like me, you might enjoy https://projecteuler.net or https://cses.fi/problemset/.

            Step 1: official docs are good, “Programming Rust” is also good. But, again, the devil is not with what is being said, but with understanding what is being said. For me, I’ve learned Rust just after C++, and that explained pretty much the whole why. More specifically, “Effective Modern C++” book was the Rust ad that got me. Also, meditating on the API of iterators in Rust can reveal the whole language.

            Step 2: there’s abundance of stuff here! IIRC, for me back than it was:

            • reading the book (the first version) from cover to cover, returning back to specific chapters as needed.
            • asking and answering questions on users.rust-lang.org
            • skimming through every page of stdlib docs
            • good old googling

            These days, there are also rust-by-example (https://doc.rust-lang.org/rust-by-example/), rustlings (https://github.com/rust-lang/rustlings), https://cheats.rs, discord, probably half-a-dozen more I am forgetting about.

            Step 3: that’s the key, really. If you can build “a thing” fully by yourself, you clearly know the language already, and, simultaneously, building the thing is how you get the tacit knowledge/commit all the little bits into the muscle memory. Here’s a short list of weekend projects I can recommend as an exercise:

            • ray tracer (https://raytracing.github.io, though it’s a bit too step by step, ideally you’d just look up formulas on the wikipedia page and draw the rest of the owl yourself. Won’t be a pretty owl, the first one, but you’ll learn a lot).
            • software rasterizer: if ray tracer works by simulating a path of a ray of light in a physically-plausible way, rasterizing is much dumber: just project trianges in space to the screen. One of my earliest “wow” in programming was when I created a “rotating cube” in Java, and I was recently reminded of that when someone on reddit was super-enthusiastic about doing the same thing in Rust.
            • compiler for a toy programming language
            • a chat server (a binary which listens on a port, accepts connections from clients, and then forwards messages between the clients)

            Again, I stress that the goal here is not to build a fancy ray tracer, but to learn as much language as you can. So, don’t use any libraries except for std if you can avoid it, cut problem definition corners to allow for that (eg, using text-based image format), and do go for overengineerd solutions.

            1. 5

              My biggest stumbling block was when you do hit a lifetime error, the solution is given in terms of how to satisfy the rust compiler.

              When it’s given in terms of what’s actually happening with memory - or what potentially could happen! - it stuck with me a lot longer. The frame shifting from “satisfying an irritating compiler” to “preventing unsafe memory edge case x” was big,

              1. 4

                Honestly as someone who has just picked up rust in the last month, the rustlings course has, I think, solved the problem of learning rust. You just need to dedicate an entire workweek to going through every one of the 90-something exercises. After that you’ll have a solid base on which to build. The only thing that really has given me any trouble since then is the slice::split_at_mut function, or rather my lack of knowledge that it existed - if I could lobby for anything to be included in the rustlings course, it would be that. Perhaps I should issue a PR! I suppose the “dropped while borrowed” error also gave me a bit of trouble, but was fine once I figured out you just need to make a separate variable for the value to live in before borrowing it. Sometimes this means you have to split up a long map/filter/fold function chain, which is a bit annoying.

                If people try to “learn rust by stackoverflow search” like any other language, which I definitely did at first, they will inevitably fail. They will also inevitably fail if they just skim the first few chapters of The Book that cover borrowing, without any practice. This gives rust most of its reputation for difficulty in my opinion.

                Also, ChatGPT has been very helpful. The rust code it generates is often not correct, but it more often is. And when it generates incorrect code, it is basically a very good guess - the sort of guess that I try to generate whenever I write code, which can often lead to fruitful searches (ex. “what is the equivalent function to X in this scenario?”) so it reduces quite a bit of cognitive effort and thus frustration.

                1. 4

                  One thing I struggled with was finding stuff that other languages include but Rust doesn’t: networking, SQL, encryption, JSON, etc. Java, Go, Python have a lot of these items, so I figured Rust would have that too, which is incorrect.

                  It was confusing trying to figure out which crate to use. Something like encryption, should I use Rust-Crypto, Ring, or one of the crates which wrap boring/sodium/openssl? Are they maintained? Are they safe? What if I use one, and a dependency uses another, will they be able to interact with each other? Are they supported on all platforms? A lot of these questions aren’t an issue when the language includes that particular feature, but for Rust, it made getting started difficult. My first thought was that Rust hadn’t matured enough to include these things, I didn’t yet realise these weren’t included as part of the design.

                  And for platforms, you know the built-in stuff for Java, Python, Go will work on the various supported platforms. But are you trying to use Ring on PowerPC64le? Doesn’t work. Want to use some of the C-based crates with WebAssembly? Likely doesn’t work. Want to use some of the C-based crates with one of the Musl targets? Also likely doesn’t work. So it becomes a guessing game trying to find the correct crate with the needed features, needed platform support, and isn’t abandoned.

                  The Ring and ppc64le one bothers me. Someone from IBM contributed a PR for ppc64le support, which has been open for over two years (!!!) [1] while the Ring maintainer tries to make up his mind on whether or not he’ll finally accept it. Something important, like crypto, should be actively maintained, not someone’s second or third job for him to work on when/if he feels like getting around to it. This is why I personally prefer Rust-Crypto. It may be slower, low-level (difficult to use), but works everywhere, and is actively maintained.

                  And there’s also crate-squatting on crates.io, [2] which isn’t going away anytime soon. This sort of thing may lead to malware, like with Python’s Pypi.

                  [1] https://github.com/briansmith/ring/pull/1057 [2] https://github.com/dtolnay/squatternaut