Lately I've been talking to a few people about whether it might be possible to replace the Pin wrapper in Rust with a new Move trait. Pin is one of those things that, while amazing that Rust can express the concept and semantics using just a library type, is not a lot of fun to use. This creates friction in building out Rust's async features because currently doing so increases the opportunities for users to be exposed to Pin. Maybe we can just get rid of Pin entirely?

One possible way to do this is with a trait called Move. This would be a trait that most types implement and indicates that you have the ability to perform a move operation on a value of that type. It's sort of like the Send trait, where when a type implements Send it means values of those types can be sent to another thread.

This trait was considered somewhere in the Rust 2015 and 2018 timeframe but was rejected in favor of Pin instead. The main reason is that Pin could be added in a way that meets Rust's stability guarantees. Indeed, how to add Move now is a huge problem that we're going to entirely ignore in this post.

I recently discovered that the version of Move that was originally considered worked quite different from how I would have expected. As a result, we accidentally have two Move designs. In this post, I want to briefly describe each one and then discuss some of the tradeoffs they make.

The Original Move🔗

Originally Move was a marker trait that meant values of a type that implements Move can be moved even after taking that value's address. That definition applies to basically all types in Rust now, so the more interesting thing to think about is what can you do with a value of a type that does not implement Move?

You end up with two phases to a value's lifetime. There's some period in which you can move a value around, but then when you take its address the value can no longer be moved.

Futures in Rust work this way now. When you create a future, you can move it around, send it to another thread (as long as it implements Send), put it in a data structure, etc. But in order to call poll, you first have to pin the future, and once you do that no longer move the future.

If we had the Move trait instead, the Future trait would look like this:

trait Future {
    type Output;
    fn poll(&mut self, cx: &mut Context) -> Poll<Self::Output>;
}

Notice how Pin has just gone away?

When you call the poll method, doing something like f.poll(cx), Rust automatically borrows (i.e. takes the address of) f for you and passes that as the self argument to poll. If the future does not implement Move, the compiler would notice that you've taken the address of f and prevent you from moving f after that point.

When I first heard this I thought the analysis to make this work would surely be infeasible. It turns out it's actually not too hard and can be done entirely with local reasoning. For example, if you've received a reference to some !Move type, you know it's pinned because someone had to take the address to give you a reference. So you basically just need to track where the first place you borrow some owned type happens. This lines up neatly with the kind of analysis the compiler is already doing in borrow checking.

So why don't we have this version of Move?🔗

The main reason is that it's not backwards compatible. Let's look at one of many examples of the way it's not backwards compatible. Functions in Rust implement one or more of the various Fn* traits. Each of these have an Output parameter that represents the return value. For !Move types, we want to be able to return them from a function, such as to make a constructor function called new. So this means we need to add ?Move as a bound to the Output parameter of the Fn* traits. But now, any type you want to have the existing behavior, where return values are movable, you'd have to add a Output: Move bound to your type signature.

This version of Move also struggles with projections.1 Consider the following code:

struct Slot<T: ?Move> {
    slot: Option<T>
}

impl<T: ?Move> Slot {
    fn fill_slot(&mut self, value: T) {
        self.slot = Some(value);
    }

    fn take(&mut self) -> T {
        self.slot.take().unwrap()
    }
}

Is take legal? Strictly no, because to call take you've exposed the address of self, which technically means you've exposed the address of all the fields of self, meaning slot would no longer be moveable. This particular case happens to be safe since we can see that there's no way to actually observe the address of slot. But we've now made this an interprocedural analysis instead of a local analysis. Consider if we add the following method:

fn poll_inner(&mut self, cx: &mut Context) -> Option<Poll<T>>
where
    T: Future
{
    self.map(|f| f.poll(cx))
}

Now knowing whether take is safe depends on whether poll_inner has been called.

This particular case is probably not insurmountable. We'd probably just go with our initial conservative observation that take exposes the address of slot and therefore slot is no longer moveable. But this also greatly limits the flexibility of the Move trait.

The New Move🔗

When I first started thinking about a Move trait, I assumed its definition was that a value of a certain type could be moved if and only if the type implements Move.2 In particular, you'd need a Move implementation to do any of the following things:

  • Pass a value to a function
  • Return a value from a function3
  • Use mem::swap, mem::replace, mem::take, or similar functions

Many things about this version of the trait just work in surprisingly pleasing ways. For example, pin projection just turns into normal projection. You don't need any annotations to say which fields can be pinned or not because this information is carried by whether the type of the field implements Move. Similarly, there's no ambiguity in the Slot example before; in order to write take you must have a Move bound on T.

The downside is that this doesn't naturally support the patterns where a type may move around for a while until at some point it's pinned and then remains pinned.

Instead we have to rely on API design and use one type to encapsulate the unpinned phase of a value's lifetime, and then use another type to encapsulate the pinned phase. For Future, we already have this, with the IntoFuture trait being able to represent the unpinned phase and the Future trait representing the pinned phase.

How you convert from one to the other is hard though, because you can't return a !Move type from a function. In stable Rust today, you could work around this using MaybeUninit but long term we'd need to design some kind of placement new or emplace feature.

What about backwards compatibility? This formulation doesn't have the issue of needing to add ?Move to the Fn*::Output types. In fact, there aren't many places where you'd need to add the bound. The reason is that if you aren't moving something, you are already taking a reference to it. Reference types like &T and &mut T implement Move even if T does not. So the only place where it really makes sense to add a ?Move bound is if you are already taking a generic parameter by reference and don't need to move out of it.

The bigger backwards compatibility question is how you'd make Move interoperate well with the existing Pin wrapper and the functions that use it. I haven't thought much about this problem, so that will have to wait for another post.


1

Thanks to Boats for pointing this out to me.

2

This is also the definition that Yosh Wuyts was working from in his post Ergonomic Self-Referential Types for Rust. Also, thanks for Yosh for helping me work through the ideas in this section.

3

It's interesting that in this formulation we also cannot return a !Move type from a function, but in this case it's a feature rather than an insurmountable obstacle.