Creating cacher for closure with generics

I am following "the book" and I'm working on chapter 13 regarding functional programming features. In this chapter I created a Cacher for an expensive function but am stuck with making improvements to fix the limitations mentioned here.

I successfully converted my Cacher to accept generic values but when I want to create the specific case mentioned I cannot. I want to accept a string slice and return usize however, the compiler tells me I can't do this because I am forcing my parameters to have the Copy trait of which String does not.

How should I have written my Cacher to accept generic functions and allow me pass in 2 different closures. One that simply accepts and returns the u32 parameter passed in and another that accepts a string slice parameter and return usize. I saw an example where the person implemented lifetimes but the code look very unreadable and when I got it to work this way without lifetimes I was happy but maybe that is required. Here is that code found in a SO question.

Here is the relevant code:

edit: I'm unsure why my code is not indented correctly at first. I used the blockquote function. Is this incorrect?

 struct Cacher <T, K, V>
     where T: Fn(K) -> V
{
    calculation: T,
    values: HashMap<K, V>,
}

impl<T, K, V> Cacher<T, K, V>
where
    T: Fn(K) -> V,
    K: Hash + Eq + Clone + Copy,
    V: Copy
{
    fn new(calculation: T) -> Cacher<T, K, V> {
        Cacher {
            calculation,
            values: HashMap::new()
        }
    }

    fn value(&mut self, arg: K) -> V {
        match self.values.get(&arg) {
            Some(v) => *v,
            None => {
                let v = (self.calculation)(arg);
                self.values.insert(arg, v);
                v
            },
        }
    }
}

Here are the 2 closures I'm trying to implement:

    let mut expensive_closure = Cacher::new(|num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    });

    let mut expensive_closure_2 = Cacher::new(|string_slice: String| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        string_slice.chars().count()
    });

Code blocks are done using three backtics:

```
// your code here
```

You are using the feature meant for quotes.

1 Like

You can avoid the Copy requirement with two approaches:

  1. Stop using Copy and explicitly clone it.
  2. Give the closure a borrow instead of an owned value.

Using an explicit clone:

fn value(&mut self, arg: K) -> V {
    match self.values.get(&arg) {
        Some(v) => *v,
        None => {
            let v = (self.calculation)(arg.clone()); // clone it!
            self.values.insert(arg, v);
            v
        },
    }
}

playground

and to give it a borrow, you change the function to accept an &K instead:

impl<T, K, V> Cacher<T, K, V>
where
    T: Fn(&K) -> V, // a reference
    K: Hash + Eq + Clone,
    V: Copy
{

    fn value(&mut self, arg: K) -> V {
        match self.values.get(&arg) {
            Some(v) => *v,
            None => {
                let v = (self.calculation)(&arg); // a reference
                self.values.insert(arg, v);
                v
            },
        }
    }
}

playground

I'm not quite sure what you mean with the two closures?

Thanks! The clone implementation achieves exactly what I was trying to do.

Regarding what I meant with my two closures...After I modified my Cacher to use a closure that accepted and returned generics it worked for my first closure that passed in u32 by luck because it is a primitive type that is saved on the stack but my second closure passed in a string slice which is on the heap and required some changes to make it work as well. Your recommendation for cloning or borrowing solves this.

As a new rust developer I was just confused regarding where do I put the notation for &K since it's mentioned in multiple places (in struct Cacher signature, impl Cacher signature, and the 2 functions inside the impl block).

(P.S. can you confirm my understanding of why my generic closure worked with u32 but not string slice is correct)

Yeah well the difference between u32 and String is that only one of them are Copy. For a type to be Copy means that you can clone it simply by taking a copy of the memory. You can't do this with a string because the string owns the heap allocation that actually contains the data of the string, so the clone would need it's own unique heap allocation, which involves more work than just copying the type itself. Note that "copy of the memory" would not involve the heap allocation — it would just refer to the pointer to the heap allocation that is stored in the String proper.

Note that String is not called a string slice. There's another type &str which is called a string slice.

ah, thanks for clarifying that! I'm still figuring out String vs str to be honest but I am aware they are 2 different things.

That's a very common question, and a rather comprehensive answer can be found over here. Basically the difference is that a String is a thing that owns an allocation, while a &str is a slice into some allocation owned by something else.

A common misconception is that &str is the immutable version of String, but if you build from this assumption, you will run into lifetime troubles.

1 Like

Funny you say that because &str being an immutable version of String is one of the only key differences I do understand haha. I'll take a look at that post.

edit: I think the guidelines for this forum is to only post comments that add value to the thread so I guess this is an example of an unnecessary response. Oh well.

1 Like

It's true that &str is immutable, but if you need an immutable string that owns the allocation, you still want a String.

Why does arg only need clone when creating the v variable but not when passing it into the HashMap's insert function ?

    fn value(&mut self, arg: K) -> V {
        match self.values.get(&arg) {
            Some(v) => *v,
            None => {
                let v = (self.calculation)(arg.clone());
                self.values.insert(arg, v);
                v
            },
        }
    }

I understand the need for reference to arg in values.get(&arg) because we just want to look it up and not change ownership.

I understand the need for clone in let v = (self.calculation)(arg.clone()); because a new variable is being created with that data and we want v to have it's own copy since we are going to insert it into our HashMap which will live on past this value function.

Lastly though, self.values.insert(arg, v); does not take a reference or copy of arg. Doesn't this make insert function take ownership of arg ?

Yeah, both calculation and insert take ownership, but we already own one in the function, so we need one clone to give away ownership twice.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.