[HN Gopher] Visualize Ownership and Lifetimes in Rust
       ___________________________________________________________________
        
       Visualize Ownership and Lifetimes in Rust
        
       Author : ljahier
       Score  : 204 points
       Date   : 2025-02-14 20:16 UTC (4 days ago)
        
 (HTM) web link (github.com)
 (TXT) w3m dump (github.com)
        
       | mrbonner wrote:
       | How many of those advanced concept in Rust such as borrow checker
       | & lifetime could be avoided as a beginner? If utmost performance
       | is not required (i.e., for mortals dealing with Java/Python/JS on
       | a daily basis) would those lifetime and borrow checker concepts
       | be a hinder to move quick?
       | 
       | I read this article https://corrode.dev/blog/prototyping/ and it
       | seems to address almost all of my concerns when I started
       | learning Rust. I think it could take a beginner a long way until
       | they need to get down to the borrow checker/lifetime. I said I
       | agree with the blog "mostly" because there are situations where
       | you have to interact with 3rd party libraries or APIs dealing
       | with those lifetime. The, you need to know about those concept.
       | If you know a better way to handle this, please let me know.
        
         | vlovich123 wrote:
         | Yup, that's definitely correct in terms of avoiding most of the
         | complexity. The tips in that blog post can apply to third party
         | libraries, but sometimes the complexity of the lifetime can
         | leak past the API boundary in a way you can't ignore (but often
         | you can).
        
         | echelon wrote:
         | > How many of those advanced concept in Rust such as borrow
         | checker & lifetime could be avoided as a beginner?
         | 
         | As a beginner, you can avoid references (&) and simply clone()
         | everything when it gives you trouble. If you start off by
         | writing simple Actix/Axum web services instead of manually
         | multithreaded apps, the problem domain is inherently linear and
         | you'll avoid lifetimes and the borrow checker almost entirely.
         | This lets you feel productive while getting a feel for the rest
         | of the language features.
         | 
         | Don't do this once you learn the ropes of the borrow checker,
         | of course. Once you grok it, the borrow checker is almost
         | second nature.
        
           | idontknowmuch wrote:
           | I don't think the basic usage of references is hard to grok
           | for a beginner. If you aren't going to mutate data and only
           | access it, then pass a reference. No need for over-
           | complicated semantics when describing it to a new Rust user.
        
             | teaearlgraycold wrote:
             | What if you want to add a reference field to a struct?
             | That's the point where I usually get pretty upset with how
             | Rust works.
        
               | MrJohz wrote:
               | You probably don't want a reference field in a struct, at
               | least to begin with. It's a lot easier to reason about
               | structs if they contain owned data only, and you take a
               | reference to the entire struct. There are some specific
               | cases where it might be sensible, or even necessary to do
               | that, but for someone who is still learning lifetimes,
               | these cases are unlikely to come up.
               | 
               | I recommend reading Steve Klabnik's "When should I use
               | String vs &Str?" post, which is generally good advice
               | when deciding between owned data and references. The
               | "Level 4" section covers the case of references in
               | structs.
        
               | duped wrote:
               | You get a lot more compiler help than when you try and
               | put a reference in a class in C++, and if you want to use
               | smart pointers its even better because you'll never have
               | to learn about move and copy constructors or copy and
               | move assignment operators.
        
         | mmastrac wrote:
         | I've tried to wrap up my philosophy on how a significant chunk
         | of rust code can be written without lifetimes using shared and
         | sharedmut primitives.
         | 
         | I've shipped three projects on it and they are pretty much as
         | performant as they can be. I've never regretted skipping the
         | lifetime work in application code.
         | 
         | https://github.com/mmastrac/keepcalm
         | 
         | I still dig into lifetimes for a lot of true low-level code but
         | it doesn't need to exist at all at the high level
        
         | duped wrote:
         | The places where you'll usually see lifetimes creep in where
         | you may not expect are in closures (say your app is one big
         | struct, you call a library function that takes a callback, you
         | reference &self in the callback and get an error that the arg
         | type must be 'static, or some other bound), or if you're
         | spawning async tasks and using locks you may get weird
         | Send/Sync and lifetime errors around await and spawn points.
         | 
         | They all make sense if you know why the program doesn't
         | compile, but it may be surprising to newbies.
        
         | gpm wrote:
         | The borrow checker and lifetimes aren't simply a matter of
         | performance, they are a matter of correctness. Languages
         | without them (go, java, etc) allow for bugs that they prevent -
         | dataraces, ConcurrentModificationException, etc. The fact that
         | you can only write through pointers that guarantee they have
         | unique access is what lets the language statically guarantee
         | the absence of a wide category of bugs, and what makes it so
         | easy to reason about rust code as a human (experienced with
         | rust). You can't have that without the borrow checker, and
         | without that you lose what makes rust different (it would still
         | be a fine language, but not a particularly special one).
         | 
         | You could simplify rust slightly by sacrificing performance.
         | For example you could box everything by default (like java) and
         | get rid of `Box` the type as a concept. You could even make
         | everything a reference counted pointer (but only allow mutation
         | when the compiler can guarantee that the reference count is 1).
         | You could ditch the concept of unsized types. Things like that.
         | Rust doesn't strive to be the simplest language that it could
         | be - instead it prefers performance. None of this is really
         | what people complain about with the language though.
        
           | cageface wrote:
           | I recently came to this realization in a large typescript
           | codebase. It's really important to understand who owns data
           | and who has the right to modify it. Having tools to manage
           | this and make it explicit built into the language is so
           | helpful for code correctness and is especially beneficial for
           | maintaining code you didn't write yourself.
        
             | diggan wrote:
             | Another great way of handling this if you cannot switch out
             | the language, is to start adopting a more functional
             | approach and also try to keep mutations into one place/less
             | places. So instead of having all the X
             | services/adapter/whatever being able to pass data around
             | that they mutate along the way (like the typical "hiding
             | implementation details in objects/classes"), have all those
             | just do transformation on data and return new data, then
             | have one thing that can mutate things.
             | 
             | Even if you cannot go as extreme as isolating the mutation
             | into just one place, heavily reducing the amount of
             | mutation makes that particular problem a lot easier to
             | handle in larger codebases.
        
               | cageface wrote:
               | Right this is what I tried to do but unfortunately trying
               | to mark everything immutable in Typescript leads to some
               | very unergonomic type signatures. Hopefully this can
               | improve in the future.
        
               | diggan wrote:
               | Could you show an example of what you mean? Not sure how
               | _not_ mutating data would lead to more unergonomic type
               | signatures, I 'm sure an example would help me
               | understand. Although it wouldn't surprise me TypeScript
               | makes things harder.
        
               | tekkk wrote:
               | You are polluting every variable signature with
               | `readonly`. This also can create cascading effects where
               | making one function accept only readonly variables forces
               | you to declare readonly elsewhere as well. Quite similar,
               | in a way, to Rust.
        
               | cageface wrote:
               | If I have a complex structure MyStruct that I make
               | recursively readonly it doesn't show up in the IDE as
               | DeepReadOnly<MyStruct> or something like that. It shows
               | up as a huge tree of nested readonly declarations, so the
               | original type is highly obscured.
        
           | eaglelamp wrote:
           | Couldn't the same guarantees be achieved with immutability?
           | Of course this would be setting aside concerns with
           | performance/resource usage, but the parent is describing an
           | environment where these concerns are not primary.
           | 
           | Personally I find it much easier to grok immutable data, not
           | just understand when concentrating on it, then ownership
           | rules.
        
             | gpm wrote:
             | Absolutely, with full immutability the borrow checker
             | doesn't give you much at all.
             | 
             | It also doesn't cost you much, the borrow checker just gets
             | out of your way if you just wrap all your immutable data in
             | reference counted pointers (try out imbl [1] for instance).
             | It's not free - there's some syntax overhead compared to a
             | language that was intended to primarily work this way - but
             | it's cheap.
             | 
             | I think it's reasonable to view the borrow checker as a
             | generalization of immutability. Immutability says "no
             | mutation", the borrow checker says "no mutation unless you
             | are the only thing that might be accessing the data". Edit:
             | Worth noting though that the rust standard library and
             | ecosystem is a take on this that doesn't emphasize staying
             | within the fully immutable regime as much as it could,
             | instead preferring to improve performance. A variant of
             | rust that tried to explore keeping more things immutable
             | would be interesting.
             | 
             | Personally my take is that there are some problems which
             | are very naturally represented immutably, and there are
             | others that are very hard to fit in that framework. The
             | borrow checker is general enough to capture almost all of
             | that second category as well. But if you're firmly in the
             | first category, and you aren't worried about every last
             | drop of performance, there's probably some managed language
             | with strong immutability that is a better fit.
             | 
             | [1] https://github.com/jneem/imbl
        
         | baq wrote:
         | Ownership isn't an advanced concept. It is a software
         | engineering problem, not a rust problem. Rust is one of the few
         | languages which make it explicit and even checkable at compile
         | time and the first popular one.
         | 
         | What is hard is designing systems in a way resource ownership
         | can be tracked and controlled without impacting performance.
         | Rust makes it possible, but you can use smart pointers to give
         | up speed and take simplicity instead. Most other languages
         | assume (rightly so) you're too dumb to do it correctly and give
         | you smart pointers by default; some assume you're smart enough
         | and are proven wrong all the time (this is assembly and C
         | relatives; actually they say 'we don't want smart pointers
         | _and_ we want a simple compiler, sucks to be you').
        
           | lenkite wrote:
           | It very quickly becomes a special Rust-only software
           | engineering problem. Rust has no partial borrows and this
           | affects many designs where a lot of data needs to access
           | fields of other data. Consequently, you see humongous large,
           | flat structures in many Rust projects. And of-course, the
           | famous "replace references with array indices" and just skip
           | the borrow checking and lifetime rules by simply making your
           | own custom pointer system - which is also common in many Rust
           | projects and famously popularized by the "Object Soup is Made
           | of Indices" Rust post here on HN.
        
             | baq wrote:
             | I assume you mean these are bad things; I see it as
             | 'ownership enforcement pushing architecture towards memory
             | safety' thing. Path of least resistance changes for the
             | better - if you don't want to use a Box or a RefCell, that
             | is.
        
               | lenkite wrote:
               | Yes, these are bad things. The extreme burden imposed by
               | lifetimes and the prevention of easy refactoring for
               | changes causes spectacular design bloat via workarounds
               | and safety circumvention mechanisms which are unique to
               | Rust projects. Its a special and necessary Rust skill.
        
               | baq wrote:
               | You only need to circumvent safety if you have it...
               | 
               | You can choose to have it in runtime. You don't get that
               | choice pretty much anywhere else. If you don't want to
               | make that choice in a granular way as rust allows, pick a
               | language from the other two groups.
        
           | wavemode wrote:
           | No, Rust definitely does add some additional complexity on
           | top of the inherent complexity of ownership. Despite what
           | some people think, Rust's borrowing rules are actually
           | extremely simple. So simple that they reject a lot of safe
           | programs.
           | 
           | Paradoxically, programmer life would be made simpler if there
           | were some more complex borrowing rules, that would allow (for
           | example) partial borrows of objects, or allow aliasing &mut
           | in single-threaded circumstances where it's known to be safe
           | (i.e. when the data is something primitive like an int, where
           | it doesn't actually matter if it's overwritten while
           | referenced).
           | 
           | But I know there's extra language design complexity that this
           | introduces, and extra codegen complexity (Rust makes certain
           | aliasing promises to LLVM that it isn't allowed to break) so
           | it will take time. But, there are proposals in the works.
        
             | gpm wrote:
             | > or allow aliasing &mut in single-threaded circumstances
             | where it's known to be safe (i.e. when the data is
             | something primitive like an int, where it doesn't actually
             | matter if it's overwritten while referenced).
             | 
             | Incidentally this is basically what the `Cell` type does. I
             | suspect that making it the default wouldn't make it harder
             | for me to reason about the code I'm working on - but it is
             | an interesting proposal.
        
             | j-krieger wrote:
             | Partial borrowing is not that much of a problem for the
             | borrow checker. It _is_ a problem for bikeshedding core
             | language developers, apparently...
        
         | juped wrote:
         | None. And lifetime elision and other forms of pretending to
         | beginners that they don't exist are why so many people have
         | trouble with Rust.
        
         | ForHackernews wrote:
         | ... just write Java/Python/JS, then? Or Kotlin, Typescript,
         | Python+type annotations if you're feeling more modern.
         | 
         | There's nothing wrong with any of those languages (mostly) and
         | not everything has to be written in Rust. IMHO the real value
         | of Rust is as a systems programming language that's safer than
         | C.
        
         | NoboruWataya wrote:
         | I think you will run into the borrow checker pretty soon,
         | because you have to deal with it whenever you deal with
         | references (which you will inevitably have to do if you are
         | dealing with anything more complex than number types). But that
         | aspect of the borrow checker is not that difficult. You could
         | avoid it entirely by cloning everything but IMO it's not
         | necessary and you would do better to invest a small amount of
         | time in understanding why the borrow checker is complaining.
         | 
         | Lifetimes you can probably get further without having to deal
         | with. Just avoid storing references in structs and you will
         | avoid a lot of lifetime headaches. Cloning can again be helpful
         | here.
         | 
         | An alternative to cloning everything, if you are dealing with
         | simple data types, is to derive copy for your structs so you
         | can pass them around without worrying about ownership. It's not
         | always possible though.
         | 
         | Smart pointers are another workaround, as others have said. But
         | my problem with (some) smart pointers is that they simply move
         | the checks to runtime, meaning now your code has a much higher
         | chance of panicking at runtime.
        
           | mrbonner wrote:
           | That's basically being the point of the article I shared
        
         | csomar wrote:
         | You can Arc<Mutex<T>> your way out of ownership/borrowing. At
         | some point, you'll start to see your program as just data (and
         | functional programming will make sense as the only way to
         | program) moving around. I think it's an under-rated way to get
         | started (I started that way) and at some point a bulb will
         | light up and you'll start seeing programming as data moving
         | around and you'll care about ownership/borrowing at the
         | foundational/prototype level.
        
         | nocarrier wrote:
         | That article on rapid Rust prototyping matches my experience
         | with using Rust as the backend for a web and iOS app. I used
         | clone() and Vec and String and other shortcuts from that
         | article as much as possible since I was building a backend
         | application versus an operating system. It enabled a lot more
         | velocity and made it fun to add features. And it was still
         | blazing fast.
         | 
         | If anyone is considering using Rust and is nervous about
         | lifetimes and bare metal, check out that article and try its
         | guidance. I learned these things on my own the hard way and
         | would have loved to read this article 18 months ago. It's
         | really quite good.
        
         | the_gipsy wrote:
         | You can skip lifetimes in the beginning, and I think that's the
         | sane thing to do, if you come from a GC languages.
         | 
         | Borrow checker, well, this actually includes lifetimes. But
         | let's say "basic ownership and basic borrowing", there is no
         | way around starting with that, and it should be a point to
         | learning rust.
        
       | ninetyninenine wrote:
       | Id like a plugin for rustrover.
        
         | echelon wrote:
         | A family of these plugins would be a true killer app for Rust
         | beginners.
        
         | juped wrote:
         | Rustrover can do this, although unfortunately it requires cargo
         | check/clippy turned on rather than being native (still faster
         | than LSP, at least).
         | 
         | https://www.jetbrains.com/help/rust/rust-external-linters.ht...
        
           | nicce wrote:
           | I have found clippy better anyway.
        
             | tialaramex wrote:
             | Clippy lints are nice. I'm a particular fan of
             | `while_let_loop` which is basically Clippy looks at a Rust
             | loop you wrote and it says "Hey, this is equivalent to
             | while let SomePatternHere = some_function(blah) { };" and
             | mostly you realise oh yes it is, and that's much clearer
             | than what I wrote.
             | 
             | Once in a while I get that lint and I think no, what I
             | wrote is easier to understand, and I just
             | #[allow(clippy::while_let_loop)] to acknowledge that.
             | 
             | The way I end up with these loops is I realise I need a
             | loop - we're definitely doing _something_ here more than
             | once, so in Rust that 's loop - then during further
             | development and refinement of the software I get the exact
             | rules correct and then it's obvious (to clippy) that this
             | is a while-let loop, often it's in the form while let
             | Some(thing) = container.pop() { /* do stuff with the thing,
             | maybe putting more stuff in the container in the process */
             | }; but the loop { } construction is harder to read if left
             | in that form and the whole _point_ of source code is to be
             | readable to humans.
             | 
             | Many languages don't like to give these diagnostics names.
             | The reality is that these names mean something. If you
             | called it 81402 then now that's just arcane knowledge, like
             | if you insisted on calling all the elements by their atomic
             | number. If we call it "Element 26" rather than "Iron" it's
             | the same except harder to remember. I think software which
             | uses numbers here is trying to avoid the semantic value,
             | but that's never going to work, so just embrace it.
        
             | juped wrote:
             | It's slow to call out to external linters. But it's even
             | slower to use LSP (which rustrover saves me from), it's
             | really not a huge deal in practice for me.
        
       | superkuh wrote:
       | It's too bad the lifetime of rustc is only about 3 months before
       | breaking changes (new features, updates, etc) are introduced to
       | the compiler and used by devs.
        
         | gpm wrote:
         | New features aren't usually considered breaking changes, only
         | modifying existing ones so that old code doesn't work.
         | 
         | The rust compiler never strives to _never_ introduce breaking
         | changes (by the definition I just described). It doesn 't quite
         | succeed (because some things like correctness are considered
         | more important), but it fails
         | 
         | a) Very rarely, not once every 3 months.
         | 
         | b) In very small ways, that only break a tiny portion of code.
         | 
         | c) In ways that are very easy to fix.
         | 
         | d) Usually the rustc-devs will go offer patches to the entire
         | open source ecosystem before any such release.
        
           | superkuh wrote:
           | I imagine that is a dev's perception (in shipping rust
           | binaries to users) with their rolling OS and constantly
           | updated rustc. But as a user trying to compile rust code
           | written by rust devs what happens is that it won't compile
           | because the compiler is 3 months out of date and rust devs
           | immediately use new features. I had this happen personally
           | often enough it put me off even trying out rust written
           | applications. This is not things I've heard. It is things
           | I've directly experienced.
           | 
           | rustc is a rolling only compiler and that's not great and it
           | does break often (not be able to compile code) in distros
           | that are not rolling. And no, curl|sh and/or rustup are not
           | solutions. I think the only solution is waiting for rust to
           | become popular enough that the proportion of bleeding edge
           | using devs to normal devs goes down.
        
             | gpm wrote:
             | Right, that's usually described as "forwards
             | compatibility", and yes, rust the language doesn't attempt
             | to supply it. And yes, rustup is in fact a (the) solution
             | for users. Distros increasingly package it because it is
             | the recommended way to use the rust compiler (e.g. if
             | you're on the latest ubuntu or any of its derivatives you
             | can install it with apt instead of curl|sh).
             | 
             | A decent portion of projects, especially bigger ones, will
             | try to support stables going back a few versions, but at
             | best they're just changing the timeline slightly. This is
             | really intended to help packaging for distributions, not
             | users. Users should just use a compiler at least as up-to-
             | date as the software they are trying to build.
        
               | superkuh wrote:
               | Exactly. rustc has a lifetime of about 3 months. gcc has
               | a lifetime of about 5-10 years. Perl's interpreter has a
               | lifetime of about 20 years.
               | 
               |  _edit since I can 't reply_: The difference is that Perl
               | devs don't immediately use the new features (and Perl 6
               | is not Perl). Bash too constantly gets incompatible
               | (forwards) changes, but no Bash dev is so inconsiderate
               | as to use these as they want their software to actually
               | run on people's machines.
               | 
               | That's my entire point: rust's immature/bleeding edge dev
               | culture causes the problem. rustc could be okay, but the
               | culture is too bleeding edge for using rust software now.
               | Maybe in a decade it'll settle down.
        
               | gpm wrote:
               | Perl 5 introduced forwards incompatible changes less than
               | a year ago. Perl 6 was so backwards incompatible that
               | after 20 years of work they renamed the language to
               | something else - it last had forwards incompatible
               | changes sometime in january.
               | 
               | Not only are these not meaningfully better, they're also
               | the opposite of what I would hold out as a "successful
               | versioning model".
               | 
               | Rustc lasts until you are using software that depends on
               | a more modern version of rustc, just like any dependency.
               | Then you upgrade it - which just like any dependency with
               | backwards compatibility - is painless (and in fact
               | entirely transparent if you use tooling like rustup).
        
             | duped wrote:
             | > And no, curl|sh and/or rustup are not solutions.
             | 
             | I'm confused - you are frustrated that the official release
             | channel of software that is used by almost all users of
             | that software is "not a solution" because you want to use
             | unofficial release channels that have outdated versions of
             | the software that are much more rarely used?
        
               | superkuh wrote:
               | Correct. I avoid rust because it is a language where it's
               | pretty much infeasible to have a compiler from your
               | system repositories unless it's a rolling distro. Having
               | some random application require updates so often (like a
               | browser) is marginally acceptable if distasteful. But a
               | compiler and toolchain that exists entirely outside my
               | distro? No thanks.
               | 
               | What I am confused about is how everyone is pretending
               | this is a normal software situation. It's vastly weird
               | and different from most compilers and toolchains. It may
               | be normal if you're coming from web dev but web dev is
               | _not_ normal and not a healthy ecosystem.
        
               | duped wrote:
               | It's totally normal except for C/C++ toolchains _on
               | Linux_. C /C++ compilers are managed this way on MacOS,
               | Windows, and by most embedded toolchains.
               | 
               | Why don't you want your toolchain managed outside your
               | distro? Don't you want your software to run outside your
               | distro?
        
       | Exuma wrote:
       | Wow this is super cool. Is this pretty reliable for more advanced
       | scenarios? Simple ones I wouldn't need to see this on, but the
       | more complex things I'd be very interested in perusing code to
       | make certain things click.
        
       | bfrog wrote:
       | This would be fantastic in helix
        
       ___________________________________________________________________
       (page generated 2025-02-18 23:01 UTC)