Lifetime issues when using generics and boxed closures

I'm struggling to get a good abstraction for a problem of mine.
What I want:
Some type Condition that represents a tree of logic operators such as && and || where the leaves contain some closure that takes something of type T and returns a bool.
In my use case the T actually would be two types, called T1 and T2.
To start I implemented the Condition type without generic parameters and the closure type was Box<dyn Fn(&T1, &T2) -> bool> which worked and could easily be used.

Now I thought to myself, why not make it generic? If I find a good abstraction I might have other use cases for it!
So I went and added replaced the occurrences of T1 and T2 in the implementation of Condition with T as well as adding the generic parameter (T1,T2) to all my uses of the type.

The problem I encountered is the following:
Before I could pass &T1 and &T2 to the eval function (as it had two parameters). Now I have to pass either a &(T1, T2) or &(&T1,&T2) and both suffer some problems: the first requires to move T1 and T2 into a tuple, but I might not own them, the second works but now I have some lifetime in the type (Condition<(&'a T1,&'a T2)>) which makes it really unhandy to use and syntactically is not the same as before. For example before, the following function works perfectly fine and returns a Condition without any lifetime bounds whatsoever:

fn some_condition(&self) -> Condition { //condition has no lifetime parameter
    condition::condition!(|foo, bar| (foo.val == 0) and (bar.val > 20))
}

After the changes the code basically stays the same, except for a elided lifetime (and the tuple):

fn some_condition(&self) -> Condition { //has an elided lifetime &'a self and Condition<'a> 
    condition::condition!(|(foo, bar)| (foo.val == 0) and (bar.val > 20))
}

Now, from my understanding Condition wouldn't need the lifetime at all, as it doesn't actually contain anything of the type T (similar to how Box<dyn Fn(&T)> does not require a lifetime specifier). Is there any way how I can get rid of this lifetime?

(The macro expands into the tree of Condition).

Simplified code for the generic version:

enum Condition<T> {
    And {
        lhs: Box<Self>,
        rhs: Box<Self>,
    },
    Bool {
        closure: Box<dyn Fn(&T) -> bool>,
    },
}
impl<T> Condition<T> {
    pub fn eval(&self, t: &T) -> bool {
        match self {
            Self::And { lhs, rhs } => lhs.eval(t) && rhs.eval(t),
            Self::Bool { text: _, closure } => closure(t),
        }
    }
}

Simplified code for the non generic version:

enum Condition {
    And {
        lhs: Box<Self>,
        rhs: Box<Self>,
    },
    Bool {
        closure: Box<dyn Fn(&T1, &T2) -> bool>,
    },
}
impl Condition {
    pub fn eval(&self, t1: &T1, t2: &T2) -> bool {
        match self {
            Self::And { lhs, rhs } => lhs.eval(t1, t2) && rhs.eval(t1, t2),
            Self::Bool { text: _, closure } => closure(t1, t2),
        }
    }
}

If you always want two parameters, why not encode that?

enum Condition<T, U> {
    And {
        lhs: Box<Self>,
        rhs: Box<Self>,
    },
    Bool {
        closure: Box<dyn Fn(&T, &U) -> bool>,
    },
}

impl<T, U> Condition<T, U> {
    pub fn eval(&self, t: &T, u: &U) -> bool {
        match self {
            Self::And { lhs, rhs } => lhs.eval(t, u) && rhs.eval(t, u),
            Self::Bool { closure } => closure(t, u),
        }
    }
}

one minor note, you could change Condition to

enum Condition<T, U> {
    And(Box<(Self, Self)>),
    Bool {
        closure: Box<dyn Fn(&T, &U) -> bool>,
    },
}

to avoid allocating twice on every And, although this does complicate things a bit

impl<T, U> Condition<T, U> {
    pub fn eval(&self, t: &T, u: &U) -> bool {
        match self {
            Self::And(operands) => {
                let (lhs, rhs) = *operands;
                lhs.eval(t, u) && rhs.eval(t, u)
            },
            Self::Bool { closure } => closure(t, u),
        }
    }
}

Because I don't :slight_smile:
My current use case is that way, but I have some other cases in mind where I would like to test it (and probably use it) that don't have two parameters.
I think variadic generics would solve my problem, but those still seem far away..

Good point with the unnecessary allocations, those will be removed ^^

Yes, it looks like variadic generics would neatly solve your problem. There are ways to emulate variadic, but their all really ugly. Instead, why not be generic over the function type? Then you could use a macro to fill in "variadic generics" up to whatever argument limit you want like this. (you can use cargo expand to see the macro expansion). This also allows you to easily extend to Send + Sync on the closure if you need those.

2 Likes

That might work! But it's nearly 3am here and I think I need a bit of sleep to fully appreciate the macro :stuck_out_tongue:

Wouldn't it be possible to extend the eval function to take a closure that can execute the closures contained in the condition and completly get rid of the macro?
Something along the lines:

fn eval<Exec: Fn(F) -> bool> (&self, exec: Exec) -> bool {...}

If you have any questions, feel free to ask!

How would you pass the arguments in?

The caller of eval would provide them in the call to eval like so (if that works):

let foo = ...;
let bar = ...;
let foo = &foo;
let bar = &bar;
let constraint = get_some_constraint();
constraint.eval(|closure| closure(foo, bar));

Do you think this could work?

Yeah, that could work! Something like this

I was trying to figure it out too, but i don't seem to get the bounds correct in order to be able to actually call the function:
playground
And honestly I'm not exactly sure what the compiler is trying to tell me right now -.-

  1. You forgot the ?Sized bound on the impl
  2. You can also loosen the Fn to FnMut

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e5ff712f8393c5dab225a6f4a5e90c4e

eval_impl is similar to your eval, and my eval is there to all you to pass in the closure by value.

I thought I tried that, but looking at my history I forgot the ?...

Perfect, I think this solution will fit my problem really well and might even allow other use cases.
Thanks for all the help!!!

1 Like