Skip to content(if available)orjump to list(if available)

Flattening Rust’s learning curve

Flattening Rust’s learning curve

404 comments

·May 13, 2025

Animats

It's like reading "A Discipline of Programming", by Dijkstra. That morality play approach was needed back then, because nobody knew how to think about this stuff.

Most explanations of ownership in Rust are far too wordy. See [1]. The core concepts are mostly there, but hidden under all the examples.

    - Each data object in Rust has exactly one owner.
      - Ownership can be transferred in ways that preserve the one-owner rule.
      - If you need multiple ownership, the real owner has to be a reference-counted cell. 
        Those cells can be cloned (duplicated.)
      - If the owner goes away, so do the things it owns.

    - You can borrow access to a data object using a reference. 
      - There's a big distinction between owning and referencing.
      - References can be passed around and stored, but cannot outlive the object.
        (That would be a "dangling pointer" error).
      - This is strictly enforced at compile time by the borrow checker.
That explains the model. Once that's understood, all the details can be tied back to those rules.

[1] https://doc.rust-lang.org/book/ch04-01-what-is-ownership.htm...

frankie_t

Maybe it's my learning limitations, but I find it hard to follow explanations like these. I had similar feelings about encapsulation explanations: it would say I can hide information without going into much detail. Why, from whom? How is it hiding if I can _see it on my screen_.

Similarly here, I can't understand for example _who_ is the owner. Is it a stack frame? Why would a stack frame want to move ownership to its callee, when by the nature of LIFO the callee stack will always be destroyed first, so there is no danger in hanging to it until callee returns. Is it for optimization, so that we can get rid of the object sooner? Could owner be something else than a stack frame? Why can mutable reference be only handed out once? If I'm only using a single thread, one function is guaranteed to finish before the other starts, so what is the harm in handing mutable references to both? Just slap my hands when I'm actually using multiple threads.

Of course, there are reasons for all of these things and they probably are not even that hard to understand. Somehow, every time I want to get into Rust I start chasing these things and give up a bit later.

kibwen

> Why would a stack frame want to move ownership to its callee

Rust's system of ownership and borrowing effectively lets you hand out "permissions" for data access. The owner gets the maximum permissions, including the ability to hand out references, which grant lesser permissions.

In some cases these permissions are useful for performance, yes. The owner has the permission to eagerly destroy something to instantly free up memory. It also has the permission to "move out" data, which allows you to avoid making unnecessary copies.

But it's useful for other reasons too. For example, threads don't follow a stack discipline; a callee is not guaranteed to terminate before the caller returns, so passing ownership of data sent to another thread is important for correctness.

And naturally, the ability to pass ownership to higher stack frames (from callee to caller) is also necessary for correctness.

In practice, people write functions that need the least permissions necessary. It's overwhelmingly common for callees to take references rather than taking ownership, because what they're doing just doesn't require ownership.

Hackbraten

I think your comment has received excellent replies. However, no one has tackled your actual question so far:

> _who_ is the owner. Is it a stack frame?

I don’t think that it’s helpful to call a stack frame the owner in the sense of the borrow checker. If the owner was the stack frame, then why would it have to borrow objects to itself? The fact that the following code doesn’t compile seems to support that:

    fn main() {
        let a: String = "Hello".to_owned();
        let b = a;
        println!("{}", a);  // error[E0382]: borrow of moved value: `a`
    }
User lucozade’s comment has pointed out that the memory where the object lives is actually the thing that is being owned. So that can’t be the owner either.

So if neither a) the stack frame nor b) the memory where the object lives can be called the owner in the Rust sense, then what is?

Could the owner be the variable to which the owned chunk of memory is bound at a given point in time? In my mental model, yes. That would be consistent with all borrow checker semantics as I have understood them so far.

Feel free to correct me if I’m not making sense.

adastra22

I believe this answer is correct. Ownership exists at the language level, not the machine level. Thinking of a part of the stack or a piece of memory as owning something isn’t correct. A language entity, like a variable, is what owns another object in rust. When that object goes at a scope, its resources are released, including all the things it owns.

kibwen

> Why can mutable reference be only handed out once?

Here's a single-threaded program which would exhibit dangling pointers if Rust allowed handing out multiple references (mutable or otherwise) to data that's being mutated:

    let mut v = Vec::new();
    v.push(42);
    
    // Address of first element: 0x6533c883fb10
    println!("{:p}", &v[0]);
    
    // Put something after v on the heap
    // so it can't be grown in-place
    let v2 = v.clone();
    
    v.push(43);
    v.push(44);
    v.push(45);
    // Exceed capacity and trigger reallocation
    v.push(46);
    
    // New address of first element: 0x6533c883fb50
    println!("{:p}", &v[0]);

kazinator

The analogous program in pretty much any modern language under the sun has no problem with this, in spite of multiple references being casually allowed.

To have a safe reference to the cell of a vector, we need a "locative" object for that, which keeps track of v, and the offset 0 into v.

Someone

> // Put something after v on the heap

> // so it can't be grown in-place

> let v2 = v.clone();

I doubt rust guarantees that “Put something after v on the heap” behavior.

The whole idea of a heap is that you give up control over where allocations happen in exchange for an easy way to allocate, free and reuse memory.

dwattttt

> Why would a stack frame want to move ownership to its callee, when by the nature of LIFO the callee stack will always be destroyed first, so there is no danger in hanging to it until callee returns.

It definitely takes some getting used to, but there's absolutely times when you could want something to move ownership into a called function, and extending it would be wrong.

An example would be if it represents something you can only do once, e.g. deleting a file. Once you've done it, you don't want to be able to do it again.

oconnor663

> Could owner be something else than a stack frame?

Yes. There are lots of ways an object might be owned:

- a local variable on the stack

- a field of a struct or a tuple (which might itself be owned on the stack, or nested in yet another struct, or one of the other options below)

- a heap-allocating container, most commonly basic data structures like Vec or HashMap, but also including things like Box (std::unique_ptr in C++), Arc (std::shared_ptr), and channels

- a static variable -- note that in Rust these are always const-initialized and never destroyed

I'm sure there are others I'm not thinking of.

> Why would a stack frame want to move ownership to its callee, when by the nature of LIFO the callee stack will always be destroyed first

Here are some example situations where you'd "pass by value" in Rust:

- You might be dealing with "Copy" types like integers and bools, where (just like in C or C++ or Go) values are easier to work with in a lot of common cases.

- You might be inserting something into a container that will own it. Maybe the callee gets a reference to that longer-lived container in one of its other arguments, or maybe the callee is a method on a struct type that includes a container.

- You might pass ownership to another thread. For example, the main() loop in my program could listen on a socket, and for each of the connections it gets, it might spawn a worker thread to own the connection and handle it. (Using async and "tasks" is pretty much the same from an ownership perspective.)

- You might be dealing with a type that uses ownership to represent something besides just memory. For example, owning a MutexGuard gives you the ability to unlock the Mutex by dropping the guard. Passing a MutexGuard by value tells the callee "I have taken this lock, but now you're responsible for releasing it." Sometimes people also use non-Copy enums to represent fancy state machines that you have to pass around by value, to guarantee whatever property they care about about the state transitions.

kazinator

> Why would a stack frame want to move ownership to its callee

Happens all the time in modern programming:

callee(foo_string + "abc")

Argument expression foo_string + "abc" constructs a new string. That is not captured in any variable here; it is passed to the caller. Only the caller knows about this.

This situation can expose bugs in a run-time's GC system. If callee is something written in a low level language that is resposible for indicating "nailed" objects to the garbage collector, and it forgets to nail the argument object, GC can prematurely collect it because nothing else in the image knows about that object: only the callee. The bug won't surface in situations like callee(foo_string) where the caller still has a reference to foo_string (at least if that variable is live: has a next use).

lucozade

> _who_ is the owner. Is it a stack frame?

The owned memory may be on a stack frame or it may be heap memory. It could even be in the memory mapped binary.

> Why would a stack frame want to move ownership to its callee

Because it wants to hand full responsibility to some other part of the program. Let's say you have allocated some memory on the heap and handed a reference to a callee then the callee returned to you. Did they free the memory? Did they hand the reference to another thread? Did they hand the reference to a library where you have no access to the code? Because the answer to those questions will determine if you are safe to continue using the reference you have. Including, but not limited to, whether you are safe to free the memory.

If you hand ownership to the callee, you simply don't care about any of that because you can't use your reference to the object after the callee returns. And the compiler enforces that. Now the callee could, in theory give you back ownership of the same memory but, if it does, you know that it didn't destroy etc that data otherwise it couldn't give it you back. And, again, the compiler is enforcing all that.

> Why can mutable reference be only handed out once?

Let's say you have 2 references to arrays of some type T and you want to copy from one array to the other. Will it do what you expect? It probably will if they are distinct but what if they overlap? memcpy has this issue and "solves" it by making overlapped copies undefined. With a single mutable reference system, it's not possible to get that scenario because, if there were 2 overlapping references, you couldn't write to either of them. And if you could write to one, then the other has to be a reference (mutable or not) to some other object.

There are also optimisation opportunities if you know 2 objects are distinct. That's why C added the restrict keyword.

> If I'm only using a single thread

If you're just knocking up small scripts or whatever then a lot of this is overkill. But if you're writing libraries, large applications, multi-dev systems etc then you may be single threaded but who's confirming that for every piece of the system at all times? People are generally really rubbish at that sort of long range thinking. That's where these more automated approaches shine.

> hide information...Why, from whom?

The main reason is that you want to expose a specific contract to the rest of the system. It may be, for example, that you have to maintain invariants eg double entry book-keeping or that the sides of a square are the same length. Alternatively, you may want to specify a high level algorithm eg matrix inversion, but want it to work for lots of varieties of matrix implementation eg sparse, square. In these cases, you want your consumer to be able to use your objects, with a standard interface, without them knowing, or caring, about the detail. In other words you're hiding the implementation detail behind the interface.

codeflo

That's not explaining ownership, that motivating it. Which is fine. The thing that's hard to explain and learn is how to read function signatures involving <'a, 'b>(...) -> &'a [&'b str] or whatever. And how to understand and fix the compiler errors in code calling such a function.

throwaway81523

Is it a lot different from std::unique_ptr in C++?

I thought the Rust Book was too verbose but I liked Comprehensive Rust: https://google.github.io/comprehensive-rust/

I felt like I understood the stuff in the book based on cursory reading, but I haven't tried to actually use it.

fastasucan

>Is it a lot different from std::unique_ptr in C++?

Is knowing C++ a pre-requisite?

steveklabnik

> Is it a lot different from std::unique_ptr in C++?

It’s both identical and very different, depending on the level of detail you want to get into. Conceptually, it’s identical. Strictly speaking, the implementations differ in a few key ways.

cmrdporcupine

The good news is that idiomatically written good clean Rust code doesn't need to rely on such borrow signatures very often. That's more when you're leaving the norm and doing something "clever."

I know it throws people off, and the compiler error can be confusing, but actual explicit lifetimes as part of a signature are less common than you'd expect.

To me it's a code smell to see a lot of them.

ameliaquining

Summarizing a set of concepts in a way that feels correct and complete to someone who understands them, is a much easier task than explaining them to someone who doesn't. If we put this in front of someone who's only worked with call-by-sharing languages, do you think they'll get it right away? I'm skeptical.

bloppe

For me it really clicked when I realized ownership / lifetimes / references are just words used to talk about when things get dropped. Maybe because I have a background in C so I'm used to manual memory management. Rust basically just calls 'free' for you the moment something goes out of scope.

All the jargon definitely distracted me from grasping that simple core concept.

josephg

Almost all of it.

Rust also has the “single mutable reference” rule. If you have a mutable reference to a variable, you can be sure nobody else has one at the same time. (And the value itself won’t be mutated).

Mechanically, every variable can be in one of 3 modes:

1. Directly editable (x = 5)

2. Have a single mutable reference (let y = &mut x)

3. Have an arbitrary number of immutable references (let y = &x; let z = &x).

The compiler can always tell which mode any particular variable is in, so it can prove you aren’t violating this constraint.

If you think in terms of C, the “single mutable reference” rule is rust’s way to make sure it can slap noalias on every variable in your program.

This is something that would be great to see in rust IDEs. Wherever my cursor is, it’d be nice to color code all variables in scope based on what mode they’re in at that point in time.

mikepurvis

"Rust basically just calls 'free' for you the moment something goes out of scope."

C++ does that too with RAII. Go ahead and use whatever STL containers you like, emplace objects onto them, and everything will be safely single-owned with you never having to manually new or delete any of it.

The difference is that C++'s guarantees in this regard derive from a) a bunch of implementation magic that exists to hide the fact that those supposedly stack-allocated containers are in fact allocating heap objects behind your back, and b) you cooperating with the restrictions given in the API docs, agreeing not to hold pointers to the member objects or do weird things with casting. You can use scoped_ptr/unique_ptr but the whole time you'll be painfully aware of how it's been bolted onto the language later and whenever you want you can call get() on it for the "raw" underlying pointer and use it to shoot yourself in the foot.

Rust formalizes this protection and puts it into the compiler so that you're prevented from doing it "wrong".

pinoy420

[dead]

Animats

Right. If you come to Rust from C++ and can write good C++ code, you see this as "oh, that's how to think about ownership". Because you have to have a mental model of ownership to get C/C++ code to work.

But if you come from Javascript or Python or Go, where all this is automated, it's very strange.

math_dandy

The list in the above comment isn’t a summary — it’s a precise definition. It can and must be carefully explained with lots of examples, contrasts with other languages, etc., but the precise definition itself must figure prominently, and examples and intuition should relate back to it transparently.

andrewflnr

Practically, I think it suggests that learning the borrow checker should start with learning how memory works, rather than any concepts specific to Rust.

raincole

And, after someone who doesn't know rust reads this neat and nice summary, they would still know nothing about rust. (Except "this language's compiler must have some black magic in it.")

xiphias2

I think the most important lesson is this:

Ownership is easy, borrowing is easy, what makes the language super hard to learn is that functions must have signatures and uses that together prove that references don't outlive the object.

Also: it's better not store referenced object in a type unless it's really really needed as it makes the proof much much more complex.

echelon

> Ownership is easy, borrowing is easy

100%. It's the programmer that needs to adapt to this style. It's not hard by any means at all, it just takes some adjustment.

geodel

Indeed. Programmers are holding Rust wrong.

psychoslave

This explanation doesn't expose anything meaningful to my mind, as it doesn't define ownership and borrowing, both words being apparently rooted in an analogy with financial asset management.

I'm not acquainted with Rust, so I don't really know, but I wonder if the wording plays a role in the difficulty of concept acquisition here. Analogies are often double edged tools.

Maybe sticking to a more straight memory related vocabulary as an alternative presentation perspective might help?

arnsholt

The way I think about it is more or less in terms of how a C program would work: if you assume a heap allocated data structure, the owner is the piece of code that is responsible for freeing the allocation at the appropriate time. And a reference is just a pointer with some extra compile time metadata that lets the borrow checker prove that the reference doesn’t outlive the referent and that there’s no mutable aliasing.

throwaway81523

If you've worked inside of CPython or other programs with manual reference counting, the idea of borrowing shows up there, where you receive a reference from another part of the program and then mess with the object without tweaking the reference count, "borrowing" an existing reference because any copies you've of the address will be short lived. The term shows up throughout CPython.

Renaud

I find it strange that you relate borrowing and ownership to financial asset management.

From that angle, it indeed doesn’t seem to make sense.

I think, but might be completely wrong, that viewing these actions from their usual meaning is more helpful: you own a toy, it’s yours to do as tou please. You borrow a toy, it’s not yours, you can’t do whatever you want with it, so you can’t hold on to it if the owner doesn’t allow it, and you can’t modify it for the same reasons.

ithkuil

Analogies often leak.

1. In real life I can borrow a toy from you and while I have that toy in my hands, the owner can exchange ownership with somebody else, while the object is borrowed by me. I.e. in real life the borrowing is orthogonal to ownership. In rust you can't do that.

2. Borrowing a toy is more akin to how mutable references work in rust. Immutable references allow multiple people to play with the same toy simultaneously, provided they don't change it.

Analogies are just analogies

psychoslave

What do you mean with usual sense? Maybe it's "financial" that put the interpretation out of the track, but financial comes fidus, that is trust, as in trust that outcomes of reality will meet some expectation of a mental representation.¹

"You own a toy" is the first thing a child is teached as wrong assumption by reality if not by careful social education, isn't it? The reality is, "you can play with the toy in some time frame, and sharing with others is the only way we can all benefit of joyful ludic moment, while claims of indefinite exclusive use of the toy despite limited attention span that an individual can spend on it is socially detrimental."

Also memory as an abstract object pragmatically operate on very different ground than a toy. If we could duplicate any human hand grabbable object as information carried by memory holding object, then any economy would virtually be a waste of human attention.

¹ edit: actually I was wrong here, I have been in confusion with "fiduciary". Finance instead comes from french "fin"(end), as in "end of debt".

xnickb

Many people can borrow your toy to have look at it, but only one person can borrow it and play with it. And they are only allowed to play while no one is watching. And if you want to modify your toy with some tool it's not your's anymore, it yas moved and now belongs to the tool.

I guess I'm trying to say that analogy is of limited use here.

imtringued

You think borrowing a lawn mower or owning a power drill is financial asset management?

psychoslave

On the most abstract level, even "I" and "think" are misleading notions of what’s passing through current attention. So "borrowing" and "owning" are not really great starting point notions to "think" in that sense. But on the more mundane level of mentally handling stuffs, that’s an analogy that can have its own merits (and flaws, of course).

BlackFly

That really doesn't explain the model because you have completely left out the distinction between exclusive/shared (or mutable/immutable) borrows. Rust made a large number of choices with respect to how it permits such borrows and those do not follow from this brief outline nor from intuition or common sense. For example, the no aliasing rule is motivated not by intuition or common sense but from a desire to optimize functions.

The most complicated aspect of the borrows comes about from the elision rules which will silently do the wrong thing and will work fantastically until they don't at which point the compiler error is pointing at a function complaining about a lifetime parameter of a parameter with the trait method implying that the parameter has to live too long but the real problem was a lifetime in the underlying struct or a previous broken lifetime bound. Those elision rules are again not-intuitive and don't fall out of your explanation axiomatically. They were decisions that were made to attempt to simplify the life of programmers.

bombela

I usually teach it by translating it to our physical world by way of an object like a book, which I like to think is intuitive.

I have a book. I own it. I can read it, and write into the margin. Tear the pages off if I want. I can destroy it when I am done with it. It is mine.

I can lend this book in read only to you and many others at the same time. No modifications possible. Nobody can write to it, not even me. But we can all read it. And borrower can lend it recursively in read only to anybody else.

Or I can lend this book exclusively to you in read/write. Nobody but you can write on it. Nobody can read it; not even me; while you borrow it. You could shred the pages, but you cannot destroy the book. You can share it exclusively in read/write to anybody else recursively. When they are done, when you are done, it is back in my hands.

I can give you this book. In this case it is yours to do as you please and you can destroy it.

If you think low level enough, even the shared reference analogy describes what happens in a computer. Nothing is truly parallel when accessing a shared resource. We need to take turns reading the pages. The hardware does this quickly by means of cached copies. And if you don't want people tearing off pages, give then a read only book except for the margins.

nayuki

> the shared reference analogy describes what happens in a computer. Nothing is truly parallel when accessing a shared resource. We need to take turns reading the pages. The hardware does this quickly by means of cached copies.

This exists, but it's uncommon: https://en.wikipedia.org/wiki/Dual-ported_RAM , https://en.wikipedia.org/wiki/Dual-ported_video_RAM

8s2ngy

It took me a few tries to get comfortable with Rust—its ownership model, lifetimes, and pervasive use of enums and pattern matching were daunting at first. In my initial attempt, I felt overwhelmed very early on. The second time, I was too dogmatic, reading the book line by line from the very first chapter, and eventually lost patience. By then, however, I had come to understand that Rust would help me learn programming and software design on a deeper level. On my third try, I finally found success; I began rewriting my small programs and scripts using the rudimentary understanding I had gained from my previous encounters. I filled in the gaps as needed—learning idiomatic error handling, using types to express data, and harnessing pattern matching, among other techniques.

After all this ordeal, I can confidently say that learning Rust was one of the best decisions I’ve made in my programming career. Declaring types, structs, and enums beforehand, then writing functions to work with immutable data and pattern matching, has become the approach I apply even when coding in other languages.

ModernMech

Your experience matches an observation I have made, that when C++ developers approach Rust for the first time they often "fight the borrow checker" when they use C++ idioms in Rust. Then they start to learn Rust idioms, and bring them back to C++, which causes them to write more robust code despite not having and borrow checking at all.

surajrmal

For the most part true, but there exists patterns I can do safely and easily in c++ which I cannot in rust. Structured concurrency being one of the major ones. If a child object takes a reference to the parent, I shouldn't need to do it by way of an Arc, but because of the fact that leaking memory is safe, this isn't possible to do in rust without using the unsafe keyword. So I end up with more refcounting that I want. (This is often in the context of async). I don't bring this pattern back to c++ with me.

explodes

I had quite a similar experience. During the 3rd attempt at learning, everything seemed to click and I was able to be effective at writing a few programs.

This is all despite a long career as a programmer. Seems like some things just take repetition.

The "Dagger" dependency injection framework for the JVM took me 3 'learning attempts' to understand as well. May say more about myself than about learning something somewhat complicated.

zahlman

Out of curiousity, how many other programming languages were you familiar with before Rust?

jillesvangurp

Rust has a few big hurdles for new users:

- it's very different from other languages. That's intentional but also an obstacle.

- it's a very complex language with a very terse syntax that looks like people are typing with their elbows and are hitting random keys. A single character can completely change the meaning of a thing. And it doesn't help that a lot of this syntax deeply nested.

- a lot of its features are hard to understand without deeper understanding of the theory behind them. This adds to the complexity. The type system and the borrowing mechanism are good examples. Unless you are a type system nerd a lot of that is just gobblygook to the average Python or Javascript user. This also makes it a very inappropriate language for people that don't have a master degree in computer science. Which these days is most programmers.

- it has widely used macros that obfuscate a lot of things that further adds to the complexity. If you don't know the macro definitions, it just becomes harder to understand what is going on. All languages with macros suffer from this to some degree.

I think LLMs can help a lot here these days. When I last tried to wrap my head around Rust that wasn't an option yet. I might have another go at it at some time. But it's not a priority for me currently. But llms have definitely lowered the barrier for me to try new stuff. I definitely see the value of a language like users. But it doesn't really solve a problem I have with the languages I do use (kotlin, python, typescript, etc.). I've used most popular languages at some point in my life. Rust is unique in how difficult it is to learn.

devnullbrain

>it's very different from other languages. That's intentional but also an obstacle.

It's very different from a lot of the languages that people are typically using, but all the big features and syntax came from somewhere else. See:

>The type system and the borrowing mechanism are good examples. Unless you are a type system nerd a lot of that is just gobblygook to the average Python or Javascript user.

Well, yeah, but they generally don't like types at all. You won't have much knowledge to draw on if that's all you've ever done, unless you're learning another language in the same space with the same problems.

syklemil

Python is growing type annotations at a brisk pace though, and Typescript is cannibalizing Javascript at an incredible speed. Between that and even Java getting ADTs, I suspect the people who whine about "type nerds" are in for some rough years as dynamic languages lose popularity.

And I suspect the people who are familiar with seeing something like `dict[str, int]` can map that onto something like `HashMap<String, i32>` without actually straining their brains, and grow from there.

greazy

> it has widely used macros that obfuscate a lot of things that further adds to the complexity.

This is what's stumped me when learning Rust. It could be the resources I used, whixh introduced macros early on with no explanation.

ModernMech

Macros are introduced early in Rust for a couple reasons.

1. println!() is a macro, so if you want to print anything out you need to grapple with what that ! means, and why println needs to be a macro in Rust.

2. Macros are important in Rust, they're not a small or ancillary feature. They put a lot of work into the macro system, and all Rust devs should aspire to use and understand metaprogramming. It's not a language feature reserved for the upper echelon of internal Rust devs, but a feature everyone should get used to and use.

toprerules

Hard disagree, macros almost always lead the "too clever for you own good" even when the macro system is safe. Macros should always be used sparingly, and I think that Rust teeters on the edge of encouraging too much complexity for the sake of convenience. As a systems programmer I am allergic to unnecessary levels of indirection that make it more difficult for me to reason about what the system is actually doing and how performance and hardware interaction will be affected.

steveklabnik

At the same time, you don’t need to author macros. I have basically never written one, for example.

cyber1

"Safe" Rust is generally a simple language compared to C++. The borrow checker rules are clean and consistent. However, writing in it isn't as simple or intuitive as what we've seen in decades of popular systems languages. If your data structures have clear dependencies—like an acyclic graph then there's no problem. But writing performant self-referential data structures, for example, is far from easy compared to C++, C, Zig, etc.

On the opposite, "Unsafe" Rust is not simple at all, but without it, we can't write many programs. It's comparable to C, maybe even worse in some ways. It's easy to break rules (aliasing for exmaple). Raw pointer manipulation is less ergonomic than in C, C++, Zig, or Go. But raw pointers are one of the most important concepts in CS. This part is very important for learning; we can't just close our eyes to it.

And I'm not even talking about Rust's open problems, such as: thread_local (still questionable), custom allocators (still nightly), Polonius (nightly, hope it succeeds), panic handling (not acceptable in kernel-level code), and "pin", which seems like a workaround (hack) for async and self-referential issues caused by a lack of proper language design early on — many learners struggle with it.

Rust is a good language, no doubt. But it feels like a temporary step. The learning curve heavily depends on the kind of task you're trying to solve. Some things are super easy and straightforward, while others are very hard, and the eventual solutions are not as simple, intuitive or understandable compared to, for example, C++, C, Zig, etc.

Languages like Mojo, Carbon (I hope it succeeds), and maybe Zig (not sure yet) are learning from Rust and other languages. One of them might become the next major general-purpose systems language for the coming decades with a much more pleasant learning curve.

make3

"raw pointers are one of the most important concepts in CS" that's a reach and a half, I don't remember the last time I've used one

wepple

The concept of being able to reference a raw memory address and then access the data at that location directly feels pretty basic computer science.

Perhaps you do software engineering in a given language/framework?

A clutch is fundamental to automotive engineering even if you don’t use one daily.

tremon

It all depends on how you define computer science vs computer engineering. In all my CS classes, not once did I need to deal with pointer arithmetic or memory layout. That's because my CS classes were all theoretical, concerning pseudocode and algorithmic complexity. Mapping the pseudocode onto actual hardware was never a consideration.

In contrast, there was hardly ever a computer engineering class where I could ignore raw memory addresses. Whether it was about optimizing a memory structure for cache layout or implementing some algorithm efficiently on a resource-anemic (mmu-less) microcontroller, memory usage was never automatic.

make3

I thought you meant untyped ones. Afaik most algorithms and data structures use typed pointers. Ofc low level computer engineering stuff uses untyped pointers. That indeed touches the point of the neighbour comment, to me this is computer engineering, not computer science, but reasonable minds may disagree

devnullbrain

>and then access the data at that location

Or many other locations, by many other authors, at many other times, or simultaneously.

vacuity

I think Java pointers wouldn't count as raw pointers despite having many similar characteristics.

zahlman

The importance of a concept is not related to its frequency of direct use by humans. The https://en.wikipedia.org/wiki/Black%E2%80%93Scholes_equation is one of the most important concepts in options trading, but AFAIK quants don't exactly sit around all day plugging values into it.

toprerules

As a systems programmer I found Rust relatively easy to learn, and wonder if the problem is non-systems programmers trying to learn their first systems language and having it explicitly tell them "no, that's dangerous. no, that doesn't make sense". If you ask a front end developer to suddenly start writing C they are going to create memory leaks, create undefined behavior, create pointers to garbage, run off the end of an array, etc. But they might "feel" like they are doing great because there program compiles and sort of runs.

If you have already gotten to the journeyman or mastery experience level with C or C++ Rust is going to be easy to learn (it was for me). The concepts are simply being made explicit rather than implicit (ownership, lifetimes, traits instead of vtables, etc).

SyrupThinker

I think this is good insight, and I would extend this further to “coming from a less strict language to a very strict one”.

As someone who self-learned Rust around 1.0, after half a year of high school level Java 6, I’ve never had the problems people (even now) report with concepts like the ownership system. And that despite Rust 1.0 being far more restrictive than modern Rust, and learning with a supposedly harder to understand version of “The Book”.

I think it’s because I, and other early Rust learners I’ve talked to about this, had little preconceived notions of how a programming language should work. Thus the restrictions imposed by Rust were just as “arbitrary” as any other PL, and there was no perceived “better” way of accomplishing something.

Generally the more popular languages like JS or Python allow you to mold the patterns you want to use sufficiently, so that they fit into it. At least to me with languages like Rust or Haskell, if you try to do this with too different concepts, the code gets pretty ugly. This can give the impression the PL “does not do what you need” and “imposes restrictions”.

I also think that this goes the other way, and might just be a sort of developed taste.

steveklabnik

For whatever it's worth, I used to do a lot of teaching, and I have a similar hunch to

> I think it’s because I, and other early Rust learners I’ve talked to about this, had little preconceived notions of how a programming language should work. Thus the restrictions imposed by Rust were just as “arbitrary” as any other PL, and there was no perceived “better” way of accomplishing something.

RealityVoid

As a low level programmer, my biggest pain with rust was the type system. More precisely the traits and the trait bounds.

I suspect if you have C++ experience it's simpler to grokk, but most of the stuff I wrote was C and a bunch of the stuff Rust did were not familiar to me.

GenshoTikamura

> Stop resisting. That’s the most important lesson

> Accept that learning Rust requires...

> Leave your hubris at home

> Declare defeat

> Resistance is futile. The longer you refuse to learn, the longer you will suffer

> Forget what you think you knew...

Now it finally clicked to me that Orwell's telescreen OS was written in Rust

atoav

But it is true. My own biggest mistake when learning Rust was that I tried to torce Object Oriented paradigms on it. That went.. poorly. As soon as I went "fuck it, I just do it like you want" things went smoothly.

rikafurude21

Sounds like an abusive relationship if im being honest. Your programming language shouldnt constrict you in those ways.

jplusequalt

>Your programming language shouldn't constrict you in those ways

Says who? Programming languages come in all shapes and sizes, and each has their tradeoffs. Rust's tradeoff is that the compiler is very opinionated about what constitutes a valid program. But in turn it provides comparable performance to C/C++ without many of the same bugs/security vulnerabilities.

hbn

Every programming language has constrictions by the nature of having syntax.

In JavaScript you can declare a variable, set it to 5 (number), and then set it to the "hello" (string), but that's not allowed in e.g. C. Is C constricting me too much because I have to do it in C's way?

ModernMech

Abusive relationships involve coercion, control, fear, and often violate personal autonomy and consent. One party dominates the other in a harmful way. Using Rust is not harmful.

Placing restrictions on the programs a programmer can write is not abusive. The rules exist to ensure clarity, safety, performance, and design goals. In an abusive relationship, rules are created to control or punish behavior, often changing capriciously and without reason or consultation. By contrast, Rust is designed by a group of people who work together to advance the language according to a set of articulated goals. The rules are clear and do not change capriciously.

Abuse causes emotional trauma, isolation, and long-term harm. Rust may cause feelings of frustration and annoyance, it may make you a less efficient programmer, but using it does not cause psychological or physical harm found in abusive relationships.

lagniappe

It helps to understand the cultural ethos of the original rust devs, and the situation that gave rise to it.

freilanzer

If your compiler does not let you compile garbage code, then it's restricting you, but that's exactly what you want - you don't want to compile something that is incorrect. Rust just enforces more rules than, say, Ruby or C/C++.

pessimizer

Works that way with learning a spoken language, too. I couldn't learn my second language until I stopped thinking I was supposed to judge whether things in the language were "good" or not. Languages aren't meant to be "good" in a beauty contest sense, they're supposed to be useful. Accept that they are useful because many, many people use them, and just learn them.

I probably wouldn't have been able to do that with Rust if I hadn't been an Erlang person previously. Rust seems like Erlang minus the high-overhead Erlangy bits plus extreme type signatures and conscious memory-handling. Erlang where only "zero-cost abstractions" were provided by the language and the compiler always runs Dialyzer.

TheChaplain

My problem with rust is not the learning curve, but the absolute ugliness of the syntax. It's like Perl and C++ template metaprogramming had a child. I just can't stand it.

Python is my favourite, C is elegance in simplicity and Go is tolerable.

krior

C may be simple, but its too simple to be called elegant. The lack of namespacing comes to mind. Or that it is a staticly typed language, whose type system is barely enforced (you have to declare all types, but sometimes it feels like everything decays to int and *void without the right compiler incantations). Or the build system, where you have to learn a separate language to generate a separate language to compile the program (which a both also not really simple and elegant in my eyes). Or null-terminated strings: to save some 7 bytes per string (on modern platforms) C uses one of the most dangerous and unelegant constructs in the popular part of the programming-world. Or the absolutely inelegant error handling, where you either return an in-band-error-value, set a global variable or both or just silently fail. Or the standard-library, that is littered with dangerous functions. Or the reliance of the language definition on undefined behaviour, that forces you to read a 700-page, expensive document back to back to know whether a vital check in your program might be ignored by compilers or when your program might shred your hard drive, despite you never instructing it to do so. Or...

C has a simple syntax, but it is most certainly not elegant.

0x000xca0xfe

C is elegant because as an extremely powerful programming language used to create an uncountable number of high-profile projects it's simple enough that I feel optimistic I could write a C compiler myself if it was really necessary.

It may be impractical for some tasks but the power:complexity rate is very impressive. Lua feels similar in that regard.

dwattttt

The mechanism for sharing definitions, 'include' aka "copy file here", is absolutely not elegant. The absolute minimum you could do maybe, but not elegant.

int_19h

Modula-2 is elegant. C is hacked together. Both have equal power and mostly map 1:1, but design matters. Writing a Modula compiler would be easier.

syklemil

There's no discussing taste, especially with syntax. Personally I find the Rust syntax unoffensive, while the Go syntax comes off kind of … weird, with type signatures especially looking kind of like run-on sentences by someone who hates punctuation. C's type signatures come off as a turgid mess to me; stuff like this https://www.ericgiguere.com/articles/reading-c-declarations.... is just a design mistake as far as I'm concerned. And Python … kind of goes into the "ah, I give up, slap an `Any` signature on it" territory.

And some people love that! It just ain't for everyone.

Oreb

Have you had a look at Nim? I think you may like it.

mkaic

I love the look and feel of Nim, but found it to be stuck in a weird chicken-and-egg situation where it didn't have enough of a following to have a Convenient Package For Everything, ultimately turning me off it. Of course I recognize that the only way a language gets a Convenient Package For Everything is if it gets popular, but still...

cadamsdotcom

Rust is wonderful but humbling!

It has a built in coach: the borrow checker!

Borrow checker wouldn't get off my damn case - errors after errors - so I gave in. I allowed it to teach me - compile error by compile error - the proper way to do a threadsafe shared-memory ringbuffer. I was convinced I knew. I didn't. C and C++ lack ownership semantics so their compilers can't coach you.

Everyone should learn Rust. You never know what you'll discover about yourself.

gerdesj

"Rust is wonderful but humbling!"

It's an abstraction and convenience to avoid fiddling with registers and memory and that at the lowest level.

Everyone might enjoy their computation platform of their choice in their own way. No need to require one way nor another. You might feel all fired up about a particular high level language that you think abstracts and deploys in a way you think is right. Not everyone does.

You don't need a programming language to discover yourself. If you become fixated on a particular language or paradigm then there is a good chance you have lost sight of how to deal with what needs dealing with.

You are simply stroking your tools, instead of using them properly.

cadamsdotcom

@gerdesj your tone was unnecessarily rude and mean. Part of your message makes a valid point but it is hampered by unnecessary insults. I hope the rest of your day improves from here.

I don’t specifically like Rust itself. And one doesn’t need a programming language to discover themselves.

My experience learning Rust has been that it imposes enough constraints to teach me important lessons about correctness. Lots of people can learn more about correctness!

I’ll concede- “everyone” was too strong; I erred on the side of overly provocative.

prmph

It does not teach you any fundamental lessons about correctness. It teaches you lessons about correctness within the framework Rust imposes; that's all

gerdesj

"@gerdesj your tone was unnecessarily rude and mean."

"You" are not human.

kupopuffs

Wow who pissed in your coffee? he likes rust ok?

codr7

And he's telling other people they should like it as well, because he has seen the light.

My gut feeling says that there's a fair bit of Stockholm Syndrome involved in the attachments people form with Rust.

You could see similar behavioral issues with C++ back in the days, but Rust takes it to another level.

namuol

> Everyone should learn Rust.

I know this feels like a positive vibe post and I don’t want to yuck anyone’s yum, but speaking for myself when someone tells me “everyone should” do anything, alarm bells sound off in my mind, especially when it comes to programming languages.

vacuity

I think everyone should learn many different programming languages, because being exposed to different paradigms helps develop programming skill.

namuol

Yeah I agree, I enjoy the process. I don’t think that’s what’s behind “everyone should learn rust” in this case, and many cases. It feels like a “cause”.

pjmlp

The compilers maybe not, but static analysers already go a long way, it is a pity that it is still a quixotic battle to make developers adopt them, even if it isn't 100% all the way there.

If it isn't the always hated SecDevOps group of people pushing for the security tooling developers don't care about, at very least on build pipelines, they would keep collecting digital dust.

sph

> Borrow checker wouldn't get off my damn case - errors after errors - so I gave in. I allowed it to teach me

Bondage driven development.

noman-land

Got recommended learning paths? I tend to prefer follow along adventures via video.

maxbond

Check out Jon Gjengset.

https://www.youtube.com/@jonhoo

Measter

I wouldn't agree with that. Jon's content is great, but it's really not aimed at beginners, and some of his stuff really gets into the weeds.

mprovost

One approach that I don't see often enough is to focus on learning a subset of the language first. For example, in my own book on Rust, I skip teaching lifetimes. It's not necessary to write functions with lifetimes to build quite a few fully functioning programs. The same with macros (although the fact that their signatures are opaque doesn't make it easy for beginners). On the other hand, I disagree with the advice to rely on copy() or clone() - it's better to learn about borrowing from the beginning since it's such a fundamental part of the language.

baalimago

>For instance, why do you have to call to_string() on a thing that’s already a string?

It's so hard for me to take Rust seriously when I have to find out answers to unintuitive question like this

akewovtsn

Python community famously learned the hard way that sometimes the programmer needs to know that there are multiple kinds of string.

Personally, I’ve been using to_owned instead. Some of the people looking at my code don’t write rust, and I figure it makes things a bit easier to understand.

int_19h

Modern Python has a single type of string.

What used to be called "string" in Python 2 is no longer called that, precisely so as to avoid unnecessary confusion. It's called "bytes", which is why the question of "why do I have to convert it to string?" doesn't arise.

darthrupert

On the other side where this question is not asked we have things like

    > "1" + 2 
    3
And it's utter madness that everyone does anything important with languages like that.

int_19h

  Python 3.11.12 (main, Apr  8 2025, 14:15:29) [Clang 16.0.0 (clang-1600.0.26.6)] on darwin
  Type "help", "copyright", "credits" or "license" for more information.
  >>> "1" + 2
  Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
  TypeError: can only concatenate str (not "int") to str

darthrupert

Yes, python has strong typing and in that sense passes this bar. It's a fine choice for many things, and if you enforce certain things in CI, it can be a good choice for many more.

umanwizard

I’m not sure why it’s counterintuitive that &str and String are different things. Do you also find it counterintuitive in C++ that std::string is different from const char* ? What about &[u8] and Vec<u8> ?

dezgeg

Better analogy is std::string_view vs std::string

umanwizard

Technically that's a bit closer, yes, but way more people have heard of char* than string_view, and char* is similar _enough_ to &str that the analogy still works.

scotty79

Nah. &str is const char* exactly. It's as primitive as types in rust get.

joatmon-snoo

Strings are like time objects: most people and languages only ever deal with simplified versions of them that skip a lot of edge cases around how they work.

Unfortunately going from most languages to Rust forces you to speedrun this transition.

winrid

The question is worded weird for fun. One is a string slice (like char*) and one is String or &String, which is closer to an object.

SkiFire13

Just because a language is not high level enough to have a unique concept of "string" type doesn't mean you shouldn't take it seriously.

swiftcoder

Even very high-level languages don't have singular concepts of string. Every serious language I can think of differentiates between:

- A sequence of arbitrary bytes

- A sequence of non-null bytes interpreted as ASCII

- A sequence of unicode code points, in multiple possible encodings

int_19h

I can't think of many languages that differentiate between #1 and #2 in your list. And languages that differentiate between #1 and #3 generally don't refer to #1 as "strings" at all, so you still have a single definitive notion of what a string is (and some other things that are emphatically not strings).

3836293648

C++ is a horribly cmplicated language, sometimes I have to cast something to an int when it's already an integer. /s

I have a hard time understanding why people have such a hard time accepting that you need to convert between different text representations when it's perfectly accepted for numbers.

ants_everywhere

A learning curve measures time on the x axis and progress on the y axis.

A flat learning curve means you never learn anything :-\

alpinisme

You may be able to draw one that way but it completely neglects the way people use the term ordinarily “a steep learning curve” is not an easy to learn thing.

In point of fact, I think the intended chart of the idiom is effort (y axis) to reach a given degree of mastery (x axis)

ants_everywhere

I don't think the idiom has in mind any particular curve. I think it's just another case of a misuse becoming idiomatic without any meaning beyond the phrase taken as a unit. E.g.

- another think coming -> another thing coming

- couldn't care less -> could care less

- the proof of the pudding is in the eating -> the proof is in the pudding

It's usually not useful to try to determine the meaning of the phrases on the right because they don't have any. What does it mean for proof to be in a pudding for example?

The idiom itself is fine, it's just a black box that compares learning something hard to climbing a mountain. But learning curves are real things that are still used daily so I just thought it was funny to talk as if a flat one was desirable.

prmph

What “steep learning curve” is getting at is a situation where a lot of learning needs to occurs in a short time to make any meaningful progress.

It is not in opposition to a flat learning curve, but a gentle one

azornathogron

(not related to your overall point)

> - another think coming -> another thing coming

Fascinating. I had never come across this before. I've only ever seen people use "another thing coming".

LambdaComplex

"Flattening the derivative of Rust's learning curve" really doesn't roll off the tongue though

cozzyd

A steep line still has a flat derivative

ants_everywhere

Yeah that's true. But it would be on brand for a post that emphasizes the importance of accuracy and attention to detail.

tacitusarc

This is incorrect. A learning curve measures expertise on the x axis and effort on the y axis. Hence the saying "steep learning curve".

tacitusarc

Calling it inaccurate was too harsh; my definition only became common usage in 1970, and the original “time vs learning” is still used in academic circles.

nchmy

Academic circles? That's how it is just used in general. You're the anomaly.

ergonaught

tacitusarc

It is unclear how this comment was meant; in any case, it is appreciated. As stated in the link:

“The common English usage aligns with a metaphorical interpretation of the learning curve as a hill to climb.”

Followed by a graph plotting x “experience” against y “learning.”

saretup

That’s interesting. I always intuitively assumed x-axis was progress and y-axis was cumulative effort.

raincole

It should be called "learning hill" instead.

People (colloquially) use phrases like "steep learning curve" because they imagine learning curve is something you climb up, a.k.a. a hill.

autoexec

What we want is an "effort/difficulty curve" that measures how difficult something typically is over time from introduction to proficiency

fastasucan

But flattening the learning curve doesn't have to mean to make it completely flat, just making it less steep :)

Zambyte

It also could mean you don't need to learn beyond a certain point.

doug_durham

This reads like a list of symptoms of what's wrong with the ergonomics of Rust. This is not to bash Rust. It has its uses. But you need to balance what you are sacrificing for what you are getting.