Tomáš Dvořák
Tomáš Dvořák

Reputation: 1520

structs with boxed vs. unboxed closures

I'm still internalizing closures in Rust and how to best work with them, so this question might be somewhat vague, and there will perhaps be silly sub-questions. I'm basically looking for proper idioms and maybe even transforming the way I think about how to do some stuff in Rust.

Storing unboxed closure

The Rust book has an example simple Cacher in it's chapter on closures:

struct Cacher<T>
where
    T: Fn(u32) -> u32,
{
    calculation: T,
    value: Option<u32>,
}

impl<T> Cacher<T>
where
    T: Fn(u32) -> u32,
{
    fn new(calculation: T) -> Cacher<T> {
        Cacher {
            calculation,
            value: None,
        }
    }

    fn value(&mut self, arg: u32) -> u32 {
        match self.value {
            Some(v) => v,
            None => {
                let v = (self.calculation)(arg);
                self.value = Some(v);
                v
            }
        }
    }
}

It is supposed to be used like this:

let mut c = Cacher::new(|a| a);
let v1 = c.value(1);

That is perfectly fine and useful, but what if I need to have this Cacher be a member of another struct, say (in the spirit of the Rust book chapter), a WorkoutFactory? Since Cacher is parameterized by the closure's type, I am forced to parameterize WorkoutFactory with the same closure type.

Is my understanding correct? I guess so, the Cacher struct structure depends on the type T of the calculation, so the struct WorkoutFactory structure must depend on the type of Cacher. On one hand this feels like a natural, unavoidable and perfectly justified consequence of how closures work in Rust, on the other hand it means that

Is there some way to work around these problems without changing the signature of Cacher? How do others cope with this?

Storing boxed closure

If I want to get rid of the type parameters, I can Box the closure. I've come up with the following code:

struct BCacher {
    calculation: Box<Fn(u32) -> u32>,
    value: Option<u32>,
}

impl BCacher {
    fn new<T: Fn(u32) -> u32 + 'static>(calculation: T) -> BCacher {
        BCacher {
            calculation: Box::new(calculation),
            value: None,
        }
    }

    fn value(&mut self, arg: u32) -> u32 {
        match self.value {
            Some(v) => v,
            None => {
                let v = (self.calculation)(arg);
                self.value = Some(v);
                v
            }
        }
    }
}

I can use it exactly like Cacher:

let mut c = BCacher::new(|a| a);
let v1 = c.value(1);

...almost :( The 'static' annotation means I can't do this:

let x = 1;
let mut c = BCacher::new(|a| a + x);

because the closure may outlive x. That is unfortunate, something possible with the non-boxed version is no longer possible with the boxed version.

Furthermore, this version is less efficient, it is necessary to dereference the Box (is that correct?), and RAM access is slow. The difference will most probably be negligible in most cases, but still..

I could address the first issue with lifetime annotation:

struct BLCacher<'a> {
    calculation: Box<Fn(u32) -> u32 + 'a>,
    value: Option<u32>,
}

but now I'm back to Cacher with type parameters and all the unpleasant consequences of that.

The right to choose

This seems like an unfortunate situation. I have two approaches to storing closure in a struct, and each has it's own set of problems. Let's say I'm willing to live with that, and as the author of the awesome fictional Cacher crate, I want to present the users with both implementations of Cacher, the unboxed Cacher and the boxed BCacher. But I don't want to write the implementation twice. What would be the best way - if there's any at all - to use an existing Cacher implementation to implement BCacher?

On a related note (maybe it is even the same question), let's assume I have a

struct WorkoutFactory<T>
where
    T: Fn(u32) -> u32,
{
    cacher: Cacher<T>,
}

Is there a way to implement GymFactory without type parameters that would contain - for private purposes - WorkoutFactory with type parameters, probably stored in a Box?

Summary

A long question, sorry for that. Coming from Scala, working with closures is a LOT less straightforward in Rust. I hope I've explained the struggles I've not yet found satisfactory answers to.

Upvotes: 9

Views: 1587

Answers (2)

ByteVoyager
ByteVoyager

Reputation: 121

Cacher<T> is strictly more general than BCacher and BLCacher<'a>.

type BCacher = Cacher<Box<dyn Fn(i32) -> i32>>;

type BLCacher<'a> = Cacher<Box<dyn Fn(i32) -> i32 + 'a>>;

This works because Box<T> implements the Fn trait whenever T does, and dyn Fn(i32) -> i32 implements Fn.

Upvotes: 0

matiu
matiu

Reputation: 7725

Not a full answer sorry, but in this bit of code here:

let x = 1;
let mut c = BCacher::new(|a| a + x);

Why not change it to:

let x = 1;
let mut c = BCacher::new(move |a| a + x);

That way the functor will absorb and own 'x', so there won't be any reference to it, which should hopefully clear up all the other issues.

Upvotes: 0

Related Questions