rust

Dealing with poor closure inference causing unbounded borrows


Note: In the middle of writing this question, I realized the specific example I give below doesn't actually work perfectly with the title, but I couldn't think of another way of putting it, and I think it is still a good description for my general issue, so I've left it as is for now.

I'm currently working on an internal component for a project of mine, and have run into a fairly opaque compiler error I've been trying to solve for the last day or so without much success. The original code was quite long, so I've spent a little while trying to create a minimal example that shows the issue. This example is quite divorced from my actual use-case, so please do not comment on its specific structure; it is meant to be purely illustrative.

My original code centered around iterators. Trait, GetAssoc, and Ref are meant to capture the essence of that API. Namely, they correspond roughly to Iterator, IntoIterator, and slice::Iter, respectively, while Struct is meant to represent something like a Vec. Try not to pay much attention to these; they exist purely to make the overall span of code being investigated smaller.

#![allow(unused)]

use std::marker::PhantomData;

trait Trait {} // Iterator

trait GetAssoc { // IntoIterator
    type Assoc: Trait;
    fn get(self) -> Self::Assoc;
}

struct Struct {} // Vec

struct Ref<'a> { _phantom: PhantomData<&'a ()> } // slice::Iter
impl<'a> Trait for Ref<'a> {}

impl<'a> GetAssoc for &'a Struct {
    type Assoc = Ref<'a>;
    fn get(self) -> Self::Assoc { Ref { _phantom: PhantomData } }
}
// *ACTUAL CODE STARTS BELOW HERE*
struct Wrapper<'a, T: 'a, F: Fn(&'a T) -> Ret, Ret: Trait> {
    value: T,
    map: F,
    _phantom: PhantomData<(&'a (), Ret)>
}

impl<'a, T: 'a, F: Fn(&'a T) -> Ret, Ret: Trait> Wrapper<'a, T, F, Ret> {
    fn transform(&'a self) -> Ret {
        (self.map)(&self.value)
    }
}

fn main() {
    let obj = Struct {};

    let wrapper = Wrapper { value: obj, map: |x| x.get(), _phantom: PhantomData };
    { wrapper.transform(); }
    let _a = wrapper;
}

Roughly: I am wrapping a value and a closure together (Wrapper), then invoking that closure in a method (Wrapper::transform). The example exactly as given here results in cannot move out of `wrapper` because it is borrowed on rustc 1.86, at let _a = wrapper; (this line exists just to demonstrate the issue). In earlier, more faithful versions of this example, I could replace the transform call with { (wrapper.map)(&wrapper.value); } (just expanding it out directly) to make this compile. However this unfortunately doesn't work here.

If you replace the closure passed to wrapper.map with a function fn map(x: &Struct) -> Ref { x.get() }, it works fine. If you try to replicate that signature on the closure (|x: &Struct| -> Ref { x.get() }), it fails (in some versions of the test this also worked). I cannot use impl Trait return types in the closure since it is currently not allowed, and using it in the function causes it to fail again.

My problem here is that while writing out the full map signature in a function here is trivial, it is anything but in my actual use-case. The real return type is a fairly large chain of iterator adaptors (some of which exist purely to work around other issues in the type-system), and I end up having to write it out in full, as attempting to leave any to be inferred runs into trait solver limitations (this example cannot reproduce that).

If I remove the explicit lifetimes on Wrapper and its impl, I run into

map: |x| x.get(),
      -- ^^^^^^^ returning this value requires that `'1` must outlive `'2`

Using the function map replacement then gives

{ wrapper.transform(); }
          ^^^^^^^^^ method not found in `Wrapper<Struct, fn(&Struct) -> Ref<'a> {map}, 

even if I explicitly annotate it, which feels a bit insane considering the impl has the exact same signature as the type itself. Expanding the call to transform gives this wonderful little error :)

expected struct `Ref<'_>`
   found struct `Ref<'_>`

There are other possible solutions to my original issue, like using a trait on the wrapped type instead of a closure (except for me this also runs into trait solver limitations, and is fairly un-ergonomic to use), or just throwing away the generics entirely and creating a type only for my specific case, but it just seems ridiculous that I can't get this to work given how simple I feel it should be.

Is there any way I can write either Wrapper or the map closure to make this compile without needing to write out the return types (if you can get an impl Trait to work that's fine) in full? I'd prefer to keep the closure and a completely inferred return type as the actual closure code is fairly small, but would accept needing to use functions. Moreover, is this somehow intended behavior, or just a limitation in rustc?

I am unwilling to accept a solution that uses unstable features (though of course you are welcome to mention them), unless it is evident that stabilization is immanent (ie could be reasonably expected by rustc 1.88-1.89).


Solution

  • Why your attempt doesn't work

    The basic problem here is that you're trying to do two different things with the 'a lifetime on Wrapper:

    (You have an 'a in both places: F: Fn(&'a T) -> Ret hardcodes 'a allowing Ret to also have it hardcoded; and fn transform(&'a self) has a &'a on the self parameter.)

    As such, you're attempting to call a method whose receiver type is &'a Wrapper<'a, _, _, _> – you have a reference to a type that lasts as long as the type itself does (because Wrapper has to live at least as long as 'a due to containing an &'a (), but 'a has to live at least as long as the referenced Wrapper so that the &'a Wrapper remains valid). In other words, this means that as long as your code contains a call to transform, Wrapper is permanently borrowed – the borrow can't possibly end because (by definition) it lasts as long as the object being borrowed does (they have the same lifetime!).

    Rust does allow you to create a permanent borrow, but once you do, you can never again create a conflicting borrow of the object, nor move it. So you're getting an error because you're attempting to move a permanently borrowed object. (It would be nice if Rust's error messages were clearer in this situation: this sort of thing is easy to do by mistake and it can be quite hard to work out what's going on.) The closure doesn't directly have anything to do with the issue; the type of transform is enough to cause the problem. (That said, the closure is indirectly causing the issue because you wouldn't have had to write the type of transform like that otherwise.)

    The basic issue

    The basic issue behind what's going on is that your code is attempting to implement F for one particular lifetime of the argument to the called closure – it has a hardcoded &'a T argument type, so it only allows the use of arguments with lifetime 'a, so the argument always has to have the same lifetime. That implies that any given Wrapper object is only usable once (which is why you ended up writing it with the permanent borrow – because, given the type of F, transform fundamentally can't be written to work more than once, the code naturally ended up being written in a state where each Wrapper could only be used once).

    Instead, F is, in this case, inherently a lifetime-generic function; you want it to be callable with a reference with any lifetime 'b, and to return a return value with the same lifetime 'b. Syntactically, that would look like this:

    F: for<'b> Fn(&'b T) -> Ret<'b>
    

    …which would be fine if Ret were a concrete type constructor. But it isn't – it's a type parameter of Wrapper, so you've basically ended up designing a generic wrapper type where one of its generic parameters is not a type, but rather a generic type constructor. These are normally known as higher-kinded types, and although they exist in some programming languages, they don't exist in Rust. (As seen here, there are situations where they're potentially useful; however, they can also be quite confusing to work with and would likely be difficult to implement in the type-checker. So it's unlikely they'll be implemented in Rust any time soon, and the Rust developers are more likely to work on alternatives than on implementing higher-kinded types directly.)

    Some possible solutions

    A type alias

    If you're OK with writing out the complicated type in question once and it's always the same, the simplest approach is to use a type alias:

    type Ret<'b> = Ref<'b>; // big complicated right hand side
    
    struct Wrapper<T, F: for<'b> Fn(&'b T) -> Ret<'b>> {
        value: T,
        map: F,
    }
    
    impl<T, F: for<'b> Fn(&'b T) -> Ret<'b>> Wrapper<T, F> {
        fn transform(&self) -> Ret {
            (self.map)(&self.value)
        }
    }
    
    fn main() {
        let obj = Struct {};
    
        let wrapper = Wrapper { value: obj, map: |x| x.get() };
        { wrapper.transform(); }
        let _a = wrapper;
    }
    

    This way, Ret<'b> is always the same type apart from the lifetime 'b, so Wrapper doesn't have to be generic over it – and the lifetime 'a has basically disappeared altogether, so you don't get any issues with Wrapper becoming permanently borrowed. (The lifetime that was 'a is still present in transform as an elided lifetime, with the signature being sugar for fn transform<'a>(&'a self) -> Ret<'a>, but is no longer in Wrapper.) This solution is easy to understand and works on stable Rust, but isn't quite as general as the original code. I would recommend using it if it works in your case, because it makes it clear what's happening to both the user and compiler.

    A generic associated type (Rust 1.65+)

    Generic associated types are a language feature which is able to replace most uses of higher-kinded types. However, it's generally not very usable, and generally works better in the internals of libraries than it does as something for an end-user to write directly (it exists primarily because some programs can't be written at all without it).

    It's not too bad to write Wrapper in terms of a generic associated type:

    trait FnReturningTrait<T> {
        type Ret<'b>: Trait where Self: 'b, T: 'b;
        fn call<'b>(&'b self, arg: &'b T) -> Self::Ret<'b>;
    }
    
    struct Wrapper<T, F: FnReturningTrait<T>> {
        value: T,
        map: F,
    }
    
    impl<T, F: FnReturningTrait<T>> Wrapper<T, F> {
        fn transform(&self) -> <F as FnReturningTrait<T>>::Ret<'_> {
            self.map.call(&self.value)
        }
    }
    

    The idea here is that F of Wrapper is a trait that supports the operation we want: taking a *'b T as argument, and returning a Trait whose lifetime depends on 'b as its return value. Implementing transform here is easy; the trait FnReturningTrait is unfortunately a bit complex, though, because there are a lot of lifetime annotations.

    The problem comes when it comes to actually using Wrapper, because closures don't implement FnReturningTrait and I couldn't find a way to blanket-implement it even using unstable features. So you have to define the closure by hand in main:

        struct GetAssocClosure;
        impl<T> FnReturningTrait<T> for GetAssocClosure
        where for<'b> &'b T: GetAssoc {
            type Ret<'b> = <&'b T as GetAssoc>::Assoc
                where Self: 'b, T: 'b;
            fn call<'b>(&'b self, arg: &'b T) -> Self::Ret<'b> {
                arg.get()
            }
        }
    
        let wrapper = Wrapper { value: obj, map: GetAssocClosure };
    

    This isn't actually difficult. But it's also very verbose and not very easy to read, because you can't use the normal closure syntax. As such, I'd reserve this technique for situations where there's no other way to implement what you want.

    Although the technique in the next section is generally more usable, this one works on the most versions of Rust (back to Rust 1.65) and is fully general.

    Return position impl Trait in trait (Rust 1.75+)

    The previous solution is somewhat unwieldy when it comes to defining the traits. When you don't need to be able to name the return type, it recently became possible to sugar the generic associated type into a return-position impl Trait. That makes it possible to write what is effectively the previous example, but in a somewhat less ugly way:

    trait FnReturningTrait<T> {
        fn call<'b>(&'b self, arg: &'b T) -> impl Trait;
    }
    
    struct Wrapper<T, F: FnReturningTrait<T>> {
        value: T,
        map: F,
    }
    
    impl<T, F: FnReturningTrait<T>> Wrapper<T, F> {
        fn transform(&self) -> impl Trait { // Rust 2024 edition
            self.map.call(&self.value)
        }
    }
    
    fn main() {
        let obj = Struct {};
    
        struct GetAssocClosure;
        impl<T> FnReturningTrait<T> for GetAssocClosure
        where for<'b> &'b T: GetAssoc {
            fn call<'b>(&'b self, arg: &'b T) -> impl Trait {
                arg.get()
            }
        }
    
        let wrapper = Wrapper { value: obj, map: GetAssocClosure };
        { wrapper.transform(); }
        let _a = wrapper;
    }
    

    The main difference is that because Ret is no longer named – it's just used as an opaque return value – if you're using Rust 2024 edition, an explicit listing of all the lifetime constraints on it is no longer needed.

    This technique can be made to work back as far as Rust 1.75 and in previous editions, but the syntax for declaring and defining transform varies:

    fn transform(&self) -> impl Trait; // Rust 2024 edition
    fn transform(&self) -> impl Trait + use<'_, F, T> // Rust 1.82+, any edition
    fn transform(&self) -> impl Trait + '_; // Rust 1.75+, somewhat hacky
    

    The first two possibilities say that the lifetime of the return value may be based on the lifetime of self, which is the correct behaviour for the function. This was made the default in Rust 2024 edition (and has always been the default for a return-position impl Trait in a trait, as opposed to an associated function like transform is).

    The third possibility says that the return value lasts at least as long as self, which isn't actually what you want, but is close enough to work in this case (it's known as the "outlives trick") – it works by interacting with the way the lifetime erasure algorithm in Rust 2021 and earlier chooses default lifetimes, but adds an extra constraint (which fortunately happens to be true in this case, but isn't always true in cases where multiple lifetimes are involved).

    The main disadvantage of this is that closures only implement Fn traits – they don't implement custom traits that use the impl Trait syntax – so you have to implement the closure by hand rather than using closure syntax.

    With unstable features

    I couldn't find an exact solution using unstable features, but I got very close using unboxed_closures:

    #![feature(unboxed_closures)]
    
    struct Wrapper<T, F> {
        value: T,
        map: F,
    }
    
    impl<T, F: for<'b> Fn<(&'b T,), Output: Trait>> Wrapper<T, F> {
        fn transform(&self) -> impl Trait {
            (self.map)(&self.value)
        }
    }
    
    fn main() {
        let obj = Struct {};
    
        fn get_assoc_closure<'b>(s: &'b Struct) -> impl Trait {
            s.get()
        }
        let wrapper = Wrapper { value: obj, map: get_assoc_closure };
        // let wrapper = Wrapper { value: obj, map: |x: &Struct| x.get() };
        { wrapper.transform(); }
        let _a = wrapper;
    }
    

    The idea is to simulate impl Trait in a Fn by placing a requirement on the output of the Fn, which is the behaviour you'd intuitively expect for impl Trait on the return position of a Fn. This actually does work in the case where the Fn is a function, and has its argument and return type listed. The unboxed_closures feature is required in order to be able to name the parts of the Fn trait indivdually (which isn't possible in stable Rust).

    Unfortunately, the closure version (with |x: &Struct| x.get()) doesn't work and I couldn't find a way to make it work – that looks like it may actually be a closure inference bug, given that it works when the closure is converted into a function.