[HN Gopher] Rust concepts I wish I learned earlier ___________________________________________________________________ Rust concepts I wish I learned earlier Author : rck Score : 314 points Date : 2023-01-18 15:14 UTC (7 hours ago) (HTM) web link (rauljordan.com) (TXT) w3m dump (rauljordan.com) | jasonjmcghee wrote: | Small 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. | perrygeo wrote: | Nice 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 wrote: | In 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 wrote: | A good discussion of why using Deref to simulate inheritance | is considered an "anti-pattern": https://rust- | unofficial.github.io/patterns/anti_patterns/der... | jcuenod wrote: | Thanks for this; I saw it, and it made me twitch, but I | didn't know why... All I had was "composition over | inheritance" | lakomen wrote: | God almighty, that language is sl fugly to look at | kris-s wrote: | The 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. | virtualritz wrote: | TLDR; learning Rust through canonical code in tutorials often | requires the student to learn bits about the language that are | more advanced than the actual problem the resp. tutorial tries | to teach how to solve in Rust. ;) | | I prefer the latter now that I understand how all the | Result/Option transformations work. As a beginner this would be | hard to read but the former looks clunky. | | Clippy also got pretty good lately at suggesting such | transformations instead of if... blocks. I.e. I guess that | means they are considered canonical. | | In general I find canonical Rust often more concise than what a | beginner would come up with but it does require deeper | understanding. I guess this is one of the reasons why Rust is | considered 'hard to learn' by many people. | | You could actually teach Rust using pretty verbose code that | would work but it wouldn't be canonical (and often also not | efficient, e.g. the classic for... loop that pushes onto a Vec | vs something that uses collect()). | mprovost wrote: | This is very true - to fully explain a "hello world" program | you'd have to dive into macro syntax... When writing my Rust | book I often start by showing the naive solution, and then | later move to more canonical code once I've introduced more | syntax that enables something more elegant. But I'm aware | that I'm showing something non-optimal to start. Maybe that | loses some trust, like people think they're learning | something only to be told later on that it's wrong? On the | other hand if you start with the optimal solution you have to | teach so much syntax that it's overwhelming. I expect that | some folk want every example to be perfect, but I'm going | with an approach where you iterate through each chapter and | as you progress through the whole book the examples get | better and more idiomatic. | steveklabnik wrote: | This is basically an eternal battle when teaching. | Personally I prefer to try and stay to only idiomatic code | if at _all_ possible, but there 's pros and cons to each | approach. | | (This is one reason why I'm glad other people are also | writing books! Not everyone likes my style!) | h4x0rr wrote: | I'd prefer a match approach | [deleted] | runevault wrote: | Once you know what map does with an option I'd say it is mostly | pretty readable. Basically map (when run against an Option | value) is a way to say "if the value passed in has a value run | this function on it otherwise return None. | edflsafoiewq wrote: | First one isn't idiomatic anyway with return. You can just do | Some(x? + y?) though. | civopsec wrote: | What's there to say? It works the same way as `zip` does for an | iterator over a `Vec`. So if you understand `zip` as applied to | an iterator over `Vec`, then you might understand `zip` applied | to `Option`. | | In other words: | | Y is clear if you already understand X. Like how returning | early is simple to understand if you understand if-blocks. | | That's the problem with replying to these kinds of questions: | the response is so short and context-dependent that it can look | curt. | | EDIT: the first code is also inelegant in that it effectively | checks if both values are `None` twice in the `Some` case: once | in the if-condition and once in `unwrap()` (the check that | panics if you try to unwrap a `None`). | adwn wrote: | Both are rather ugly. This is much more idiomatic: | match (x, y) { (Some(x), Some(y)) => Some(x + y), | _ => None, } | dullcrisp wrote: | Don't know Rust, but wouldn't this have to be: | (Some(x), Some(y)) => Some(x + y) else => None | [deleted] | steveklabnik wrote: | You're correct, except that "else" is a keyword and so | cannot be used there. You'd want _ => None, | | instead, which is the "catch all" arm. | | (For those that didn't catch it, the parent code is trying | to use None, but it's actually a tuple, and there's four | different cases here, not two. So the catch-all arm is | better than spelling each of them out in this case.) | adwn wrote: | You're right - fixed. That's what I get from writing code | in a simple text area. | wizzwizz4 wrote: | Your "fixed" version is also broken (at time of writing). | :-) | adwn wrote: | Fixed again. I'm starting to run out of excuses... ;-) | [deleted] | kzrdude wrote: | This 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. | 0x457 wrote: | Pretty sure clippy will tell you to rewrite it as: | if let (Some(x), Some(y)) = (x, y) { Some(x + y) | } else { None } | | `match` in place of `if` looks weird. IMO example with `zip` | is better though. | steveklabnik wrote: | Clippy will not complain about the parent's code. It's not | really in place of an if; there's four cases there. To be | honest, I find 'if let... else None' to be worse looking | than the match, though I'm unsure if I truly prefer the zip | version to the Some(x? + y?) version. | woodruffw wrote: | Using `zip` feels excessively clever to me. I'd probably prefer | something in between, like `and_then` followed by `map`, or | just matches. | linhns wrote: | Not sure if it's only me but after using `zip` for the first | time in any language, I tend to overuse it too much while | there are better, more idiomatic alternatives. | civopsec wrote: | If you are fine with using `map` then I don't see how this is | "clever". `zip` is basically "map2". | trevor-e wrote: | Yea the latter code is unreadable to me and feels obfuscated, | like the author is trying to force functional programming where | it doesn't belong. | Gigachad wrote: | I've done a non trivial amount of functional programming and | know what zip and map do but I can't off the top of my head | work out how that example works. | lilyball wrote: | `x.zip(y)` evaluates to `Option<(i32, i32)>` | [deleted] | [deleted] | rom-antics wrote: | I think the idiomatic way to write that is: fn | add(x: Option<i32>, y: Option<i32>) -> Option<i32> { | Some(x? + y?) } | jxcl wrote: | You do get better about using the functional operators as you | use them, and they can be incredibly powerful and convenient in | certain operations, but in this case he's missing the simplest | implementation of this function using the `?` operator: | fn add2(x: Option<i32>, y: Option<i32>) -> Option<i32> { | Some(x? + y?) } | [deleted] | bkolobara wrote: | Wait! The ? operator works with the `Option<T>` type? I | assumed it was only reserved for `Result<T,E>`. | misnome wrote: | I also (written a few tools in rust, dabble now and then) | was under the impression that it only worked with Result. | Is this recent or just never encountered? | steveklabnik wrote: | ? is able to be used with Option as of Rust 1.22, | released in November of 2017 https://blog.rust- | lang.org/2017/11/22/Rust-1.22.html | misnome wrote: | Well, I guess this counts as a concept I wish I learned | earlier! | [deleted] | steveklabnik wrote: | To _slightly_ elaborate on yccs27 's good answer, at the | moment, you can use ? for both Option<T> and Result<T, E>, | but you can only use them in functions that return the same | type, that is fn | can_use_question_mark_on_option() -> Option<i32> { | fn can_use_question_mark_on_result() -> Result<i32, ()> { | | if you have them mixed in the body, they won't work | directly. What you should/can do there depends on | specifics. For example, if you have a function that returns | Option and you have a Result inside, and you want any E to | turn into None, you can add .ok() before the ? and then it | still looks nice. (The compiler will even suggest this | one!) | pavon wrote: | And an even smaller caveat: If you use older (distro | provided) Rust versions note that it may look like there | is some partial implementation of allowing mixing with | NoneError, etc. Ignore this. It doesn't work, and was | removed in later versions. | nicoburns wrote: | But also: don't use your distro provided version of Rust. | It's intended for compiling distro packages that depend | on Rust, not for developing with Rust. Get Rust from | https://rustup.rs | pavon wrote: | And hence for writing rust code you would like to package | for your distro of choice. I publish my software because | I want others to use it, and including it in distro repos | is the most friendly way to do that. | yccs27 wrote: | It currently works with `Result` and `Option` types, and | with RFC#3058 will work with all types implementing the | `Try` trait. | satvikpendem wrote: | Monadic do notation equivalent at that point, I suppose. | yccs27 wrote: | Although that would be really fun, the proposed `Try` | basically only allows you to call the `Result<_, E>` by a | different name. | ryanianian wrote: | As someone new to Rust, I look at the latter and see `.zip()` | (apparently unrelated to python zip?), and then a lambda/block | which intuitively feels like a heavyweight thing to use for | adding (even though I'm like 90% sure the compiler doesn't make | this heavyweight). | | By comparison, the first one is straightforward and obvious. | It's certainly kinda "ugly" in that it treats Options as very | "dumb" things. But I don't need to read any docs or think about | what's actually happening when I read the ugly version. | | So TLDR: This reads a bit like "clever" code or code-golfing, | which isn't always a bad thing, especially if the codebase is | mature and contributors are expected to mentally translate | between these versions with ease. | kaba0 wrote: | It is kinda common pattern for some FP languages (Haskell), | so it doesn't seem too clever to me. | civopsec wrote: | > So TLDR: This reads a bit like "clever" code or code- | golfing, which isn't always a bad thing, especially if the | codebase is mature and contributors are expected to mentally | translate between these versions with ease. | | You contradict yourself. You can't deride it as "clever" | (whatever the quotes mean) and then in the next breath say | that it might be a practical style. | | And yes, Rust code in the wild does look like this. | steveklabnik wrote: | > (even though I'm like 90% sure the compiler doesn't make | this heavyweight). | | Yes, as of Rust 1.66, this function compiles to | example::add: xor edi, 1 xor edx, | 1 xor eax, eax or edx, edi | sete al lea edx, [rsi + rcx] ret | edflsafoiewq wrote: | It's the same zip, just think of an Option as being a list of | length at most 1. [] = None, [1] = Some(1), etc. | leshow wrote: | What you find "clever" or not is really a function of what | you are most used to seeing. There are likely many folks who | use combinators frequently who find them easier to read, | myself included. | | The first example, to me, is the worst of all worlds, if you | want to be explicit use `match`. Otherwise, having a | condition then using `unwrap` just feels like it's needlessly | complicated for no reason... Just adding my subjective | assessment to the bikeshed. | nicoburns wrote: | I'd probably write this fn add(x: | Option<i32>, y: Option<i32>) -> Option<i32> { match | (x, y) { (Some(x), Some(y)) => Some(x + y), | _ => None, } } | Yoric wrote: | Agreed. That's way more readable. | [deleted] | geewee wrote: | Having 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 wrote: | You'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. | kris-nova wrote: | Great read -- the author should feel proud. This made my morning. | linhns wrote: | This is somewhat off-topic but it's nice to see someone using | Zola for their own blog, awesome SSG built on Rust! | cormacrelf wrote: | "Fed up with the borrow checker? Try writing your own immutable | linked list." Yeah, that'll help! | Ygg2 wrote: | You mean use immutable data structures? | draw_down wrote: | [dead] | ElderKorihor wrote: | > 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. | ihnorton wrote: | Really 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 wrote: | > 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 wrote: | Personally 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). | lesuorac wrote: | Well 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 wrote: | Have 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 | wizzwizz4 wrote: | > _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. EUR30 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] | friedman23 wrote: | The book "Programming Rust" (very good book btw) only mentions | iter() and iter_mut() briefly and focuses on into_iter because | the IntoIterator trait only implements into_iter. | | You can replicate iter() with `(&x).into_iter()` and you can | replicate iter_mut() with `(&mut x).into_iter()` and obviously | `x.into_iter()` consumes the contents. | joshfee wrote: | > 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 wrote: | hah got me there. Content on that topic will be coming soon | after this post :). This was my first foray into writing about | the language | jyu wrote: | architecture patterns and antipatterns would be a welcome | contribution | tmpz22 wrote: | My 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 wrote: | Rust 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.) | jayy-lmao wrote: | Full time Rust web dev here (have been so for about a | year). | | Feature delivery was slow to start due to familiarity | with the language, the business domain, and the project. | Now that the patterns within our project have been | established (EDA, Vertical Slice, some DDD for those | interested) it's actually proving quite easy to work on. | | Have been a ts dev in a past life, and while I wouldn't | necessarily reach for Rust for future green fields it has | been quite pleasing to work with for the last year. | [deleted] | endorphine wrote: | > 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 wrote: | There 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. | woodruffw wrote: | Nice 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&editio... | cormacrelf wrote: | Not 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&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. | woodruffw wrote: | Genuinely asking: what about this is different from what I | said in the original comment? It's possible that I'm using | the wrong words, but my understanding of Rust's ownership | semantics is that "owns a reference" and "borrows the | underlying value" are equivalent statements. | cormacrelf wrote: | You can have a PhantomData without a bar field. You could | have baz and qux fields as well. The compiler does not know | the lifetime in the phantom data is connected to any other | field whatsoever. It doesn't look through the other fields | searching for *const T. It's just a lifetime and is | connected to T, not any particular pointer to T. The bar | field happens to have a T in it, so the T: 'a constraint | does apply to what you store inside the pointer, but the | *const T is lifetime-erased as it relates to the pointer, | so you have to ensure that the lifetime of the *const T | matches up with the lifetime your Foo type claims it has | (in its documentation). You must do that manually by | constraining the struct creation and field access. You | would also typically have a safe method to turn bar back | into a borrow in some way (like IterMut::next does) and | when you do, you will want to be sure that the pointer is | actually valid for the lifetime you say it is.* | woodruffw wrote: | Thanks for the explanation! I can see how my nitpick | wasn't right then, either. I'm going to make an edit | noting that. | atemerev wrote: | Yes, 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. | 1980phipsi wrote: | The one about exclusive references is good. | friedman23 wrote: | I'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. | anderskaseorg wrote: | > _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...) | but it's turned off by default. | antman wrote: | I read this 3-4 times and realized why people stick to python | though | waiseristy wrote: | RE: Any article about the GIL | [deleted] | anderskaseorg wrote: | Because, what, Python doesn't have any language details | related to method lookup that might require a sentence or two | of technical explanation? __getattr__, __getattribute__, | __get__, base classes, metaclasses, __mro__, __mro_entries__, | __dict__, __slots__, __call__, bound and unbound methods-- | none of this has ever confused someone looking at it for the | first 3-4 times? | | I assume you're going to reply that you don't need to think | about all this when writing most Python. And that's true of | most Rust too. Don't get me wrong, Rust does have some unique | complexity that Rust programmers need to learn in exchange | for its unique safety guarantees--but method lookup isn't | where the complexity is. | spoiler wrote: | Python is not immune to this (for different reasons | though)... If you want to be snide online, make sure you're | correct first :) | Yoric wrote: | Certainly, 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 wrote: | Yeah. Speed and memory efficiency don't come for free. Use | Rust when you benefit enough to compensate for the cost of | grokking the plumbing. | Gigachad wrote: | Arc is a low level tool for writing multi threaded | programs. If it scares you back to Python you could also | just write single threaded Rust. | atemerev wrote: | There 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). | ben-schaaf wrote: | > Rust somehow became even more complex than C++ | | I use C++ professionally and the small subset of it that | I've learnt is already way more complicated than rust. | nequo wrote: | Two 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 | | [2] 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. Not too sure | what is going on.) | | [3] https://github.com/khigia/ocaml-anneal | chowells wrote: | Fwiw, hmatrix-gsl works trivially and painlessly. Why | does a working library need changes? | nequo wrote: | Good to know that hmatrix-gsl works too. In this case, I | went down the Rust route instead of the Haskell one. I | use Haskell too, just not for simulated annealing. | | My main domain is scientific computing and I get nervous | about the prospect of not being able to run my code 5 or | 10 years down the road (somewhat above the typical | lifespan of a project that ends up published in my | discipline). GHC gets updates that sometimes require | library maintainers to update their libraries. Here is a | list of a few: | | https://github.com/fumieval/Haskell-breaking-changes | | Rust promises no breaking changes in the stable part of | the language. Hopefully this promise will hold up. | friedman23 wrote: | The complexity exposed by Rust is overwhelming for people | that haven't done any low level programming. | | It's not just lifetimes, a basic Rust function can expose the | ideas of exclusive and non-exclusive ownership, traits, | generics with and without trait bounds, algebraic data types, | closures, closures that move, async, async blocks, smart | pointers, macros, trait objects, etc etc. | | There is absolutely a learning curve and it makes me | understand why Golang is the way it is. The language authors | of Golang saw the business needs of Google, realized that | they need to get domain experts that are not programming | savants to write code and created a language that would allow | the largest number of non programmer domain experts be | productive coders. | | That being said, the Golang developers went too far and let | too many of their biased ideas for what is "simple" infect | the language. In what way is it simple for a language to not | be null safe. I have a bunch of other complaints too. | | But anyway, I don't think Rust is going to be a revolution in | writing software unfortunately. I don't think enough people | will be able to learn it for companies to be able to hire for | Rust positions. | shortcake27 wrote: | Rust keeps getting hyped so I thought I should give it a | go. I've been writing JS/TS, Ruby, PHP, Python for 10+ | years. | | It was a real struggle, and I wasn't enjoying it at all. | "Programming Savant" is a great way to put it because all | the people I know who like Rust also happen to be the | smartest engineers. In the end I gave up because I wasn't | getting any value. I was also trying to find a Rails | equivalent, tried Actix and I didn't like that either. | shepherdjerred wrote: | Rust is a good replacement for C/C++. It's not a good | replacement for the languages you listed. | Gigachad wrote: | Disagree, I've used it to replace Ruby at work. I haven't | had any criticisms of the language itself, only the | library ecosystem and community knowledge make it | difficult. I spent a lot of time working out what the | right library is or working without examples for some | libraries. In the end the finished product is as easy to | work on as our Ruby code once the patterns are | established and libraries imported. | kenhwang wrote: | Rust is a decent replacement for any static compiled | language too. It's honestly a breath of fresh air if you | actually need to use parallelism and had to experience | that with other static languages. | | Even my peers that transitioned from ruby to rust had a | pretty smooth time since ruby's functional-ish, | enumerable dependence, and blocks style coding lends | itself well to rust's idioms. | | But if you've only ever stayed in iterative dynamic | language land and don't remember university coursework on | programming languages, compilers, or operating systems, | yes, you'll probably be in for a world of pain and | frustration. | satvikpendem wrote: | Disagree, I have a web server in Actix that I would have | written in TypeScript and Node, or Python and Django, or | Ruby and Rails, and the Rust version is similarly high | level but far faster and more efficient. I recommend | people read Zero To Rust In Production if they want to | build a web API in Rust. | | https://zero2prod.com | renewiltord wrote: | I had the opposite experience. I wanted a quick http | server that would proxy some requests where I couldn't | control the client and the real servers. I hadn't written | either language at the time, but I had it in seconds in | Golang and fully functional and I used actix and halfway | through the dependencies disappeared and then afterwards | it was still a hassle even with help from people on the | subreddit and discord. | | Reduced to its barebones, the problem is: | | 1. Expose a HTTP server | | 2. Make n HTTP requests | | 3. Combine their output | | 4. Return the result | | But I was definitely expecting to have an outcome in a | few hours. This might be a language that it takes more | than that time for me to be proficient enough to use | async code in. | | Here's an example from my friend, almost exactly my | problem and what was suggested: https://old.reddit.com/r/ | rust/comments/kvnq36/does_anyone_kn... | satvikpendem wrote: | Not sure what you mean by dependencies disappearing. | | > _This might be a language that it takes more than that | time for me to be proficient enough to use async code | in._ | | Yes, it does take more time to learn the language, but | once you do, it's pretty straightforward. Rust is a | different way of thinking (a Meta Language dialect in C | clothing, as I like to call it; it looks like C but it's | definitely not the same semantically) while Go is similar | enough to C-based languages and web based languages like | Javascript that it's relatively easy to get started with. | | It's much the same as learning Haskell or APL, the entire | paradigm will be different and can't be picked up in a | few hours. However, if you already have functional or | array programming experience, Haskell and APL will seem | straightforward. It is because you have a lot of | C-language or JS based experience that Go was easy for | you. | tayo42 wrote: | > because all the people I know who like Rust also happen | to be the smartest engineers. | | I wish people didnt say or think things like this. a lot | of the times people get into these complex things and | talk over people to make them selves look smart. or make | things look more complicated then they really are to | improve their image. i ran into this a lot when dealing | with rust. if you can't do complex things in a clear and | simple way, your just regular. | shepherdjerred wrote: | Go and Rust solve different problems. Rust is a very | complicated language, but it's a much safer language for | systems programming. I don't think that there is a better | language for systems programming. | | Go is probably the most widely used 'simple' language, and | it's primarily used for applications. You can find some | examples of using Go for systems-like programming, but | that's not where it shines. | antonvs wrote: | Python is not even remotely an option for the sorts of | programs Rust is intended for. | vvillena wrote: | Those types are wrappers that keep a reference to some data. | In any language, it's common to encounter issues when people | confuse copying the underlying data with copying just the | container around the data and keep pointing to the same | thing. | | In Python, a list of lists has the same behavior. | l = [ [ 1, 2, 3 ] ] l2 = l.copy() | l[0][0] = 99 print(l) // [[99,2,3]] | print(l2) // [[99,2,3]] | itronitron wrote: | there was similar confusion regarding pass by reference and | pass by value when Java first came out. | Shish2k wrote: | It's basically as complicated as "If a subclass and a parent | class both define a `bar()` method, and you have an instance | of the subclass named `foo`, calling `foo.bar()` will call | the subclass version" | | I actually find that simpler than some of the spaghettis you | can create with Python's multiple inheritance :P | efnx wrote: | The types will be different depending on which operation is | used to clone so I wouldn't really worry about this one too | much. | badrequest wrote: | I 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. | friedman23 wrote: | I 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 wrote: | Quite the opposite. Rust won't even let you pull the trigger. | C++ on the other hand is very much littered with footguns. | coder543 wrote: | Now _that 's_ a strawman if I've ever seen one. | anonymous_sorry wrote: | Footguns 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. | cassepipe wrote: | One 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. | puffoflogic wrote: | Author has confused the drop functions. `Drop::drop` and | `mem::drop` have nothing to do with each other. | woodruffw wrote: | Hm? `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 | coder543 wrote: | The 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 wrote: | Oh, 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. | steveklabnik wrote: | > Even things that are Copy are implicitly Drop, it's | just that the copy is dropped instead of the value. | | While that mental model could make sense in an abstract | way, it's not literally true. Copy types are forbidden to | implement Drop. fn takes_drop<T: | Drop>(t: T) { todo!() } | fn main() { takes_drop(5i32); } | | gives error[E0277]: the trait bound | `i32: Drop` is not satisfied --> | src/main.rs:6:20 | 6 | | takes_drop(5i32); | ---------- ^^^^ the | trait `Drop` is not implemented for `i32` | | | | required by a bound introduced by | this call | note: required by a bound | in `takes_drop` --> src/main.rs:1:22 | | 1 | fn takes_drop<T: Drop>(t: T) { | | ^^^^ required by this bound in `takes_drop` | woodruffw wrote: | Thanks for the example! Yeah, I'm realizing my framing | (around the Drop trait, and not "droppability" as a | concept) was incorrect. | | Would it be more accurate to say that Rust guarantees the | droppability of owned values? I know there's a guarantee | that &'static items never have their underlying value | dropped, but that works because you can never actually | hold the static value itself, only its reference. | steveklabnik wrote: | > Would it be more accurate to say that Rust guarantees | the droppability of owned values? | | I'm not really sure, exactly, since "droppability" isn't | really a thing that's talked about, because as you're | sort of getting at here, it's universal, and therefore | not really an interesting concept. | | > I know there's a guarantee that &'static items never | have their underlying value dropped, | | Even this is sort of... not correct. Consider this | program: #[derive(Debug)] struct | Boom; impl Drop for Boom { fn | drop(&mut self) { println!("BOOM"); | } } use std::mem; static | mut FOO: Boom = Boom; fn main() { | let mut s = Boom; unsafe { | dbg!(&FOO); mem::swap(&mut FOO, | &mut s); dbg!(&FOO); } | } | | This will print BOOM, as the initial Boom is swapped out | from behind the reference and then dropped. | coder543 wrote: | I 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. | woodruffw wrote: | > 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 in the | generated code. | | I understand that types that don't implement Drop do not | _literally_ have an implicit `Drop` trait implemented for | them by the compiler. | | What I meant is that there is no "undroppable" type in | Rust: the best you can do is make the type panic in a | custom Drop implementation, but _any_ function that takes | ownership of a value is effectively described as | forwarding, dropping, or forgetting that value based on | the lifetime /type of its return. In other words, | `mem::forget` can _only_ be defined in terms of Drop (or | default drop behavior for a type) in terms of ownership | semantics, because its signature admits no other | possibilities. | coder543 wrote: | > In other words, `mem::forget` can only be defined in | terms of Drop (or default drop behavior for a type) in | terms of ownership semantics, because its signature | admits no other possibilities. | | But again, Drop is a destructor trait. It might be | confusing that this shares a name with the concept of | "dropping" in Rust, which is when a value goes out of | scope, but they're not the same thing. Not every value | has Drop, and mem::drop doesn't just work for values that | are Drop. It is not defined in terms of Drop, but just | Rust's ownership semantics. | | In fact, you can define a `drop` function that _only_ | accepts Drop types: https://play.rust- | lang.org/?version=stable&mode=debug&editio... | | Although I am disappointed that the automatically | generated Drop glue doesn't "count" for this purpose, and | there isn't a higher level Drop trait, so this isn't a | fully general solution. | | I also don't know where the concept of "undroppable" came | from for this conversation. Taken literally, that would | mean that the compiler would _emit an error_ any time a | value of that type would need to be dropped, so those | values could only exist in functions that either return | them or return `!`, or as global static values. I never | suggested that was a possibility, and Rust does not | support types that are undroppable, but it does support | types that are not Drop. | woodruffw wrote: | Thanks for the explanation! Yeah, I'm realizing that I'm | using these terms ambiguously: I do understand the | difference between dropping and Drop, but I tend to think | (incorrectly!) of the latter as an "implementation" of | the former, when it really isn't. ___________________________________________________________________ (page generated 2023-01-18 23:01 UTC)