2

I'm new to rust, and what I want is:

  • A set of properties (Prop)
  • A set of categories (Cat), which are defined by one or more Props
  • A dictionary (Dict), which stores these things. Once created, Cats and Props are read-only.

It seems straightforward enough, but Rust is making it very difficult; with my programming background it's counterintuitive and frustrating, and I can't work out the correct way to do it.

use std::collections::HashMap;

struct Prop<'a> {
    name: &'a str
}

type PropVec<'a> = Vec<&'a Prop<'a>>;

struct Cat<'a> {
    name: &'a str,
    props: PropVec<'a>
}

struct Dict<'a> {
    props: HashMap<&'a str, Prop<'a>>,
    cats:  HashMap<&'a str, Cat<'a>>,
}

impl<'a> Dict<'a> {
    pub fn define_prop(&mut self, name: &'a str) {
        self.props.insert(name, Prop { name });
    }

    pub fn prop(&self, name: &str) -> Option<&Prop> {
        self.props.get(name)
    }

    pub fn define_cat(&mut self, name: &'a str, prop_names: Vec<&'a str>) {
        let mut props: PropVec<'a> =  Vec::new();

        for name in prop_names {
            if let Some(prop) = self.prop(name) {
                props.push(prop);
            }
        }
        
        self.cats.insert(name, Cat { name: name, props });
    }
}

fn main() {
    let mut d = Dict { props: HashMap::new(), cats: HashMap::new() };
    d.define_prop("foo");
    d.define_cat("bar", vec!("foo"));
}

The compiler says:

error: lifetime may not live long enough
  --> src/main.rs:29:24
   |
19 | impl<'a> Dict<'a> {
   |      -- lifetime `'a` defined here
...
28 |     pub fn define_cat(&mut self, name: &'a str, prop_names: Vec<&'a str>) {
   |                       - let's call the lifetime of this reference `'1`
29 |         let mut props: PropVec<'a> =  Vec::new();
   |                        ^^^^^^^^^^^ type annotation requires that `'1` must outlive `'a`

error[E0502]: cannot borrow `self.cats` as mutable because it is also borrowed as immutable
  --> src/main.rs:37:9
   |
19 | impl<'a> Dict<'a> {
   |      -- lifetime `'a` defined here
...
29 |         let mut props: PropVec<'a> =  Vec::new();
   |                        ----------- type annotation requires that `*self` is borrowed for `'a`
...
32 |             if let Some(prop) = self.prop(name) {
   |                                 --------------- immutable borrow occurs here
...
37 |         self.cats.insert(name, Cat { name: name, props });
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here

And so:

  1. Because I'm using references, I need to put lifetimes everywhere, which leads to the first error. Is there another way to do it? I've tried using Box and Rc instead, but without success. If not, what is the correct way to specify the lifetimes?
  2. Is the comment about *self in line 29 anything to worry about?
  3. I understand that the first borrow of self is preventing the second, but surely it's possible to get the Prop, or a reference to it, and store it somewhere else?
Araceli
  • 31
  • 2
  • 2
    Does this answer your question? [Why can't I store a value and a reference to that value in the same struct?](https://stackoverflow.com/questions/32300132/why-cant-i-store-a-value-and-a-reference-to-that-value-in-the-same-struct) – Chayim Friedman Feb 27 '23 at 13:20
  • 1
    Why are you using references at all? If your contained types were `String` do your problems go away? The way you're structured, it doesn't look like you _need_ to have multiple things referencing common objects. Yes you'll have duplicate string allocations, but does it _matter_ to your use case? If thousands or less, it probably doesn't. If millions or billions of references, this is too simple for that anyways. – Kevin Anderson Feb 27 '23 at 13:53
  • Duplicate strings are not really the problem. As I say in a comment below, the point - which is admittedly not obvious from my code, which is a MRE - is that there will be other things in Proc and Cat which it makes no sense to duplicate. Or is that just the way Rust works? (confused) – Araceli Feb 27 '23 at 15:36
  • The best way will be to avoid self-referential structs completely, by storing the name of the `Prop` you want instead of references to them and then retrieve them on-demand from the hashmap. If this is not good for you for some reason, you can use `Rc`. – Chayim Friedman Feb 27 '23 at 16:16
  • I've replaced all references with Rc's, and it's very nearly working, but for the fact that I now have `props.push(*prop);` in `define_cat()`, and it complains that `*prop` is behind a shared reference. What else do I need to do? I could use references wrapped in Rc's, but this looks like overkill. – Araceli Feb 27 '23 at 16:53
  • It is `Rc`, right? So just `Rc::clone(prop)`. – Chayim Friedman Feb 27 '23 at 16:57
  • I didn't know you could do that, but it seems to have worked. Thank you! – Araceli Feb 27 '23 at 18:05
  • My comment is pretty useless, but I must notice that you’re not using objects in your examples. – Miiao Feb 28 '23 at 08:31

1 Answers1

1

type PropVec is Vec<&Prop> and contains the references to the props stored in Dict:

use std::collections::HashMap;

struct Prop {
    name: String,
}

type PropVec<'a> = Vec<&'a Prop>;

struct Cat<'a> {
    name: String,
    props: PropVec<'a>,
}

struct Dict<'a> {
    props: HashMap<String, Prop>,
    cats: HashMap<String, Cat<'a>>,
}

impl<'a> Dict<'a> {
    pub fn define_prop(dict: &mut Dict, name: &str) {
        dict.props.insert(
            name.to_string(),
            Prop {
                name: name.to_string(),
            },
        );
    }

    pub fn define_cat(dict: &'a mut Dict<'a>, name: &str, prop_names: Vec<String>) {
        let mut props: PropVec = Vec::new();
        for name in prop_names {
            if let Some(prop) = dict.props.get(&name) {
                props.push(prop);
            }
        }
        dict.cats.insert(
            name.to_string(),
            Cat {
                name: name.to_string(),
                props,
            },
        );
    }
}

fn main() {
    let mut d = Dict {
        props: HashMap::new(),
        cats: HashMap::new(),
    };
    Dict::define_prop(&mut d, "foo");
    Dict::define_cat(&mut d, "bar", vec!["foo".to_string()]);
}

Playground

Kaplan
  • 2,572
  • 13
  • 14
  • I don't want to use Clone since Cat and Proc will contain other fields, and it seems awfully inelegant to have several identical Proc objects when I only really need one. This is why I'm using references in the first place. – Araceli Feb 27 '23 at 15:31
  • What has changed from the original question that would require using `Rc`? Btw: it's no longer any cloning in the solution… – Kaplan Mar 01 '23 at 23:55
  • It's working rather nicely now with Rc, thanks to a helpful suggestion above. – Araceli Mar 03 '23 at 12:01
  • I would like to know where `Rc` brings something new, that doesn't already work with the above solution. What about a playground-link? Doesn't the structs have to be cloneable for `Rc`? – Kaplan Mar 03 '23 at 12:24