Reputation: 1520
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.
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
WorkoutFactory
can be contained in another struct that is also forced to be parameterized by T
, which can be contained in another struct, ... - the closure type spreads like plague. With perhaps other T
's coming from deep down the member hierarchy, the signature of the top-level struct can become monstrous.WorkoutFactory
should be just an implementation detail, perhaps the caching was even added at version 2.0, but the type parameter is visible in the public interface of WorkoutFactory
and needs to be accounted for. The seemingly implementation detail is now part of the interface, no good :(Is there some way to work around these problems without changing the signature of Cacher
? How do others cope with this?
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.
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
?
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
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
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