15 ms·
Rust concepts I wish I learned earlier
- avinassh 4y agoIn the traits example > which tells the compiler “I just want something that implements Meow”. The ‘trait Meower’ also implies same, right? If so, why can’t we use that
- LegionMammal978 4y ago"trait Meower" just declares the trait. "impl Meower" tells the compiler to accept any type that implements the trait. It's the same as adding a generic parameter "<M: Meower>" and using "M", as with the example above that one. (Except in the second example it's placed behind a shared reference; it still works either way with either syntax.)
- ww520 4y agoThis is a fantastic list. I've certainly learned something new. The comments here are unnecessary negative. People seem to be upset on things that don't look familiar. Don't let the negative comments get to you. Keep up the good work.
- mvolfik 4y agoI don't think the comments mean to say the list is useless. It's a nice overview and I learned a few things as well. However, having written non-trivial amount of code in Rust, it really struck me at how many places there are various factual errors (however small they are). And yes, Rust is a complex lang, but if you mix terms like this without noticing the differences, I'm afraid that will make it easier. To add an example of my own: fn do_the_meow(meower: Meower) seems like you want to take (consume, own) an object implementing Meower, which is correctly explained not possible like this. A suggested solution, fn do_the_meow(meower: &dyn Meower) is very different - it is now correct with regards to a trait access, however, now you're just taking a reference. Correct replacement would be Box<dyn Meower>. And the final solutions, fn do_the_meow<M: Meower>(meower: M) fn do_the_meow(meower: &impl Meower) the first one is correct and equivalent to the original intent - it takes (owns) the value. However, the second variant (which is, again, as correctly stated, the best solution), is different again - it takes a reference (`&`) to `impl Meower`. Coming back to my point - it is important to separate the difference between the ampersand and the impl/dyn part. To suggest an improvement, one could first write all variants of this function taking a reference (`&Meower`, `&dyn Meower`, `&M` and `&impl Meower`), and later introduce the difference between where one can use sized/unsized types, and that Box<dyn Meower> is owned equivalent of &dyn Meower, and why one can't have owned `dyn Meower` just lying around.
- geewee 4y agoHaving worked with Rust for a little while (about a year) none of this stuff is particularly esoteric. However it is a great list of things that are good to know, but you don't necessarily need all that often (or learn from the official rust book)
- jxcl 4y agoYou're right, but for those learning Rust, there's _so much_ to learn, that having occasional reminders of the more basic things is really handy. I've been hobby programming in Rust for years and professionally for about 6 months, and I still picked up one or two simple things from this list.
- cormacrelf 4y ago“Fed up with the borrow checker? Try writing your own immutable linked list.” Yeah, that’ll help!
- Ygg2 4y agoYou mean use immutable data structures?
- lesuorac 4y agoWell not in the article but I saw somebody doing a guard clause that I started to copy. i.e. ``` let min_id = if let Some(id) = foo()? { id } else { return; } ... let bar = if let Some(bar) = baz()? { bar } else { return; } .. // vs if let Some(id) = foo()? { ... if let Some(bar) = baz()? { .. }} ``` It's nice to also do `if let (Some(x), Some(y)) = ...` but sometimes you need the result of `x` to do a few things before you can get `y` (or maybe don't went to evaluate `y` depending on `x`). --- I like the `where` syntax more than the example formatting. ``` fn x<T>(t: T) where T: Foo {} ```
- cormacrelf 4y agoHave you heard about let-else? It’s a recent syntax addition. That example translates as let Some(min_id) = foo() else { return }; // continue coding at same indent
- woodruffw 4y agoNice list! This is a nitpick on my part, but this part on PhantomData: > Tells the compiler that Foo owns T, despite only having a raw pointer to it. This is helpful for applications that need to deal with raw pointers and use unsafe Rust. ...isn't quite right: `_marker: marker::PhantomData<&'a T>,` doesn't tell the compiler that `Foo` owns `T`, but that instances of `Foo` can't outlive its `bar` member. `bar` is in turn borrowed, since a pointer is "just" an unchecked reference. You can see this in the playground[1]: `foo1` and `foo2` have the same borrowed `bar` member, as evidenced by the address being identical (and the borrow checker being happy). Edit: What I've written above isn't completely correct either, since the `PhantomData` member doesn't bind any particular member, only a lifetime. [1]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=65fb09c6ab9736761e244531b736b304 https://play.rust-lang.org/?version=stable&mode=debug&editio...
- cormacrelf 4y agoNot quite, it doesn’t tell the compiler much about how Foo relates to its bar field unless you also constrain the public API for creating a Foo. If Foo is constructed out of a ‘new’ method with this signature: impl<'a, T> Foo<'a, T> { pub fn new(bar: &'a T) -> Self { Self { bar, _marker: PhantomData } } } … AND the bar field is private, then you are ensuring the Foo container doesn’t outlive the original bar pointer. If you don’t constrain creation and access to the bar field then people can just write foo.bar = &new_shorter_borrow as *const T; and then the lifetimes are absolutely unrelated, foo.bar will become invalid soon. A classic example of doing this correctly is core::slice::IterMut, and the slice.iter_mut() method. A short illustration (note how only one of the Foos creates a compile time error): https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=0390a64e32d02be37613b7cab66816b4 https://play.rust-lang.org/?version=stable&mode=debug&editio... A short explanation of what you’re telling the compiler with PhantomData (without accounting for the new method/ constraints on creation) is that Foo appears to have a &'a T field, for the purposes of the outside world doing type and borrow checking on Foo structs and their type parameters. That does two things: (1) Due to implied lifetime bounds, Foo’s generics also automatically become struct Foo<'a, T: 'a>, so this ensures T outlives 'a. That will prevent you accidentally making Foo<'static, &'b str>. (2) You don’t have an unused lifetime, which is an error. We always wanted Foo to have a lifetime. So this enables you to do that at all. Edit: and (3) it can change the variance of the lifetimes and type parameters. That doesn’t happen here.
- 1980phipsi 4y agoThe one about exclusive references is good.
- puffoflogic 4y agoAuthor has confused the drop functions. `Drop::drop` and `mem::drop` have nothing to do with each other.
- woodruffw 4y agoHm? `mem::drop` is defined in terms of `Drop`[1]. Are you thinking of `mem::forget`? [1]: https://doc.rust-lang.org/std/mem/fn.drop.html https://doc.rust-lang.org/std/mem/fn.drop.html
- coder543 4y agoThe complete function definition is provided in the documentation there, and it isn't defined in terms of Drop. It's just a single line function with an empty body. In fact, mem::drop will accept any value, whether it implements Drop or not. The author of the article is definitely quite confused about Drop vs mem::drop. mem::drop is not an implementation of Drop.
- woodruffw 4y agoOh, I see what you mean: it does look like they've confused `mem::drop` with an implementation of `Drop`. > In fact, mem::drop will accept any value, whether it implements Drop or not. This doesn't mean that it isn't defined in terms of Drop, because there is no such thing as !Drop in Rust. Even things that are Copy are implicitly Drop, it's just that the copy is dropped instead of the value.
- coder543 4y agoI think your second paragraph is a misinterpretation of how Rust works. Everything isn’t implicitly Drop. Drop is an explicit cleanup mechanism that types can opt into. If it helps you to think of it conceptually as everything having an implicit no-op Drop, then I guess that’s fine, but that’s not what is happening. There is an automatic Drop “glue code” for types that contain types that implement Drop, so that those will get called, of course. But `i32` does not have Drop, at all. > Even things that are Copy are implicitly Drop, it's just that the copy is dropped instead of the value. You cannot implement Drop on a Copy type, so Drop literally never gets called on Copy types. You can’t put non-Copy types inside a Copy type, so there isn’t even Drop glue code. And no, it isn’t implicitly Drop at all! And it has nothing to do with a copy being dropped instead of the original value. Drop isn’t a universal trait. I also seem to remember in the early post-1.0 days that whether a type implemented Drop or not would significantly impact lifetime analysis, requiring some occasionally obtuse workarounds. Rust lifetime analysis accepts many more correct solutions these days, and it has been awhile since I wrote a lot of Rust code, so I don’t recall how it is these days.
- jasonjmcghee 4y agoSmall note- at least one of the links on mobile does not respect the text column width of the page, so the actual page width is wider than the text and horizontally scrollable.
- kris-s 4y agoThe author mentions the following: fn add(x: Option<i32>, y: Option<i32>) -> Option<i32> { if x.is_none() || y.is_none() { return None; } return Some(x.unwrap() + y.unwrap()); } The above looks kind of clunky because of the none checks it needs to perform, and it also sucks that we have to extract values out of both options and construct a new option out of that. However, we can much better than this thanks to Option’s special properties! Here’s what we could do fn add(x: Option<i32>, y: Option<i32>) -> Option<i32> { x.zip(y).map(|(a, b)| a+b) } Do folks really prefer the latter example? The first one is so clear to me and the second looks inscrutable.
- edflsafoiewq 4y agoFirst one isn't idiomatic anyway with return. You can just do Some(x? + y?) though.
- adwn 4y agoBoth are rather ugly. This is much more idiomatic: match (x, y) { (Some(x), Some(y)) => Some(x + y), _ => None, }
- kzrdude 4y agoThis has been a thing since Rust 1.0. Just use the beautiful properties of match (or the "later" `if let`, of course). I prefer this and wish I could say it was idiomatic, but some tools like clippy push users over to helper methods instead of simple applications of match.
- dullcrisp 4y agoDon’t know Rust, but wouldn’t this have to be: (Some(x), Some(y)) => Some(x + y) else => None
- deleted 4y ago[deleted]
- steveklabnik 4y ago
- linhns 4y agoThis is somewhat off-topic but it's nice to see someone using Zola for their own blog, awesome SSG built on Rust!
- anderskaseorg 4y ago> With the code above, Rust does not know whether we want to clone Arc or the actual inner value, so the code above will fail. This is incorrect. The code works fine as written; Rust will let you write .clone() and it will clone the Arc (without cloning the inner value). More generally, methods on a wrapper type are searched before methods found via auto-deref. It’s often considered better style to write Arc::clone(…), but that’s for human readability, not the compiler. There’s a Clippy lint for it (https://rust-lang.github.io/rust-clippy/master/#clone_on_ref_ptr https://rust-lang.github.io/rust-clippy/master/#clone_on_ref...) but it’s turned off by default.
- efnx 4y agoThe types will be different depending on which operation is used to clone so I wouldn’t really worry about this one too much.
- perrygeo 4y agoNice article, but I'm not sure I like using the Deref trait just to make code "cleaner" - it does the opposite IMO, making it harder to understand what's going on. Deref is convenient for builtin "container" types where the only thing you'll ever do is access the singular value inside it. But sprinkling it everywhere can get confusing ("why are we assigning a &str to a struct here?")
- TuringTest 4y agoIn addition to containers, it seems to be useful for 'wrapper' types adding decorators to the 'base' object without having to wrap/unwrap the new type in each operation. Classic OOP would use inheritance to achieve this, while something like Deref allows you to use it with all the added behavior - without losing the possibility to assign to it values of the base type.
- perrygeo 4y agoA good discussion of why using Deref to simulate inheritance is considered an "anti-pattern": https://rust-unofficial.github.io/patterns/anti_patterns/deref.html https://rust-unofficial.github.io/patterns/anti_patterns/der...
- jcuenod 4y agoThanks for this; I saw it, and it made me twitch, but I didn't know why... All I had was "composition over inheritance"
- dgb23 4y agoIn general, I don't like the term "anti-pattern" or its sibling "best practices". Those terms give off an authoritative aura instead of spurring curiosity, and often people don't seem to remember the rationale but just associate X with absolute bad and Y with absolute good. Softer language like "guidelines" and "recommendations" or playful language like "tricks" or "hacks" seems more useful to me. "Hacks" is dirty and _interesting_ instead of normative and unquestionable.
- kris-nova 4y agoGreat read — the author should feel proud. This made my morning.
- wizzwizz4 4y ago> Normally, Rust would complain, Not in the example given. There's nothing wrong with creating an Rc<T> loop; the borrow checker doesn't come into the picture. > That is, you could mutate the data within an Rc as long as the data is cheap to copy. You can achieve this by wrapping your data within a Rc<Cell<T>>. T: Copy is only a bound on the .get() method. You can do this even if the data is expensive to copy, so long as you always swap in a valid representation of T. (I sometimes write Cell<Option<T>>, where it makes sense to replace with a None value.) > Embrace unsafe as long as you can prove the soundness of your API, In other words: avoid unsafe except as a last-ditch resort. > &impl Meower Should be impl Meower, if you want the same behaviour as the explicit-generic version. > Many tutorials immediately jump to iterating over vectors using the into_iter method Out of interest, what tutorials? I've never read one that does that! > Instead, there are two other useful methods on iterators Methods on many iterables in std. Not on iterators (nor iterables in general). > You can wrap the following types with PhantomData and use them in your structs as a way to tell the compiler that your struct is neither Send nor Sync. … You're doing it wrong. €30 says your unsafe code is unsound. > Embrace the monadic nature of Option and Result types Er, maybe? But try boring ol' pattern-matching first. It's usually clearer, outside examples like these ones (specially-chosen to make the helper methods shine). I'd go for if let, in this example – though if the function really just returns None in the failure case, go with the ? operator. > For example, writing a custom linked list, or writing structs that use channels, would typically need to implement a custom version of Drop. No, you don't typically need to. Rust provides an automatic implementation that probably does what you need already. Custom drop implementations are mainly useful for unsafe code. > Really annoyed by the borrow checker? Use immutable data structures No. No. Haskell has all sorts of optimisations to make "immutable everything" work, and Rust's "do what I say" nature means none of those can be applied by the compiler. If you want to program this way, pick a better language. > Instead, you can define a blanket trait that can make your code a more DRY. There are no blanket traits. There are blanket trait implementations, and you've missed that line from your example. All in all, this is a good article. There are some good tips in there, but I wouldn't recommend it to a beginner. I would recommend the author revisit it in a few months, and make some improvements: a tutorial designed when you're green, but with the experience from your older self, can be a really useful thing.
- deleted 4y ago
- joshfee 4y ago> there is a lot of excellent Rust content catering to beginners and advanced programmers alike. However, so many of them focus on the explaining the core mechanics of the language and the concept of ownership rather than architecting applications. The author then goes on to write an article largely covering the mechanics of the language rather than architecting applications.
- rauljordan2020 4y agohah got me there. Content on that topic will be coming soon after this post :). This was my first foray into writing about the language
- tmpz22 4y agoMy wishlist would include design patterns for business applications in Rust, where a beginner-intermediate level Rust programmer could learn the language and how to use the language practically at the same time.
- neilv 4y agoRust is a systems programming language, with a large number of systems programming-motivated concepts to learn before you don't get stuck. I suspect, if someone is looking for copy&paste code patterns for business applications (like CRUD)... they're going to get stuck in situations where they hit brick walls that the cargo-culting rituals didn't cover. Will they have somehow learned enough Rust by then to solve their problem, or will they be frantically posting StackOverflow questions on what code to copy&paste to do what they need... and end up creating new brick walls that are even harder to extricate themselves from? With lots of business applications developers on Agile-ish methodology, where workers have to continually be able to claim they are "completing tasks" at a rapid pace, I think Rust would make them cry. It's hard to call a task complete when it won't compile. And working with borrowing/lifetimes/etc. is almost always going to take longer than (what Agile very-short-term thinking rewards) leaning on copy&paste and GC and mutating with abandon for the current task, while letting any increased defects due to that become new tasks. And when those Rust business developer workers are crying and slipping on their deliverables, the leads/managers who chose to use Rust (with people who really just want to use something more like Python or Ruby or JavaScript)... are going to get called onto the carpet to explain. Live by the Agile theatre, die by the Agile theatre. (At a couple previous startups, where there was systems programming to do in addition to business-y apps, I've actually proposed using Rust as a way to attract certain kinds of genuinely enthusiastic developers, and to make it harder for some kinds of low-quality code to slip in. But I probably wouldn't do Rust if I only had a normal business application, and only normal business developers who were asking for code examples to cargo-cult.)
- friedman23 4y agoI'll share an architectural pattern from Rust that is pretty ubiquitous but not really mentioned in the official books. Skip the whole weak rc nonsense and jump directly to using an allocator (like slotmap) when dealing with cyclic data structures. Wrap the allocator in a a struct to allow for easy access and all the difficulty from rust cyclic datastructures disappears.
- badrequest 4y agoI love how Rust has so many footguns that you need to read a dozen blogs from no less than a month ago to avoid utter confusion.
- coder543 4y agoNow that's a strawman if I've ever seen one.
- anonymous_sorry 4y agoFootguns is the exactly what rust doesn't have. Some might criticise it for forcing you through a complex process to acquire a firearms license, selling you gun with an efficient safety and then insisting that you wear extremely heavily armoured shoes.
- friedman23 4y agoI consider footguns to be things that cause me to waste a ton of effort and build architecturally unsound code. The things in the article are actually just basic language concepts you need to understand to be productive. So to use the foot gun analogy, if you don't know the stuff in the article you wont even be able to pull the trigger. An actual footgun is something like monkeypatching in python or ruby. Or running for_each with an async callback in javascript.
- beoberha 4y agoQuite the opposite. Rust won’t even let you pull the trigger. C++ on the other hand is very much littered with footguns.
- antman 4y agoI read this 3-4 times and realized why people stick to python though
- Yoric 4y agoCertainly, many applications don't need this kind of complexity. But if you're working on one who does, you're typically glad to use Rust :)
- nequo 4y agoYeah. Speed and memory efficiency don’t come for free. Use Rust when you benefit enough to compensate for the cost of grokking the plumbing.
- atemerev 4y agoThere are many simpler languages with speed and memory efficiency (Nim, Crystal and the like). Rust somehow became even more complex than C++; what an achievement (not).
- nequo 4y agoTwo things that might help Rust a lot despite the complexity are the tooling and the ecosystem. Cargo is good, the compiler is extremely helpful, and there are a lot of crates to build on for all sorts of tasks. For example, if I need to use simulated annealing to solve an optimization problem, there already exist libraries that implement that algorithm well.[1] Unfortunately, the Haskell library for this seems to be unmaintained[2] and so does the OCaml library that I can find.[3] Similarly, Agda, Idris, and Lean 4 all seem like great languages. But not having libraries for one's tasks is a big obstacle to adoption. Nim looks very promising. (Surprisingly so to me.) Hopefully they will succeed at gaining wider recognition and growing a healthy ecosystem. [1] E.g., https://github.com/argmin-rs/argmin https://github.com/argmin-rs/argmin [2] https://hackage.haskell.org/package/hmatrix-gsl-0.19.0.1 https://hackage.haskell.org/package/hmatrix-gsl-0.19.0.1 was released in 2018. (Although there are newer commits in the GitHub repo, https://github.com/haskell-numerics/hmatrix https://github.com/haskell-numerics/hmatrix. Not too sure what is going on.) [3] https://github.com/khigia/ocaml-anneal https://github.com/khigia/ocaml-anneal
- draw_down 4y ago[dead]
- endorphine 4y ago> There are two kinds of references in Rust, a shared reference (also known as a borrow) Is that really what they're called? It seems confusing to me: if it's shared (i.e. many people use it at the same time) how can it also be borrowed (i.e. one person has it)?
- steveklabnik 4y agoThere are multiple ways of referring to this stuff. * mutable/immutable (this is the way the book refers to it and used to be the 'official' terms but I don't know if they've changed that since I left, partially this is the name because you spell it "&mut".) * shared/exclusive (this is the name that some people wish we had called mutable/immutable, a very very long time ago thing called the "mutapocalypse.") Both sets are adjectives for "borrow." I agree with you that "shared borrow" can feel like a contradiction in terms. In general, they are duals of each other: it depends if you want to start from a place of "can I change this" or "who all has access to this," making each term more clear depending on the perspective you take.
- endorphine 4y agoThis clears things up, thanks.
- atemerev 4y agoYes, most of my code deals with graph-like data structures. It is _really_ hard to understand how to write this in Rust. Just doesn't fit in my head.
- ihnorton 4y agoReally annoyed by the borrow checker? Use immutable data structures ... This is especially helpful when you need to write pure code similar to that seen in Haskell, OCaml, or other languages. Are there any tutorials or books which take an immutable-first approach like this? Building familiarity with a functional subset of the language, before introducing borrowing and mutability, might reduce some of the learning curve. I suspect Rust does not implement as many FP-oriented optimizations as GHC, so this approach might hit performance dropoffs earlier. But it should still be more than fast enough for learning/toy datasets.
- steveklabnik 4y ago> I suspect Rust does not implement as many FP-oriented optimizations as GHC It's more complicated than this; sometimes FPish code is super nice and easy and compiles well (see the zip example in this very thread!) and sometimes it's awkward and hard to use and slower.
- jayy-lmao 4y agoPersonally I’ve done a small amount of reading of the Hands On Functional Programming in Rust book. I’ve also found Scott Wlaschin’s F# resources quite transferable (though you may run into some issues with passing async functions around as arguments).
- dgb23 4y agoI think proper immutable data structures can be quite fast without sophisticated compiler magic if they are read and cloned often (cloning is an abstraction) and are generally long lived. Rust makes mutations safe, but immutability has benefits outside of safety.
- lakomen 4y agoGod almighty, that language is sl fugly to look at
- cassepipe 4y agoOne thing I wish Rust and C++ had and that I have only seen in Carbon is pass-by-reference by default + an explicit syntax to pass by copy + for rust, some syntax to mark that we are passing a mutable/exclusive reference.
- pornel 4y agoThat doesn't make sense for Rust. Rust's references in function arguments aren't an equivalent of C++ reference arguments. Rust doesn't reason in terms of by-reference vs by-value passing. It doesn't have pervasive expensive copy constructors that need to be avoided, NRVOs, and things like that. Rust works in terms of owning and borrowing. Moves have a special case of `Copy` types like i32, but this works only for POD types, is at worst a shallow memcpy, and the types have to opt in to being copyable like that. The default is non-copyable, even for trivial structs and integer enums. Drawing false parallels with C++'s pointer types is a major source of people fighting the borrow checker. References aren't for not-copying, they're for not-owning. `Box<T>` is a pointer that passes T by reference, but there's no `&` involved, because it is owning. OTOH Passing an argument via `&` is not just "by reference", but may require borrowing a value, which needs a location to be borrowed from, may extend scopes of loans, need specific lifetimes, etc. It's way more than just a perf tweak and is a PITA when it's done implicitly (which async fn does to some extent).
- ElderKorihor 4y ago> Use impl types as arguments rather than generic constraints when you can Eep. No. At least, not in anything public. Universal impl trait arguments can't be turbofished, so you're placing an unnecessary constraint on your caller.
- avinassh 4y ago> Universal impl trait arguments can't be turbofished, so you're placing an unnecessary constraint on your caller. What does this mean?
- orf 4y agoAs I understand it &impl Meower also uses dynamic dispatch, whereas the generic version will generate a specialized version for each concrete type it is called with.
- charrondev 4y agoAre you sure? I thought that was dyn Meower.
- orf 4y agoYou’re right! My bad