Abstract return types (impl Trait) that return references

Let's say we want to return from a function a closure that...

  1. captures some owned value, e.g. PathBuf or String, and therefore is a move closure.
  2. accepts no parameters, i.e. the signature of this closure is ()->....
  3. can be called multiple times, i.e. it is Fn() -> ... closure.
  4. returns a shared reference to the captured value that does not outlive the closure and, consequently, the captured variable.

I don't think there's a way to do it using either stable or nightly rust.

I would like to be able to write something like this...

fn path_to_example_nifti() -> impl Fn() -> &str {
    let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    // ...
    move || path.to_str().unwrap()
}

I understand that it's a limitation of Fn* traits but I still would love to be able to do something like this.

Learn more about abstract return types:

2 Likes

I don't think this directly involves impl Trait, but just Fn traits (and what closures desugar to). It has been discussed a couple of times in the past in Baby Steps and briefly in Closures that can return references to captured variables. It can't be retrofitted in the Fn* traits, but it should be possible to make new traits with the property you want (and maybe make them supertraits of the current Fn* traits?). The hard part is probably how to handle lifetime inference such that this doesn't break existing code. For now you're probably better off with a separate trait.

3 Likes

A "how can I do this today" question is a better fit for the users forum. The internals forum is better fit for "how can we improve doing this in the future."

For the purpose of future extensions, it's theoretically possible that Fn and FnMut could be GAT-ified. However, since they also imply that a closure is also FnOnce, it's not really possible for them to be used directly; there's no way for FnOnce to return borrowed state. New traits are a possibility, but lifetime inference will still be a hard problem.

For right now, since your desired shape is fn(&self) -> &T, it's interesting to note that this is the shape Deref takes. You should be able to define a type along the lines of

struct Lend<T, F, O: ?Sized>(T, F)
where
    F: Fn(&T) -> &O;

impl<T, F, O: ?Sized> Deref for Lend<T, F, O>
where
    F: Fn(&T) -> &O,
{
    type Target = O;
    fn deref(&self) -> &O {
        (self.1)(&self.0)
    }
}

which allows you to make an adhoc Deref item able to return references to the first "state capture" part, using it like

pub fn path_to_example_nifti() -> impl Deref<Target = str> {
    let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    // ...
    Lend(path, move |path| path.to_str().unwrap())
}

and this spelling appears to work, but note that it's very easy when trying to do something like this to encounter confusing higher-ranked lifetime errors, because closures aren't great at for<'a> use cases (implicitly introduced here by lifetime elision).

(For example: it's possible to define Lend without the O type parameter or the F: Fn bound, which I did originally since leaving the struct minimally bound is typically preferable. However, while the Deref impl still works, attempting to construct one a) requires an extra type annotation on the closure parameter and b) causes a (higher-ranked) lifetime error, potentially due to the presence of the type annotation, unlike when the bound is present structurally. It's very rarely clear what the compiler wants in these cases, and it takes some educated guessing to get a shape the compiler is happier about.)

The maximally general version of this kind of self-borrowing pattern is yoke, but it's extremely overkill unless you're in the very specific case that it was designed to support (zero-copy deserialization of complex types). You know if you need it (and will probably end up wanting it before you actually need it).

5 Likes

Doesn't the lifetime of a function end when the function returns making the Fn() -> &str desire untenable. Perhaps a co-routine akin to await or a yield statement that pauses execution is what is desired.

In theory it's possible, because the signature of Fn() -> &T actually looks somewhat like fn call(&self) -> &T. It just also looks like fn call_once(self) -> &T, so it can't be retrofit to the existing Fn traits[1].

Instead, it'd require separate traits, e.g.

trait FnLendMut<Args> {
    type Output<'a> where Self: 'a;
    fn call_lend_mut(&mut self) -> Self::Output<'_>;
}

trait FnLend<Args>: FnLendMut<Args> {
    fn call_lend(&self) -> Self::Output<'_>;
}

which you could write today (but would be easier to write specialized for the desired signature instead of generic).


  1. :nerd_face::point_up_2: without some undesigned and highly theoretical type shenanigans such that FnMut: FnOnce iff the associated Output type is not dependent on the GAT input lifetime provided at invoke time. ↩ī¸Ž