Can I get rid of this Box?

I have this constraint:

F: for<'a> Fn(&'a String) -> Box<dyn View + 'a>

What I would like is something like:

F: for<'a> Fn(&'a String) -> (impl View + 'a)

(which doesn't work because opaque types can only be used in certain places). Is there another way to achieve that?

I'm trying to express that the returned View cannot outlive the string passed in.

More context: can we nest states using only references? · Issue #26 · audulus/rui · GitHub

(Playground)

thanks!

Can you provide a playground.. I'm confused with the for<'a> part.. I tried to create a playground to test but it's too peculiar.

1 Like
trait View {
    
}

fn foo<'a, T>(t: T) 
where
T: Fn(&'a String) -> Box<dyn View + 'a>
//T: Fn(&'a String) -> (impl View + 'a) 
{
    let s:String = "bob".into();
    let _r = t(&s);
}

is as far as I got

You can do something like this:

trait ProduceOneView<'a> {
    type View: 'a + View;
    fn produce(&self, s: &'a String) -> Self::View;
}

trait ProduceView: for<'a> ProduceOneView<'a> {}
impl<T> ProduceView for T where T: for<'a> ProduceOneView<'a> {}

impl<'a, F, R> ProduceOneView<'a> for F
where
    F: Fn(&'a String) -> R,
    R: 'a + View,
{
    type View = R;
    fn produce(&self, s: &'a String) -> Self::View {
        self(s)
    }
}

And then elsewhere

impl<F: ProduceView> View for State<F> {
    fn draw(&self) {
        let s = "hello world".to_string();
        self.f.produce(&s).draw();
    }

    fn process(&self, _data: &String) -> Option<String> {
        let s = "hello world".to_string();
        let v = self.f.produce(&s);
        v.process(&s)
    }
}

The key to the first part is that you can only name the result type R in the context of a single lifetime. (If you name R in for<'any> Fn(&'any String) -> R, R cannot capture a borrow involving 'any.)

Playground.

4 Likes
trait View {
    
}

fn foo<'a, T, U>(t: T) 
where
T: Fn(&'a String) -> U,
U: View + 'a
//T: Fn(&'a String) -> (impl View + 'a) 
{
   unimplemented!()
}

Compiles but loses the Higher Rank Trait Bound, though it may still work depending on what you want to do.

To highlight what I meant by naming R (or here U), a higher ranked version of this on nightly is:

#![feature(unboxed_closures)]
trait View {}

fn foo<'a, T>(t: T) 
where
    T: for<'any> Fn<(&'any String,)>,
    for<'any> <T as FnOnce<(&'any String,)>>::Output: View + 'any,
{
   unimplemented!()
}

The helper trait approach I presented above is necessary on stable because the Fn(...) -> ... sugar does not allow eliding the return type like the unboxed_closures syntax does, so you cannot properly write the top bound on T. Or alternatively because you can't write something like for<'any, U: View + 'any> Fn(&'any String) -> U or -> impl View + 'any, etc.

Playground for the State example using nightly instead of the helper trait.

2 Likes

I added a playground to the original post.

Your state_two assumes the function will return an Empty.

fn state_two<'b, F: 'b + Fn(&String) -> Empty>(f: F) -> impl View + 'b {
    State { f }
}

original state can return a View:

fn state<'b, F: 'b + for<'a> Fn(&'a String) -> Box<dyn View + 'a>>(f: F) -> Box<dyn View + 'b> {
    Box::new(State { f })
}

Any idea how to fix that?

btw, this is my little test:

#[test]
    fn test_state_nested() {
        state(move |x| {
            state(move |y| {
                println!("{} {}", x, y);
                Empty {}
            })
        })
        .draw();
    }

Thanks for your help!!

Here's as far as I'm getting: Rust Playground

In the playground, State<F> will implement View for functions that return anything that implements View (so long as the functions work with all lifetimes) -- that is how I got rid of the box, and that's why it works with Empty. But it also means that if you want the option of keeping the box, Box<dyn View + '_> needs to implement View in order for the original state to work.

That's right, a `Box<dyn Trait>` does not automatically implement `Trait`.

This is often a surprise. You can still call Trait methods on a Box<dyn Trait> parameter due to deref conversion, just like you can call str's Display implementation on a String -- the language lets you utilize all traits of concrete types. But here we're in a generic context, so we need to implement View for Box<dyn View + '_> so that Box<dyn View + '_> will meet the trait bound on the implementation.

You can't just keep your original implementation and the one I've added, as the compiler will think there's the potential for overlapping implementations (and there is on nightly, where one could implement the Fn traits in multiple interesting ways). At least, I think that's a way to get actual overlap, I didn't try it out just now.


Note that state does still work in the playground, because I added the implementation:

// I also added this
impl View for Box<dyn View + '_> {
    fn draw(&self) { (&**self).draw() }
    fn process(&self, data: &String) -> Option<String> { (&**self).process(data) }
}

(You might want similar for Box<dyn View + Send + Sync + '_> or others, depending on your use cases.)

I'm a little confused, sorry. I don't want the option of keeping the box. Just need a version of that state function that will work for test_state_nested.

OK, I see your playground now and I'll take a look.

1 Like

There's something going on with the captures between the outer closure and the inner closure. Using type erasure and the dyn lifetime solves it, but I have yet to find the right nobs or nudges to get the outer closure to be properly higher-ranked. Perhaps someone else can.

This works:

fn main() {
    fn st(x: &String) -> impl View + '_ {
        state_two(move |y: &String| {
            println!("{} {}", x, y);
            Empty {}
        })
    }

    st(&"hi".into()).draw();
    state_two(st).draw();
}

And I think it's a case of not connecting the lifetimes, but those workarounds don't apply directly due to the unnameable types involved (the opaque impl View or the closure type in State<_>).

If I'm correct about that, RFC 3216 would provide a way to override the inference.

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.