问题
In chapter 13 of the Rust book, you implement a Cacher
to use memoization to demonstrate functional programming and how to speed up long-running tasks. As an extra challenge, they recommend making the Cacher
allow multiple keys using a HashMap
and also leveraging generics to allow more flexibility.
Try modifying
Cacher
to hold a hash map rather than a single value. The keys of the hash map will be thearg
values that are passed in, and the values of the hash map will be the result of calling the closure on that key. Instead of looking at whetherself.value
directly has aSome
or aNone
value, the value function will look up thearg
in the hash map and return the value if it’s present. If it’s not present, theCacher
will call the closure and save the resulting value in the hash map associated with itsarg
value.The second problem with the current
Cacher
implementation is that it only accepts closures that take one parameter of typeu32
and return au32
. We might want to cache the results of closures that take a string slice and returnusize
values, for example. To fix this issue, try introducing more generic parameters to increase the flexibility of theCacher
functionality.
I was able to implement the HashMap
, however when trying to replace the closure definition u32
with a generic type and use that as the signature of the HashMap
, I run into an issue.
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::thread;
use std::time::Duration;
struct Cacher<'a, T>
where
T: Fn(&'a u32) -> &'a u32,
{
calculation: T,
values: HashMap<&'a u32, &'a u32>,
}
impl<'a, T> Cacher<'a, T>
where
T: Fn(&'a u32) -> &'a u32,
{
fn new(calculation: T) -> Cacher<'a, T> {
Cacher {
calculation,
values: HashMap::new(),
}
}
fn values(&mut self, arg: &'a u32) -> &'a u32 {
match self.values.entry(arg) {
Entry::Occupied(e) => &*e.into_mut(),
Entry::Vacant(e) => &*e.insert(&(self.calculation)(&arg)),
}
}
}
fn generate_workout(intensity: u32, random_number: u32) {
let mut expensive_result = Cacher::new(|num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
&num
});
if intensity < 25 {
println!("Today, do {} pushups!", expensive_result.values(&intensity));
println!("Next, do {} situps!", expensive_result.values(&intensity));
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
expensive_result.values(&intensity)
);
}
}
}
fn main() {
let simulated_user_specified_value = 10;
let simulated_random_number = 7;
generate_workout(simulated_user_specified_value, simulated_random_number);
}
I tried K, V
generics as below and it complains with Expected one of 7 possible values here
pointing to the first type definition.
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::hash::Hash;
use std::thread;
use std::time::Duration;
struct Cacher<'a, T: 'a, K: 'a, V: 'a>
where
T: Fn(&'a K) -> &'a V,
K: Hash + Eq,
{
calculation: T,
values: HashMap<&'a K, &'a V>,
}
impl<'a, T: 'a, K: 'a, V: 'a> Cacher<'a, T: 'a, K: 'a, V: 'a>
where
T: Fn(&'a K) -> &'a V,
K: Hash + Eq,
{
fn new(calculation: T) -> Cacher<'a, T: 'a, K: 'a, V: 'a> {
Cacher {
calculation,
values: HashMap::new(),
}
}
fn values(&mut self, arg: &'a K) -> &'a V {
match self.values.entry(arg) {
Entry::Occupied(e) => &*e.into_mut(),
Entry::Vacant(e) => &*e.insert(&(self.calculation)(&arg)),
}
}
}
fn generate_workout(intensity: u32, random_number: u32) {
let mut expensive_result = Cacher::new(|num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
&num
});
if intensity < 25 {
println!("Today, do {} pushups!", expensive_result.values(&intensity));
println!("Next, do {} situps!", expensive_result.values(&intensity));
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
expensive_result.values(&intensity)
);
}
}
}
fn main() {
let simulated_user_specified_value = 10;
let simulated_random_number = 7;
generate_workout(simulated_user_specified_value, simulated_random_number);
}
Results in the following error:
error: expected one of `!`, `(`, `+`, `,`, `::`, `<`, or `>`, found `:`
--> src/main.rs:16:39
|
16 | impl<'a, T: 'a, K: 'a, V: 'a> Cacher<T: 'a, K: 'a, V: 'a>
| ^ expected one of 7 possible tokens here
Is the only way to add 2 more generics (i.e. K
, V
) or is there a way to reuse a single generic? If 2 required, what am I missing above?
Is there a more idiomatic approach to solving this problem? The Rust book does not offer a solution, unfortunately.
回答1:
Your implementation does not compile because lifetime bounds have to be declared only after impl
:
impl<'a, T: 'a, K: 'a, V: 'a> Cacher<'a, T, K, V>
where
T: Fn(&'a K) -> &'a V,
K: Hash + Eq,
{
fn new(calculation: T) -> Cacher<'a, T, K, V> {
Cacher {
calculation,
values: HashMap::new(),
}
}
}
Storing references into the HashMap
implies that you have to manage lifetimes and assure that the values referenced by HashMap
outlive the Cacher
.
Another approach to consider may be to cache by values:
struct Cacher<T, K, V>
where
T: Fn(K) -> V,
{
calculation: T,
value: HashMap<K, V>,
}
impl<T, K, V> Cacher<T, K, V>
where
T: Fn(K) -> V,
K: Hash + Eq + Clone
{
fn new(calculation: T) -> Cacher<T, K, V> {
Cacher {
calculation,
value: HashMap::new(),
}
}
fn value(& mut self, arg: K) -> &V {
match self.value.entry(arg.clone()) {
Entry::Occupied(v) => v.into_mut(),
Entry::Vacant(v) => v.insert((self.calculation)(arg)),
}
}
}
Please note that in this solution I added the constraint that K
is Clone
来源:https://stackoverflow.com/questions/50055756/is-it-possible-to-use-a-single-generic-for-both-key-and-value-of-a-hashmap