If everything really is immutable, then the simplest way to handle this scenario is with Rc
/Arc
(the only difference between the two is that the latter can be used for objects accessed from multiple threads.) You can define your enum as follows:
pub enum Predicate {
And(Rc<Predicate>, Rc<Predicate>),
Neg(Rc<Predicate>),
Bool(LogicOp, Literal, Literal)
}
An Rc
is a reference-counted shared pointer to a heap-allocated object. You can clone it to obtain a new pointer to the internal data without any copying, and you will never have to worry about lifetimes: internally, the Rc
keeps track of how many references are being held to its object, and automatically deallocates it when this number drops to zero. This bookkeeping incurs a small runtime cost whenever the Rc
is cloned or dropped, but greatly simplifies dealing with this sort of shared ownership.
Of course, the use of an Rc
makes it somewhat more verbose to create predicates. The example you give would become:
let foo = Rc::new(Predicate::Bool(whatever args));
let and = Rc::new(Predicate::And(foo.clone(), foo.clone()));
let neg = Rc::new(Predicate::Neg(and.clone()));
let bar = Rc::new(Predicate::And(and.clone(), neg.clone()));
(Technically not all these clones are necessary if you did not intend to use, say, neg
later.)
One way to ease this boilerplate is to use methods or associated functions on the Predicate
type to create pre-Rc
ed values, like so:
impl Predicate {
pub fn and(a: &Rc<Predicate>, b: &Rc<Predicate>) -> Rc<Predicate> {
Rc::new(Predicate::And(a.clone(), b.clone())
}
pub fn neg(a: &Rc<Predicate>) -> Rc<Predicate> {
Rc::new(Predicate::Neg(a.clone()))
}
pub fn boolean(op: LogicOp, a: Literal, b: Literal) -> Rc<Predicate> {
Rc::new(Predicate::Bool(op, a, b))
}
}
With this, your example becomes:
let foo = Predicate::boolean(whatever args);
let and = Predicate::and(&foo, &foo);
let neg = Predicate::neg(&and);
let bar = Predicate::and(&and, &neg);
There is one unavoidable downside to this approach, though. You cannot match
through an Rc
without dereferencing it first, which can make working with Rc
ADTs somewhat painful. See this question for details.