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