15 ms·
Getting Past “Ampersand-Driven Development” in Rust
- emschwartz 4y agoA little mental model I used to explain ownership and borrowing to someone who is new to Rust. I hope others find it helpful!
- hannofcart 4y agoIt's helpful. It's especially helpful to people who are both new to Rust AND come at Rust from language ecosystems with a garbage collector. People are new to Rust but have come to it from a C/C++ have had to worry about ownership all the time, so they might not find anything here a revelation except this: "Oh wow, the Rust compiler takes care of all of this for me! No inadvertent memory leaks?! Yaay!"
- emschwartz 4y agoJust want to say that I appreciated this comment when I read it earlier in the day and when reading through the thread again just now. > It's especially helpful to people who are both new to Rust AND come at Rust from language ecosystems with a garbage collector. This is exactly the audience I was writing for so I'm very happy that you thought so.
- cornholio 4y agoPerhaps add a part on the syntax, i.e: passing a reference to a function, then passing that along to some other function, declaring that function as accepting a mutable reference to a vector, versus having a mutable parameter that is an (unmutable) reference to that vector, versus declaring that parameter is a reference to a that mutable reference; same for closures, what does a &¶meter even means, when to use & in the declaration and when to *deref at the point you use it, iterating by default "for x in Vec" versus iterating &Vec, versus using .iter() and .into_iter() etc. etc. Those subtle distinctions were the overwhelming force leading me to ampersand driven development in the early days, i.e I had the "pointer" mental model and tried to shoehorn that into Rust somehow and get that pointer to work like it does in C. I don't think I saw a simple write-up with clear examples of what to do and what not aimed for beginners, seasoned rusters just act like slick professionals that wouldn't make such nonsensical silly mistakes and beginners are to somehow emulate their genius.
- dathinab 4y agoJust one small content addition: Both `&` and `&mut` can be moved to another thread (through only `&` can be used by multiple threads). This holds even if the livetime is not `'static`, but requires the usage of a scoped threads API which use a trick to assure that the sub-thread(s) can not outlive the parent thread from which the `&`/`&mut` comes. Originally such an API was in rust/std but it was removed as it was found to not be sound in the way it was implemented, but sound versions of this API do exist(1) since a long time as libraries and there have been efforts to move it back into std (but I don't know the state of it). (1): For example crossbeam::scope : https://docs.rs/crossbeam/latest/crossbeam/fn.scope.html https://docs.rs/crossbeam/latest/crossbeam/fn.scope.html
- DoubleFree 4y agostd::thread::scope was reintroduced in 1.63
- cpuguy83 4y agoAnd all it really does is force a thread.join when "scope" goes out of scope. Maybe just me but seems only marginally useful.
- pitaj 4y agoThat's exactly the same as the crossbeam implementation. It's quite useful especially for relatively simple parallelism tasks.
- tialaramex 4y agoIn Rust if you have a &Thing right here, you can't give that to some new thread unless you can promise the thread has a shorter lifespan than the &Thing does - otherwise the thread could access it after it ceases to exist, and you've introduced Undefined Behaviour. Without scoped threads this means all references you want to give to a thread must be allowed to live forever, the lifetime 'static, since your threads might live forever (the mechanism to kill them could be discarded unused somehow). Since scoped threads always cease to exist when they go out of scope, we can give them references that live any time longer than that scope.
- _visgean 4y agoGood article but whats the reason for all the syntetic meaningless images?
- emschwartz 4y agoHonestly, I thought they'd help illustrate the ideas. But maybe not! Thanks for the feedback :)
- _visgean 4y agoI think for some of it you could be nide to do diagrams, e.g. something like this: https://rufflewind.com/2017-02-15/rust-move-copy-borrow https://rufflewind.com/2017-02-15/rust-move-copy-borrow
- illiarian 4y agoThere are too many for the amount of text. So you have two paragraphs, and then an image the size of five paragraphs. Then two more paragraphs, and another 5-paragraph image. They are also somewhat inconsistent as to where they appear. You can remove all but one or two, and the article won't be any worse :)
- emschwartz 4y agoDone :)
- richardwhiuk 4y agoI'm not sure what you think you've fixed, but the images are still way too large from my perspective. Some of your images don't even fit on a single screen at once.
- emschwartz 4y agoJust updated that as well. Now they're smaller
- oytis 4y agoAre these images generated by Dall-E or something similar?
- emschwartz 4y agoMidjourney
- Joker_vD 4y agoWould you mind if I suggest Taft Test [0] instead? The pictures take about the same amount of space that the text does, and they serve as... moodsetters or whatever it's called, so I think replacing them with pictures of the America's greatest (by volume) president would work just as fine. [0] https://tafttest.com/ https://tafttest.com/
- i_love_cookies 4y ago[dead]
- BiteCode_dev 4y agoI don't understand, why is "&" not the default and passing ownership something you must explicitly request ? It seems the latter is more common than the former, and that would avoid to have to add "&" everywhere.
- emschwartz 4y agoOoh that's a fun question. I think it comes from reusing the C/C++ notation for references (https://stackoverflow.com/a/2094715/2242866 https://stackoverflow.com/a/2094715/2242866). But I have no idea where the original idea for using those characters came from. There's one other thing in Rust that would be kind of difficult if passing a reference were the default, rather than the owned value. Rust has implicit returns so the last thing at the end of a block or function is returned. If you needed to explicitly ask for ownership, you'd need to do that at the end of every function. Also, Rust will yell at you if you try to have a function return a reference to a value that's going to be dropped.
- JohnFen 4y ago> I think it comes from reusing the C/C++ notation for references (https://stackoverflow.com/a/2094715/2242866 https://stackoverflow.com/a/2094715/2242866). But I have no idea where the original idea for using those characters came from. In C++, I think that the use of '&' came from the use of '&' in C as meaning "the address of". A reference and an address to a value are similar concepts.
- Joeboy 4y agoI think it's, um, borrowed from C++'s pass by value / reference syntax. To be honest I'm still a bit muddled about how &x in Rust relates to &x in C++.
- jeremyjh 4y ago& is about the same as const & in C++, just swapping the default for immutability.
- 4y ago
- kjuulh 4y agoAnother step in getting proficient with ampersand (or rather immutable refs), is pulling stuff from enums and structs, in an immutable way. I.e. Option<SomeStruct>, or Result<SomeStruct, ErrorKind>. Using as_ref and friends. There was a great post the other day about the "Register of rust", and getting to know all the different variants of these features to be proficient and being able to build whatever you want. The same goes for async features as well. Async especially requires that you understand lambdas, borrow checking and so on. As it can be quite tricky to send a value to a tokio::task::spawn!(...) if you don't have some mastery over the borrow checker. When you get to taking lambda in a func, that should be async then you begin to get into the weeds of rust, and where some of the nasty compilers errors show up, some of the unhelpful variants, where you are expected to know the intricacies of how lifetimes work, and how rust handles generics vs impl vs enums etc. In the beginning I just recommend using a good old Arc<Mutex<...>> if you need to pass stuff to functions, you can still shoot yourself in the foot, but you can take the optimizations piecemeal.
- PaulHoule 4y agoI’m not sure I like the word ‘register’ in that if you are singing a song you might choose to switch registers from time to time to hit all the notes whereas a song is going to have certain music features consistent throughout the song. In programming you might use different ‘registers’ in different parts of a program, but for all of the lower teachings about ‘encapsulation’, ‘information hiding’, and such programs as a whole have to have certain global invariants respected if they are going to work correctly…. Particularly when it comes to memory management! Garbage collection is a great facilitator for encapsulation because it means you can just allocate and not think at all about deallocation. A strength of C is that each program can implement a custom optimal memory handling strategy but that’s a roadblock to reuse with libraries because any library has to adapt to the memory allocation strategy of the program as a whole. Superficially it looks as if objects, inheritance, and polymorphism are the key features for code reuse in Java but I’d make the case that the garbage collector is more fundamental - you certainly lose some performance but neither C nor Rust has managed the feat that LISP and Java has of encapsulating memory management in a way that allows arbitrary code to work in the same runtime together.
- iandanforth 4y agoThis is a great post demonstrating just how bad the ergonomics of Rust are (I know I know, that's not the point of Rust). But for people who care about language usability you can point to this post and say "they were not thinking about the user journey during the design process, how would you have designed it differently?" Take the first two sections as a perfect example. You decide you want to introduce a single-character control symbol `&` (which is questionable by itself), how do you apply it? In one case you decide that this control symbol indicates that a value is immutable and in another it's part of the declaration of a value as explicitly mutable. You've now introduced a semantic contradiction that a learner has to overcome. Now obviously Rust works, and that's its point, and people love that enough to overcome all the ergonomic challenges the language puts up. What I can't wait for is Pythonic Rust where we have a language that treats humans as first class citiziens but offers the guarantees that Rust does. The only question in my mind for if this will ever happen is if the fully literate programming modality enabled by LLMs will make it a moot point.
- oleganza 4y agoStrongly disagree. I am the person who hates C/C++ and likes languages like Ruby. I like Rust specifically for the reason that I don't have to learn C++ like "grown ups" and can write my crypto libraries in something much more ergonomic. When I started writing in Rust, it was 2018 and there was old borrow checker (NLL was experimental) and error messages were much more annoying than today. And still, that was nice language that is easy to write and read once you understand a couple of basic rules. Here are the rules that you need to learn and always keep in mind, even if Rust had a more verbose syntax. And I bet many people struggling with Rust simply were not lucky enough to have those rules explained to them. Rule 1. By default, in Rust things are "owned" and "moved", all the time. When you do `x = y`, you literally move "y" to "x". In other popular languages you might expect automatic copying or automatic referencing, but in Rust you get an unusual "move" semantics. This rule applies universally to all things: be it simple numbers or structs, or references, or weird Rc<T> things or whatever. Once you move "x" to another location, you cannot use it any longer from the old location. Rule 2. As an optimization, some simple types are marked "Copy" which means that you get a cheap implicit copy whenever you want to continue to use moved value. This applies mostly to numbers and fixed-sized types. This liberates you from explicitly calling ".clone()" on stupid integers. Rule 3. There are references, mutable and immutable, that let you avoid moving value when you don't need to. At any point of time, each value may have N immutable references or 1 mutable one. There are lexical references (most commonly used): & and &mut, for these the rule 3 is statically checked at compile-time. And there are runtime counterparts that implement rule 3 in runtime: Cell/RefCell. So when you design an API or use an API, you should really think about ownership of the values that you pass in according to these 3 rules. You may have these questions and use the rules above to answer them. - Should that API "consume" (get the value "moved into") that value, or should it have read-only or mutable access to it? - What part of code (which other value) actually owns the value and is responsible for cleaning it up? - Is that a dumb value that can be Copy-ied explicitly, or is there non-trivial copying cost that requires explicit references or clones for it? - What does it mean to say `y = &x`? Per rule 1 we move reference "&x" to "y", but since it's a non-mutable reference, we can have a lot of them, so it implements Copy and &x can be used again per rule 2.
- doodlesdev 4y agohttps://archive.ph/DMvoN https://archive.ph/DMvoN For those of us who are allergic to JavaScript.
- cmrdporcupine 4y agoRe: & references generally, esp to &self; one of the biggest initial temptations and early pitfalls one will fall into with Rust is to apply object-oriented design patterns (from C++ or Java world, etc), and tie a lot of functions/behaviours to structs, instead of having free-floating functions. And it's this that can quickly get you into trouble with excessive & and &mut references where you don't need them, and trouble with borrowing generally. Passing that &self argument around all over the place -- even when you might not need to -- means you end up borrowing or declaring mutability at times when you may not want to. Also leading down the path of ending up resorting to Arc/Rc, or Box etc. It's one thing I struggled with, moving mostly from C++, but I suspect people coming from a C (not C++) programming background struggle with a lot less as they are used to what I'd call module-function-level programming, rather than binding functions to classes all over the place.
- alkonaut 4y agoWhat tripped me up early on (and still does to some extent) isn't the ownership of simple values, but when you get to generics. Do I implement the trait for Option<T> and Option<&T>. When will implementations overlap and when will they not? It becomes even trickier when you realize that a generic T can also represent a reference type &'a mut U or similar. So you can't just blindly implement the trait for both T and &T either. Basically: even though I know exactly what the ownership does in the context of "it's a clever C" I'm still tripping over myself when I take the same concepts to be "a clever C++".
- philprx 4y agoPlease add example for each section ;-) that would help the rust newbie in me :-) (or links to such examples)
- MR4D 4y agoI do not understand why it is &mut var Instead of mut &var Having two version of mut just doesn’t make sense to me. I love rust, but haven’t gotten nearly enough time to use it, and little things like this use up more memory in my brain than it should for such a well thought out system.
- estebank 4y agoBoth are valid Rust. The former means "I don't own var, but I can change it", while the later means "I own a reference that I can change where it points, but I can't change the pointed value." An argument could be made that it shouldn't have been '&mut' and instead be something like '&exclusive'.
- pornel 4y ago`let mut` is completely unnecessary from perspective of the language's semantics (everything owned exclusively can be mutated safely), but exists because people really really like having an extra lint against accidental reassignment of a variable.