[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)