[HN Gopher] Borrow checking without lifetimes
       ___________________________________________________________________
        
       Borrow checking without lifetimes
        
       Author : todsacerdoti
       Score  : 80 points
       Date   : 2024-03-04 18:52 UTC (4 hours ago)
        
 (HTM) web link (smallcultfollowing.com)
 (TXT) w3m dump (smallcultfollowing.com)
        
       | JonChesterfield wrote:
       | This is about moving rust's borrow checker into the type system.
       | I don't follow the details (not a rust dev) but the top level
       | plan of doing static checks in the static type system sounds
       | right. Has interesting links to related projects.
        
         | eslaught wrote:
         | There's more going on here. They are adding a notion of
         | "places" (what I would call "field paths") and an explicit
         | syntax for "modes" (what I would call "privileges"). So you can
         | now say:                   shared(x.a.b.c)
         | 
         | or:                   mut(y.d.e.f)
         | 
         | That makes it possible to pass around a reference with the
         | promise that the callee will only access a _subset_ of the
         | fields of whatever referenced type you 're talking about, as
         | well as other things.
         | 
         | Superficially it looks very much like what I did in Regent [1]
         | though I am sure the details differ widely.
         | 
         | [1]: https://regent-lang.org/tutorial/06_privileges/
        
           | cwzwarich wrote:
           | The Rust borrow checker has had this model for over a decade
           | at this point; it's just not exposed as part of the language.
           | One strong principle in Rust language design is to avoid
           | proliferation of too many distinct reference types. In this
           | case, I personally wish the language had support for it,
           | since it would allow you to seamlessly refactor a piece of
           | code into its own function.
        
             | pornel wrote:
             | The article mentions expanding capabilities to support
             | self-referential structs.
             | 
             | Currently structs are allowed to borrow from any data
             | outside of them, but not from any of their own fields. Rust
             | doesn't want to have move constructors, and borrowing from
             | inline fields can't be safe, but borrowing from heap-
             | allocated data owned by the same struct should be possible,
             | and currently there's no non-hacky way to express that.
        
               | cwzwarich wrote:
               | I was specifically referring to the ability to borrow a
               | set of fields/paths. Enabling more useful self-
               | referential structs would be something more novel.
        
       | nu11ptr wrote:
       | I only skimmed this article, but is it fair to say if this makes
       | it into Rust proper that we would gain self referential structs?
        
         | orlp wrote:
         | No, self-referential structs are disallowed by the nature of
         | Rust having move semantics by default, without a concept of a
         | 'move constructor'.
        
           | slashdev wrote:
           | It's not clear to me that the compiler couldn't detect when
           | you try to move a self referential string and prevent it. It
           | basically does if you use pin.
        
             | orlp wrote:
             | I'm not saying a theoretical language couldn't be made that
             | allows this, but in Rust it would be breaking backwards
             | compatibility.
             | 
             | For example [T]::sort (obviously) needs to be able to move
             | the elements in the passed array, but there is no Move
             | bound on T or something similar.
             | 
             | > It basically does if you use pin.
             | 
             | And we have Pin today, but I assumed the OP meant 'self-
             | referential struct' as in, without using Pin.
        
               | nu11ptr wrote:
               | > And we have Pin today, but I assumed the OP meant
               | 'self-referential struct' as in, without using Pin.
               | 
               | I did, but with Pin would be fine as well
        
             | comex wrote:
             | Right. Pin is the canonical way to have objects that can't
             | be moved, and it was invented in order to support objects
             | with self-references (namely, futures). However, Pin's
             | design is pretty hacky, being implemented purely 'in
             | userspace' without any language support.
             | 
             | There's widespread desire to add some form of language
             | support for pinning someday, if only to make the ergonomics
             | a bit nicer (e.g. not needing a method call to reborrow
             | mutable pinned references). This would probably also be
             | needed in order for the compiler to support self-
             | referential structs. From I've seen there are two quite
             | different proposals for how to add language support for
             | pinning. One is to add sugar to `Pin` while keeping the
             | design mostly the same. For example, `Pin<&mut T>` could
             | turn into `&pin mut T` or something. The other is to
             | essentially throw away `Pin` and replace it with a `Move`
             | auto trait: instead of `Pin<&mut T>` you would just have
             | `&mut T` where `T: !Move`.
             | 
             | A `Move` trait would be quite disruptive, but would also
             | have many benefits and ultimately simplify the language.
             | For example, with `Pin`, every type has two possible kinds
             | of mutable references to it: `Pin<&mut T>` and `&mut T`. In
             | the case of self-referential structs, how would a non-
             | pinned reference to one work? And how would that interact
             | with `Drop`, which always takes `&mut self`? You could
             | answer these questions one way or another, just like
             | they're answered for futures (although futures have it
             | easier because they always start in a non-self-referential
             | state). But it all becomes simpler if you take the `Move`
             | approach, where `&mut T` _is_ pinned if `T` is `!Move`, and
             | there is no second reference type to worry about.
        
               | armchairhacker wrote:
               | Rust already has the `Unpin` trait, which is identical to
               | `Move` except that it doesn't pin `&mut T`. I've never
               | understood why Rust doesn't require the `Unpin` trait in
               | `mem::swap` and derivatives instead of having `Pin`.
               | 
               | I also don't really know of any other methods / cases
               | besides those using `mem::swap` where `Pin` and `Unpin`
               | are relevant. It works for `swap` because if you have a
               | self-referential data structure, there's never a good
               | reason you want to tangle the references.
               | 
               | Lastly, normally a mutable reference guarantees there
               | aren't other references, but a mutable reference to a
               | self-referential data-structure doesn't have this
               | guarantee because it references itself. Is this why
               | `Pin<&mut T>` is necessary, because a real mutable
               | reference to a set-referential data-structure violates
               | Rust's not-fully-defined "borrowing rules"? And do we
               | want to keep that specific rule (since we're mutable
               | borrowing the entire region including the self-
               | referential borrow, and self-referential structures are
               | already unsafe, I doubt it would cause any issues)? Maybe
               | `&pin mut T` should be added, but as its own kind of
               | reference instead of syntax sugar...
        
           | Arnavion wrote:
           | Self-referential structs work fine in Rust and always have.
           | 
           | https://play.rust-
           | lang.org/?version=stable&mode=debug&editio...
           | 
           | The compiler will correctly prevent you from moving the
           | value. (Try adding `drop(foo);` at the end.)
           | 
           | The other way to have a struct that requires mutation to move
           | in and out of self-referential-ness (as `async {}` needs, for
           | example) can be achieved with `unsafe` and
           | `Pin::new_unchecked`.
        
             | orlp wrote:
             | > Self-referential structs work fine in Rust and always
             | have.
             | 
             | They 'work fine' in that the Rust compiler will let you
             | create one and then never let you move it or access the
             | whole object mutably again while it is self-referential.
             | They don't 'work fine' as in being first-class objects in
             | the language that work as any other object.
             | 
             | For what it's worth, I think Rust made the right choice in
             | not allowing custom move constructors that would enable
             | first-class self-referential types.
        
         | comex wrote:
         | Yes. From the post:
         | 
         | > In follow-up posts I'll dig into how we can use this to
         | support interior references and other advanced borrowing
         | patterns.
         | 
         | [..]
         | 
         | > But also [current] Rust is not able to express some important
         | patterns, most notably interior references, where one field of
         | a struct refers to data owned by another field.
        
       | olliej wrote:
       | I'm curious to see how this turns out, but one thing I'm not sure
       | I agree with is that lifetimes are confusing?
       | 
       | I want to be clear here, it's been a few years since my job was
       | coding in rust, but I found lifetimes to probably be among the
       | easiest parts of rust to understand. Is the issue actually not
       | understanding what lifetimes are, or just that people need to
       | annotate them explicitly (which obviously other languages don't
       | do[1])? The former seems surprising to me, the latter I could
       | understand more readily due to the combination of the
       | newness/novelty of explicitly annotating the lifetime, and also
       | the ' prefix to lifetime names which I can imagine triggering a
       | "that's weird looking" response when people first need them - I
       | _think_ I may have had that response at first, but it was fairly
       | brief as it 's just an initially new/novel syntax, and even now
       | having the sigil prefix seems like a reasonable way to
       | distinguish lifetimes from anything else, it's similar to why
       | there's a * in generators in JS despite it not being strictly
       | necessary for parsing or type checking.
       | 
       | One thing that did just occur to me though is how many adopters
       | of rust were coming from languages with entirely automatic
       | lifetime management (GCs, etc) so having to think about object
       | lifetimes _at all_ is new? I realize I 've mostly assumed people
       | adopting rust have been C, C++, etc devs where thinking about
       | such things is mandatory and continuous, but if there are lots of
       | adopters from managed languages then maybe that's new. In that
       | case though I don't think the issue is rust's lifetimes
       | specifically in as much as it's just object lifetimes at all?[3]
       | 
       | [1] Because they don't need to because of implicit lifetime
       | management (GC, automatic refcounting[2], etc) or because their
       | programming model is "what memory safety?"
       | 
       | [2] Yes I know refcounting is technically GC, but this is
       | comparison to scanning GCs
       | 
       | [3] [this is an addition] I want to be clear, I am not critiquing
       | devs who have come from managed languages for not being aware of
       | lifetimes, this is literally just saying "if you're from a
       | language where it's not a thing you have to think about, the
       | entire concept may be new"
        
       | pimlottc wrote:
       | For someone unfamiliar with rust, the title is a real crash
       | blossom.
        
         | oersted wrote:
         | Thank you for teaching me a new phrase :)
        
       | logicprog wrote:
       | The push to eliminate lifetimes in favor of loans (with this or
       | polonius) in the pursuit of allowing a larger subset of correct
       | programs to be expressed in Rust makes sense at first blush -- of
       | course we want to be able to prove more correct programs correct!
       | Who wants to fight the borrow checker all the time over things we
       | know are fine! -- but I'm concerned in the long run it will
       | actually turn out to be a bad thing.
       | 
       | I vehemently disagree with the article that lifetimes are at all
       | nebulous or hard to grasp, IMO they're a pretty straightforward
       | concept, and they map really nicely onto single ownership, move,
       | RAII based memory management, and underlying C style memory
       | management concepts, whereas OTOH it feels like loans are perhaps
       | less nebulous, but also map more poorly onto the best
       | comprehensible, and specific ways to think about low level memory
       | management (and also less well to concepts in other low level
       | languages lile C++). Instead of seeing your program as a mostly
       | 1D collection of mostly contiguous scopes that the program
       | counter jumps around in, now you have to view it as a gigantic
       | thicket of constraints.
       | 
       | So in essence, by making Rust's static analysis more powerful and
       | less annoying at first, we're actually making it harder to fully
       | grasp in the long run. It's sort of like the Haskell monad
       | problem -- the more powerful you make your compiler/language
       | abstractions to allow proving more code, the less comprehensible
       | everything gets. And I think in both cases, trading _some_ power,
       | past a certain point, for long term comprehensability with more
       | straightforward concepts is better.
        
         | logicprog wrote:
         | I like Rust a lot right now, but with polonius and some of the
         | unnecessary and weird syntactic sugar that's being added, plus
         | the fact that instead of full coroutines to encompass both
         | async and generators (like Kotlin has) we're getting neutered
         | coroutines to do generators and async is a separate but similar
         | concept, I think the Rust designers are making a lot of
         | missteps lately. I know nothing's perfect, but it kind of
         | sucks.
        
           | armchairhacker wrote:
           | "You have become the very thing you swore to destroy!"
        
         | cogman10 wrote:
         | Counter point, my day to day programming is in GC languages. In
         | those, how long something lives is basically anyone's guess,
         | could be the entire length of the program, could be 2 seconds
         | from now.
         | 
         | This isn't something that really causes headaches, it's
         | desirable. So long as when you come in conflict with the borrow
         | checker there's an ability to unwind it, I don't see the harm.
         | 
         | My assumption is the lifetime explanation while incorrect will
         | also still work (I couldn't imagine it wouldn't as that'd break
         | too much). So assuming you do run into these compiler problems,
         | you can still revert to the old simpler mental model and move
         | forward.
        
       | adamwk wrote:
       | I may be out of my depth here as I've only casually used Rust,
       | but this seems similar to Swift's proposed lifetime
       | dependencies[1]. They're not in the type system formally so maybe
       | they're closer to poloneius work
       | 
       | [1]: https://github.com/apple/swift-
       | evolution/blob/3055becc53a3c3...
        
       ___________________________________________________________________
       (page generated 2024-03-04 23:01 UTC)