Rust Lang Team
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Owners
        • Signed-in users
        • Everyone
        Owners Signed-in users Everyone
      • Write
        • Owners
        • Signed-in users
        • Everyone
        Owners Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Help
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Owners
  • Owners
  • Signed-in users
  • Everyone
Owners Signed-in users Everyone
Write
Owners
  • Owners
  • Signed-in users
  • Everyone
Owners Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    --- title: "Design meeting 2024-08-07: Pin ergonomics" tags: ["T-lang", "design-meeting", "minutes"] date: 2024-08-07 discussion: https://rust-lang.zulipchat.com/#narrow/stream/410673-t-lang.2Fmeetings/topic/Design.20meeting.202024-08-07 url: https://hackmd.io/q3_YFTQ0RS-egNuYdstfrA --- # `Pin` Ergonomics and `Move` Thanks for TC and Yosh for their feedback in improving this document! ## Motivation Users sometimes need to work with address-sensitive types. A common example is with futures, but there are other cases such as intrusive linked lists.[^address-checksum] Because these types are address sensitive, they cannot move. Address-sensitive types have strong overlap with self-referential types, but they are not exactly the same. There are self-referential types that are not address sensitive and there are address sensitive types that are not self-referential. [^address-checksum]: Another example is in the [Rust SymCrypt] bindings, because the underlying SymCrypt library data structures often include a checksum field that is derived in part from the address of the structure. Today, `Pin` gives us the ability to work with address-sensitive types, yet the current ergonomics of `Pin` are seriously lacking. We have gotten by so far because `Pin` is only used in a few use cases and most users do not directly interact with `Pin`. Upcoming language features, such as async closures and async iterators[^afit-poll_next] will create more places where for users interact with `Pin` and create new opportunities to define self referential and address sensitive types. [^afit-poll_next]: This is true regardless of whether we use a `async fn next` design for async iterators or a `poll_next` design. Possible solutions to solutions to `Pin`'s poor ergonomics would likely be in one of these categories. 1. Improve the ergonomics of `Pin` with more language support. 2. Add an alternative to `Pin` that supports address-sensitive types. This document primarily discusses a way to do Option 1. We will also touch a design for a `Move` trait. Note that because this document is primarily rooted in the narrow motivation of improving the ergonomics around address-sensitive types, this document will undersell the possible benefits of `Move`. ## Motivating Example Below is a code example that demonstrates the kinds of data structures that come up more often with async closures. The example has roughly the same shape as merging two streams. It shows an `Interleave` struct that contains two async closures. Then the `call_both` method will call each closure which returns two futures. The `call_both` method then polls each future until one completes. The function then returns the value of the first one to complete but crucially, it does not destroy the other future. Instead, the slower future persists until the next call to `call_both`. There are a couple aspects that make this program interesting: - The struct contains both an async closure and the future returned by the closure. - `call_both` leaves a future in a partially completed state across calls. This means those futures have been pinned, which means the `Interleave` struct must be pinned, which means `call_both` must take a pinned self argument. Without these properties, you can generally get by with an async block or similar to let the compiler handle all the self-referential details. ```rust struct Interleave<Fn1, Fn2, T> where Fn1: async Fn() -> T, Fn2: async Fn() -> T, { _phantom: PhantomData<fn() -> T>, fn1: Fn1, fn2: Fn2, fn1_fut: unsafe<'u> Option<Fn1::CallRefFuture<'u>>, fn2_fut: unsafe<'u> Option<Fn2::CallRefFuture<'u>>, } impl<Fn1, Fn2, T> Interleave<Fn1, Fn2, T> { fn new(fn1: Fn1, fn2: Fn2) -> Self { Interleave { _phantom: PhantomData, fn1, fn2, fn1_fut: None, fn2_fut: None, } } /// Calls both fn1 and fn2 and advances their future /// /// Returns the value from the first one to finish. /// The next call will continue to poll the future that /// did not finish. async fn call_both(Pin<&mut Self>) -> T { // ^^^^^^^^^^^^^ needs to be pinned because it holds incomplete futures across invocations if self.fn1_fut.is_none() { // SAFETY: pin projection unsafe { // not that map_unchecked_mut is the only unsafe part here. self.as_mut().map_unchecked_mut(|this| &mut this.fn1_fut).set((self.fn1)()); } } if self.fn2_fut.is_none() { // SAFETY: pin projection unsafe { // not that map_unchecked_mut is the only unsafe part here. self.as_mut().map_unchecked_mut(|this| &mut this.fn2_fut).set((self.fn2)()); } } poll_fn(|cx| { // SAFETY: pin project, and something about 'unsafe lifetimes unsafe { match self.as_mut().map_unchecked_mut(|this| this.fn1_fut.as_mut().unwrap()).poll(cx) { Poll::Pending => match self.as_mut().map_unchecked_mut(|this| this.fn2_fut.as_mut().unwrap()).poll(cx) { Poll::Pending => Poll::Pending, Poll::Ready(x) => { // call the closure again before returning self.as_mut().map_unchecked_mut(|this| this.fn2_fut.as_mut().unwrap()).set((self.fn2)()); Poll::Ready(x); } } Poll::Ready(x) => { // call the closure again before returning self.as_mut().map_unchecked_mut(|this| this.fn1_fut.as_mut().unwrap()).set((self.fn1)()); Poll::Ready(x); } } } }).await } } ``` Note that this will also need `unsafe<..>` binders to work, but that's another feature for a different design discussion. ## Some challenges with `Pin` - Pinning and pin projection is is a unique mechanism in Rust that exists outside of the language. It isn't expressed through e.g. traits like many other features are. - `!Unpin` as a bound is a double negation and hard to reason about - `Unpin` only has meaning when combined with `Pin` - It's not possible to directly return `Pin<T>` types from constructors - Needing to write `.as_mut()` all the time. - One of the only places where arbitrary self types show up. - Pin projection need either `unsafe` for a crate that encapsulates this unsafety. - Generalizing over pinned or not-pinned requires writing two functions. - This applies to traits (although you can implement a trait for `Pin<&mut T>`). ## `&pin mut T` One path is to add pinned references. There have been many parallel ongoing discussions about these. In general, these discussions propose adding the following: - The ability to automatically reborrow `Pin` pointers to avoid needing to write `.as_mut()` everywhere - `&pin mut T` and `&pin const T` as sugar for `Pin<&mut T>` and `Pin<&T>` - Auto-pin for calling methods with a pinned `self` - `let pin x = ...` as a way of indicating places that can be pinned - Pinned fields on data types - A pinned `drop` for types with pinned fields With these changes, the earlier example would look like this: ```rust struct Interleave<Fn1, Fn2, T> where Fn1: async Fn() -> T, Fn2: async Fn() -> T, { _phantom: PhantomData<fn() -> T>, fn1: Fn1, fn2: Fn2, pin fn1_fut: unsafe<'u> Option<Fn1::CallRefFuture<'u>>, pin fn2_fut: unsafe<'u> Option<Fn2::CallRefFuture<'u>>, } impl<Fn1, Fn2, T> Interleave<Fn1, Fn2, T> { fn new(fn1: Fn1, fn2: Fn2) -> Self { Interleave { _phantom: PhantomData, fn1, fn2, fn1_fut: None, fn2_fut: None, } } /// Calls both fn1 and fn2 and advances their future /// /// Returns the value from the first one to finish. /// The next call will continue to poll the future that /// did not finish. async fn call_both(&pin mut self) -> T { // ^^^^^^^^^^^^^ needs to be pinned because it holds incomplete futures across invocations if self.fn1_fut.is_none() { self.fn1_fut = (self.fn1)(); } if self.fn2_fut.is_none() { self.fn2_fut = (self.fn2)(); } poll_fn(|cx| { // unsafe still needed for unsafe lifetime binders unsafe { match self.fn1_fut.as_mut().unwrap().poll(cx) { Poll::Pending => match self.fn2_fut.as_mut().unwrap().poll(cx) { Poll::Pending => Poll::Pending, Poll::Ready(x) => { // call the closure again before returning self.fn2_fut = (self.fn2)(); Poll::Ready(x); } } Poll::Ready(x) => { // call the closure again before returning self.fn1_fut = (self.fn1)(); Poll::Ready(x); } } } }).await } } ``` ### Why are pinned fields needed? It seems like we might be able to rely on `Unpin` to handle projection. If we did this, we would say that projecting through a pinned reference always yields a pinned reference, but if the target is `Unpin` then we could convert the reference to an unpinned one using `DerefMut`. Unfortunately, this does not work because with `Pin`, immovability is a temporal property. For example, we might have a structure like this: ```rust enum MaybeDone<F: Future> { Incomplete(F), Complete(Option<F::Output>), } impl<F: Future> MaybeDone { fn get_result(&mut self) -> F::Output { let Complete(mut val) = self else { panic!("not complete") } val.take().unwrap() } } ``` Here `F::Output` might be `!Unpin`, but we would still be allowed to move out of it in `get_result` because it hasn't been pinned yet. ## `Move` trait Another potential option is to add a new `Move` trait that is added by default to all bounds but can be opted out of with `?Move`, like `Sized`. If this were 2014 or even 2015, we could probably add `Move` and end up with a nice design. What's less clear is whether it's possible, and what it would take to adopt `Move` now that it's 2024. Let's explore. First, let's see what our running example would look like: ```rust struct Interleave<Fn1, Fn2, T> where Fn1: async Fn() -> T, Fn2: async Fn() -> T, { _phantom: PhantomData<fn() -> T>, fn1: Fn1, fn2: Fn2, fn1_fut: unsafe<'u> Option<Fn1::CallRefFuture<'u>>, fn2_fut: unsafe<'u> Option<Fn2::CallRefFuture<'u>>, } impl<Fn1, Fn2, T> Interleave<Fn1, Fn2, T> { fn new(fn1: Fn1, fn2: Fn2) -> Self { Interleave { _phantom: PhantomData, fn1, fn2, fn1_fut: None, fn2_fut: None, } } /// Calls both fn1 and fn2 and advances their future /// /// Returns the value from the first one to finish. /// The next call will continue to poll the future that /// did not finish. async fn call_both(&mut self) -> T { // ^^^^^^^^^^^^^ needs to be pinned because it holds incomplete futures across invocations if self.fn1_fut.is_none() { self.fn1_fut.emplace((self.fn1)().into_future()); } if self.fn2_fut.is_none() { self.fn2_fut.emplace((self.fn2)().into_future()); } poll_fn(|cx| { // unsafe still needed for unsafe lifetime binders unsafe { match self.fn1_fut.as_mut().unwrap().poll(cx) { Poll::Pending => match self.fn2_fut.as_mut().unwrap().poll(cx) { Poll::Pending => Poll::Pending, Poll::Ready(x) => { // call the closure again before returning self.fn2_fut.emplace((self.fn2)().into_future()); Poll::Ready(x); } } Poll::Ready(x) => { // call the closure again before returning self.fn1_fut.emplace((self.fn1)().in_future()); Poll::Ready(x); } } } }).await } } ``` It's similar to before. The `pin` keywords have been removed since movability is carried in the types. To assign an immovable value to a field, however, we need to use some kind of emplacement feature. This example uses a plausible version of it, but we haven't designed that feature yet. ### Semantics `Move` would be a marker trait that indicates you are allowed to do move operations on a type. These include: - Passing as a by-value argument to a function. - Returning from a function. - Assigning out of a place, e.g. ```rust let x: T = foo(); let y = x; // requires T: Move ``` - This includes moving out of a mutable reference as with `swap` or `take`. ### Comparison to `Pin`/`&pin mut` #### Initialization These restrictions make the type somewhat hard to work with without additional language features. For example, the usual `MyType::new()` pattern does not work for `?Move` types because you cannot return a value without a `Move` impl. Instead, with the features available today you would have to do something like: ```rust let x = MaybeUninit::uninitialized(); unsafe { let x = x.as_mut_ptr(); addr_of_mut!((*x).foo).write(42); ... } let x = x.assume_init_mut(); // can't use assume_init because that would move out of x. ``` #### Pinned and unpinned phases `Pin` allows types to exist in either a pinned state or an unpinned state, and it is common for values to exist in both. For example, someone might compose a number of futures together and move them freely in the process. However, in order to call `poll` the future must be pinned and it remains pinned from that point. With `Move`, a type is either *always movable* or *always immovable*. This means the pinned and unpinned phases have to be represented by different types. One natural way to make this split with futures is to take advantage of `IntoFuture`. A type that implements `IntoFuture + Move` could be used to represent the unpinned phase of the life cycle, and then when you call `.into_future()`, the result is a `?Move` type that implements `Future`. (Of course, this is not possible without additional language features because `into_future()` cannot return a `?Move` value.) #### Projection Pin projection is an annoyance with `Pin`. It's less present with `&pin mut T`, but it's still there in the form of needing to annotate fields for projection purposes. On the other hand, with `Move`, there is nothing special about pinned projections. You just take references to fields, and if the type of those fields is `?Move` then you can't move out of those references. #### Drop With `&pin mut T`, we require a different version of the drop trait if `T` has fields to which we can safely pin project. With `Move`, we would expect that we do not need to do anything special. If the value being dropped has `?Move` fields, then you will not be able to move out of them in `drop`. Unfortunately, some of these fields might be both `Move` and `?Unpin` meaning they might have been pinned and then we are not allowed to move out of them. [See below](#Is-Move-equivalent-to-Unpin). #### Backwards Compatibility `Pin` exists now, so there's nothing to be backwards compatible with. `&pin mut T` is sugar for `Pin<&mut T>` with extra language support, so the backwards compatibility story is good there too. The backwards compatibility story for `Move` is essentially impossible. The reason is that there exist values that would be both `Move` and `?Unpin`, and in fact these are quite common (the future returned by any `async fn` is in this category today). See [Interop with `Pin`](#Interop-with-Pin) for more details. Even if we could solve this, another significant backwards compatibility challenge is with associated type bounds. For example, we would want to add a `?Move` bound to `Deref::Target`. Doing so would break code like this: ```rust fn take<T>(x: &mut T) -> T::Target where T: Deref, T::Target: Default, // need to add `+ Move` here. { let mut temp = Default::default(); std::mem::swap(x, &mut temp); temp } ``` It might be possible to add some kind of edition-dependent bound mechanism to make this work. It's also quite likely that `Move` would require building a new version of `IntoFuture` and `Future` that work with the move trait, as well as adapters from the existing `Pin`-based `Future` trait. ### Interop with `Pin` Given that `Pin` already exists in the language, we want `Move`-based code to work as seamlessly as possible with existing `Pin`-based code. #### Is `Move` equivalent to `Unpin`? No. The reason is that `Unpin` is concerned with what happens after a pinned reference to a value has been created, while `Move` applies to the whole lifetime of a value. There are some relationships though, including: - A type that is `?Unpin` may still be `Move`. In these cases, we must rely on `Pin` to represent the pinned typestate. Values in this category occur, for example, when doing the pattern where you compose futures together, then pin them before polling. Today any `async { ... }` has a type that is `Move + ?Unpin`. - A type that is `?Move` may be `Unpin`. `Unpin` ultimately means it's safe to get a `&mut T` to the contents of a `Pin<&mut T>`. If `T` is `?Move`, then you can't move out of a `&mut T`. - `Move` does *not* imply `Unpin`. See the first bullet point for a counterexample. - A type that is `!Move` is `Unpin`. We could probably write `impl<T: !Move> Unpin for T`, although this would surely conflict with lots of other impls. #### `Pin<&mut T>` and `Move` First, let's consider if we have a `Pin<&mut T>`, what additional things would `Move` enable? It's tempting to try and relax `Pin::get_mut` to something like: ```rust fn get_mut(self) -> &mut T where T: ?Move, ``` After all, if `T` is not `Move`, then we can't move out of the reference at all, regardless of whether it's `Unpin`, and if it is `Move` then we should be able to move it. This doesn't work though, because `T` might be `Move` but not `Unpin`, like many futures today. Furthermore, if we could do this, that would enable code like this: ```rust fn pin_map<T: ?Move, U>(pin: Pin<&mut T>, f: Fn(&mut T) -> U) -> U { let inner = pin.get_mut(); // hypothetically safe because we don't have the ability to move `T` f(inner) } let x = pin!(some_unpin_future()); let oh_no = pin_map(x, |f| { let mut temp = Default::default(); // pretend our future has a Default impl for some reason // this closure can move because it has more knowledge. // it knows the concrete type and therefore that it is Move, // even though pin_map does not know this. swap(f, &mut temp); }); ``` The best we could do is probably: ```rust fn get_mut(self) -> &mut T where T: Unpin ⋁ !Move, ``` #### `Move` into `Pin` Does `Move` let us work with `Pin` a little easier, at least? Let's imagine when `Move` would let us coerce `&mut T` into `Pin<&mut T>`. If we know `T: !Move`, we can freely convert between `&mut T` and `Pin<&mut T>`. Otherwise, it doesn't seem like there's much more we could do. ## Alternate Timelines To help guide what we should do going forward, this section considers a few hypothetical situations and what we might do in those. The goal is to suss out to what extent these features are independent and have value on their own. ### `Move` was added pre-1.0 `Move` is challenging to add to Rust now, and doing so will likely require the ability to make new categories of changes across an edition. Before Rust 1.0, we could have broken backwards compatibility and added `Move`. Suppose we had done that. Would there be any reason to add `Pin` or `&pin mut T` references now? If the answer is yes, this suggest that `Pin` is not an inherently poorer solution to immovable values that we only adopted because it was backwards compatible. Instead, we would conclude that `Pin` had value *on its own*. If `Pin` has inherent value, independent of `Move`, then it stands to reason that improving the ergonomics of `Pin` would be a good thing too. I don't immediately know what this value would be, but given the existence or desire for features like `&raw T`, `UnsafeCell`, `NonNull<T>`, etc., it would not be surprising to find there are cases where lower level, dynamic control over immovability is useful. ### We Adopt and Migrate to `Move` The questions here are essentially the same as the previous scenario, but the perspective is slightly different. Let's assume we're were able to solve the backwards compatibility and migration challenges with `Move` and so we adopted it and declare `Move` to be the new way to talk about immovable types. The question is then, what to do with `Pin`, given that it is already part of the language? One option is we leave it in its current state and encourage everyone to migrate their `Pin`-based code to `Move` as soon as possible. Can we do that for all code though? Will `Pin`-based code continue to be maintained? If we cannot migrate all code, or there's a good reason to continue to maintain `Pin`-based code, then that suggest that `Pin` has inherent value, independent of `Move`. In that case, we should make the lives of maintainers of `Pin`-based code by making `Pin` easier to work with. ### If we improve `Pin`, do we need `Move`? In the two scenarios we've seen so far, it seems plausible that if we had `Move` there might also be a reason to have `Pin` for advanced use cases. Let's say we improve `Pin`'s ergonomics to the point where pinning is roughly the same level of difficulty as mutable references (i.e. `&mut T`). Would it make sense to add `Move` at that point? I think this needs more exploration. Here are some possible improvements though: - `Move` makes it easier to write code that doesn't need to move a value to be generic over movability. With `Pin` you'd have to write two versions of a function, one that takes a `&mut T` and another that takes a `&pin mut T`. ## Some other questions This section is a collection of a few questions to explore in this space. - Should implementing `Unpin` be unsafe? ### In-place construction ```rust // Can we make this legal? let x = Foo { bar: 42, baz: &x.bar, }; ``` ## References - https://blog.yoshuawuyts.com/safe-pin-projections-through-view-types - https://theincredibleholk.org/blog/2024/07/15/two-ways-not-to-move/ - https://without.boats/blog/pinned-places/ --- # Discussion ## Attendance - People: Josh, TC, Felix, Tyler, Eric Holk, Xiang, Urgau, Yosh ## Meeting roles - Minutes, driver: TC ## `?Move` does not imply `Pin` eholk: Prefilling a point raised in review: > A type that is `?Move` may be `Unpin`. > > Unpin ultimately means it's safe to get a `&mut T` to the contents of a `Pin<&mut T>`. If `T` is `?Move`, then you can't move out of a `&mut T`, which means there's not problem with TC: Analyzing the 2nd bullet point (quoted above), if we have: ```rust fn f<T: ?Move>(x: &mut T) -> Pin<&mut T> { Pin::new(x) } ``` ...that would definitely be wrong. This shows we can't treat a `?Move` type parameter as `Unpin`. (Discussion about modeling in terms of pinned places and temporalness.) TC: ```rust! let mut obj = SomeType::new(); // <-- Not a pinned place yet. frob(&mut obj); // <-- Relies on it not being a pinned place. let obj: Pin<&mut SomeType> = unsafe { Pin::new_unchecked(&mut obj) }; // <-- Now that first place is pinned. ``` Yosh: Example of when values can be moved after construction: ```rust let a = async {}; // constructed in-place let b = async {}; // constructed in-place let (a, b) = join(a, b).await; // moved into the `join` operation ``` ## `Move + ?Unpin` outside of async? Josh: The discussion of `Move` observes that it can't fully replace use of `Pin` because it doesn't handle types that are `Move + ?Unpin`, which come up in async. Do such types come up in non-async usage of address-sensitive types, such as intrusive lists and other uses in RfL? Asking because if they *don't* then we may want to consider if we should solve async with one mechanism and other address-sensitive types with another mechanism. However, if such cases *do* arise in non-async address-sensitive types as well, and there isn't a clear story for how to handle those cases with `Move`, that seems like more of a nail in the coffin of `Move`. We need some reasonable way to support address-sensitive types. We need a solution for async. It's not obvious that both of those need to, or should, use the same mechanism. We could, in particular, decide that address-sensitive types should use a different mechanism, and that async should avoid exposing `Pin` much more wherever possible, the combination of which would then mean that we shouldn't spend language budget on making `Pin` more ergonomic (and should instead spend that language budget on making async more ergonomic). Yosh: [Post on how `Move` would work for this](https://blog.yoshuawuyts.com/self-referential-types/), as well as how it generalizes to arbitrary self-referential types. TC: There almost seems an orthogonal axis about how much we push people into an `IntoFuture`-like pattern ("betting on `IntoFuture`"). If that pattern were used consistently, which would be required for `Move` anyway, then the ergonomics of `Pin` might be similar to the ergonomics of `Move`. tmandry: The biggest risk I see in Yosh's post is the reliance on emplacement. But there are also similar ergonomic hurdles to that design that might be on the same scale as with `Pin`. ## Compatibility of `Pin` with existing APIs yosh: the doc makes the following statement about backwards-compatibility of both `Pin` and `Move` (emphasis mine): > **Pin exists now, so there's nothing to be backwards compatible with**. &pin mut T is sugar for `Pin<&mut T>` with extra language support, so the backwards compatibility story is good there too. > > **The backwards compatibility story for Move is essentially impossible**. The reason is that there exist values that would be both Move and ?Unpin, and in fact these are quite common (the future returned by any async fn is in this category today). See Interop with Pin for more details. This only considers the narrow scope for which `Pin` is being used today. It does not consider the broader uses for which `Pin` isn't a good fit because it's incompatible. Consider for example the `std::io::Read` trait. Making that work with `Pin` requires minting an entirely new trait: ```rust // This is the trait we have in the stdlib today trait Read { pub fn read(&mut self, buf: &mut [u8]) -> io::Result<u8> { .. } // ^ self is not pinned } // This is the trait we'd need to add to make it possible // for `Read` to work with self-references trait PinnedRead { pub fn read(self: Pin<&mut self>, buf: &mut [u8]) io::Result<u8> { .. } // ^ self is pinned } ``` We also can't just `impl Read for Pin<&mut impl Read>` because: ```rust struct Foo {} impl Read for std::pin::Pin<&mut Foo> { fn read(&mut self, _: &mut [u8]) -> io::Result<usize> { // ^ `&mut Pin<&mut Self>`, should be `Pin<&mut Self>` } } ``` This is again a polymorphism problem, but not one we can solve with e.g. effect generics. In contrast `Move` would be more compatible with existing traits, because it is an auto-trait. That would make it have the same behavior as e.g. `Send` - which we can improve as a class. ## Backward incompatibility tmandry: We should talk about the actual example of backward compatibility. E.g.: ```rust fn take<T>(x: &mut T) -> T::Target where T: Deref, T::Target: Default, // need to add `+ Move` here. { let mut temp = Default::default(); std::mem::swap(x, &mut temp); temp } ``` Or, e.g.: ```rust pub trait Deref { type Target: ?Sized; // We'd want + ?Move here, but that's breaking. fn deref(&self) -> &Self::Target; } ``` Josh: We could have a different trait that works with `?Move`, for instance, and use that where feasible. It'd make `?Move` types less conveniently usable at first, but `Pin` is already substantially less usable in many places in the language. tmandry: We could have a trait modifier, `?Move Deref`. TC: This gets back to `Move` having similar ergonomics problems to `Pin`. We've talked about `pin` trait modifiers too. Or, e.g.: ```rust! pub trait FnOnce<Args> where Args: Tuple, { type Output; // We'd want + ?Move here (with emplacement), but that's breaking. extern "rust-call" fn call_once(self, args: Args) -> Self::Output; } ``` eholk: I don't actually think we want `+ ?Move` on `FnOnce::Output` ## Drop trait compatibility and `Drop::drop` signature > different version of the drop trait Josh: As I understand it, users aren't actually allowed to call `Drop::drop` directly. So we *could* make `Drop` special such that implementing it on a type may require a different signature for `drop`. We may or may not want to, but I *think* we can without breaking backwards compatibility. tmandry: Yes, I think we can. That's exactly what is proposed in a recent proposal to make pin more ergonomic: If you have `pin` fields, you must take a `&pin mut self` your `Drop` impl. ## Should movability really be a property of types? tmandry: I've noticed an assumption that it would be more natural to express movability as a property of a type, but I'm feeling less convinced of that now. The fact that it requires emplacement is a sign of how awkward it can be to work with such types. The big question in my mind is whether we should express movability in terms of a type or a value (or place) containing that type. There are at least two reasons why I think we might want this as a property of a place: - Types which are "temporally immovable", like futures and coroutines - Use cases for pinned references to "regular" types One reason I think we might want it as a property of a type: - Built-in self-referential types ## Pin is not part of the language yosh: The doc makes the following statement > The question is then, what to do with Pin, given that it is already part of the language? This is a fine line to tread, but if we're being specific: `Pin` is currently not a language-level item. It definitely borders on being a language item given its safety invariants and uses in `async` - but there is nothing inherent to `Pin` that makes it special. The closest we get to that is that `Unpin` is an auto-trait, which cannot be defined outside of the stdlib. The only part of Rust where we use `Pin` today in the stdlib is in the `Future` trait - which means that if we wanted to fully deprecate `Pin` that would intersects with stable Rust. It seems important to call this out, because mechanically it means we can think about it like an implementation detail of `Future` - than as something more inherent to the language like e.g. `Deref`. ## Does `Move + !Unpin` make `Move` impossible? eholk: I wrote this in the doc: > The backwards compatibility story for `Move` is essentially impossible. The reason is that there exist values that would be both `Move` and `?Unpin`, and in fact these are quite common (the future returned by any `async fn` is in this category today). See [Interop with `Pin`](#Interop-with-Pin) for more details. Upon further reflection, I'm not sure this is true. Right now everything is considered movable and it's up to `Pin` to prevent immovable values from being moved. Thus, if we add `Move`, `Pin` will still need to uphold the expectations of code relying on the `Move` trait. The part that is true is that `Pin` and `Move` will always have an impedence mismatch. ## Making futures *not* address-sensitive? Josh: This is likely to be a question with a well-explored answer, and I'm not expecting to blaze new ground here, just understand the well-explored answer: what is the underlying reason why we cannot, or do not want to, make futures able to be self-referential without being address-sensitive, such as via base-relative addressing? Yosh: because you can have segmented address spaces using the heap - relative pointers when the heap gets involved seem like they wouldn't work out well. tmandry: Dropping this because I have to leave – I remember discussion of this exact point here.. https://without.boats/blog/pin/ Yosh: ```rust let y = 42; let x = if foo() { &static_value } else { &y }; ``` Yosh: The `ptr` family of APIs also run into some pretty gnarly problems. Any form of offset-based pointers requires that it's updated when moved. However, it's legal today to use `ptr::*` to do things with types - and so we'd need to somehow encode additional rules onto them. Yosh: Adding additional requirements to safely use `ptr::*` is backwards-incompatible, which means that e.g. `Vec` or other data structures which depend on these operations would run into some pretty gnarly issues. Yosh: So even beyond whether offset-based pointers are possible, they would inherit a lot of the compat problems immovable systems have. Which unfortunately means we can't just treat relative pointers as self-contained, but they need to communicate externally somehow that internally they're relative. (The meeting ended here.)

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully