[ISSUE] Trouble expressing lifetime-coupled return types with fn(&[u8]) -> T in generic context

I've labeled this as an issue since I'm pretty sure there isn't a mistake on my end, though this problem might be known already, and worked on, by the Rust team. In any case, the compiler doesn't give me enough descriptive hints to go on.
I've simplified my code as much as I can. Here's the scenario:

A Converter is a function which takes a slice of bytes and returns a value of a type that is to be specified later, but it must implement Debug. Aside from that, I want to express that this return value must also be bound by the same lifetime as the input. Something along the lines of this:

type Converter<'a, T: 'a + Debug> = fn(&'a [u8]) -> T;

This compiles, but when I try to use it like below, it doesn't.

A MAP is a closure which returns a corresponding Converter based on a given Locale, or None if it does not have one.

I've attempted:

pub fn formatWithConverter<'strings, SHOW, MAP>(
    unconvertedStrings : &'strings Vec<Vec<u8>>,
    fmt                : &mut fmt::Formatter<'_>,
    converter          : MAP,
) -> fmt::Result
where
    SHOW : Debug,
    MAP  : Fn(Locale) -> Option<for <'x> Converter<'x, SHOW>>,
{
    // print the strings that can be converted
    ...
}

but this does not compile, which makes sense, since there is nothing to couple SHOW to 'x. However:

pub fn formatWithConverter<'strings, SHOW, MAP>(
    unconvertedStrings : &'strings Vec<Vec<u8>>,
    fmt                : &mut fmt::Formatter<'_>,
    converter          : MAP,
) -> fmt::Result
where
    SHOW : 'strings + Debug,
    MAP  : Fn(Locale) -> Option<Converter<'strings, SHOW>>,
{
    ...
}

does not compile once I actually call formatWithConverter.
Simply ignoring the lifetimes associated with the Converter alltogether, like so:

pub fn formatWithConverter<'strings, SHOW, MAP>(
    unconvertedStrings : &'strings Vec<Vec<u8>>,
    fmt                : &mut fmt::Formatter<'_>,
    converter          : MAP,
) -> fmt::Result
where
    SHOW : Debug,
    MAP  : Fn(Locale) -> Option<fn(&[byte])->SHOW>,
{
    ...
}

does not compile once I actually use a fn(&'a [byte])->&'a str as a Converter, but it does compile for fn(&'a [byte]) -> String.

this is not directly expressible in rust type system, what you need is higher kinded generic type, but what you did is an "outlives" bounds.

higher kinded type may look like this (in hypothetical pseudo syntax, NOT real rust syntax):

type Converter<'a, T<'_>: Debug> = fn(&'a [u8])-> T<'a>;

however, in some scenarios, this can be emulated, e.g. with GAT:

trait DebugWithLifetime {
    type Of<'a>: Debug;
}
type Converter<'a, T: DebugWithLifetime> = fn(&'a [u8])->T::Of<'a>;

then it can be used like this:

pub fn formatWithConverter<'strings, SHOW, MAP>(
    unconvertedStrings : &'strings Vec<Vec<u8>>,
    fmt                : &mut fmt::Formatter<'_>,
    converter          : MAP,
) -> fmt::Result
where
    SHOW : DebugWithLifetime,
    MAP  : Fn(Locale) -> Option<Converter<'strings, SHOW>>,
{
    todo!()
}

note, I don't know how your code is inteded to work, the snippet above is just syntactically adapted from your example to make it compile, but you probably will encounter other lifetime related issues.

a function pointer type with early bound lifetime is probably NOT what you want, you probably want something like this I think:

type Converter<T: DebugWithLifetime> = for<'a> fn(&'a [u8]) -> T::Of<'a>;

also, there are alternative ways to emulate higher kinded types than a GAT, see e.g.:

3 Likes

For the specific case of functions that may capture borrows, another workaround is to "hide" the return type in an associated type so it need not be named in bounds:

pub trait ConverterFn<'a>: Fn(&'a [u8]) -> Self::Out {
    type Out: Debug;
}

impl<'a, Out: Debug, F: ?Sized + Fn(&'a [u8]) -> Out> ConverterFn<'a> for F {
    type Out = Out;
}

pub fn formatWithConverter<'strings, MAP, CF>(
    unconvertedStrings: &'strings Vec<Vec<u8>>,
    fmt: &mut fmt::Formatter<'_>,
    converter: MAP,
) -> fmt::Result
where
    MAP: Fn(Locale) -> Option<CF>,
    CF: ConverterFn<'strings>,
    // If you need to pass in borrows of locals:
    // CF: for<'a> ConverterFn<'a>,
{
    todo!()
}

This should work fine with function items like defaultMap, but will interfere with closure inference in annoying ways (and the workarounds are unergonomic). On the other hand, since you were using fn(..) pointers originally, this may not be a concern.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.