I am an OLD programmer (once upon a time I maintained LISP and APL interpretters). I have read the RUST book twice, and finally came up with a project for myself. (Quirky Rolodex.) My general approach is a base object that will own the people, organizations, etc. The people, for example, also need references to other people (without confusing the ownership.) The obvious way to do this is with weak references. What causes me to ask here instead of just doing it is that I want to end up with a way to store these things. WHich leads me to wonder if I am better off with pseudo-references, e.g. wrapped IDs with methods that use the base object to check for existence and find the target. Or is there a better way around the storage / retreival problem.
Also, from what I have read, the simplest way I have to create the GUI aspects (I am terrible at GUI) is to use eguid?
Thank you very much,
Joel
As a place to get started, “pseudo-references, e.g. wrapped IDs” is a good one that won’t cause you problems. You might later choose something else when you have more knowledge of your problem and Rust.
Note that it is also possible to provide a different type that wraps the whole database to provide convenient access without requiring the caller to work with IDs. As a short sketch:
// Basic data structures:
struct Database {
entries: Vec<Person>,
}
struct PersonId(usize);
struct Person {
name: String,
friends: Vec<PersonId>,
}
// Convenient view data structure:
struct PersonView<'a> {
database: &'a Database,
person: &'a Person,
}
impl<'a> PersonView<'a> {
fn name(&self) -> &'a str {
&self.person.name
}
fn friends(&self) -> impl Iterator<Item = PersonView<'a>> {
self.person.friends.iter().map(|&PersonId(id)| PersonView {
database: self.database,
person: &self.database.entries[id],
})
}
}
This way, the Database is straightforwardly serializable, and contains no Rc or &, but the PersonView type allows an “object” access pattern without directly manipulating IDs. The key design principle here is that the PersonView that contains references is temporary — it is not itself part of the Database. This is how it avoids complicating ownership.
Form what you write, just use a HashMap. Something overly complex is best avoided.
Thank you. That sketch amplifies on what I was thinking. If I had tried to do it without your hint, I probably would have missed the need for lifetime scoping and been puzzled by the errors I got. To confirm my reading, to enable adding and subtracting things from the database, one would use the internal modifications pattern via methods on the database structure? Assuming that is right, it is very nice because I can carry the database references around without having to pass it everywhere.
Appreciate the prompt and helpful response,
I think, as per the other response, I probably want a hashmap rather than a vector in the database, so that I can remove entries?
Joel
What do you mean by “the internal modifications pattern”? Do you mean interior mutability? If so, then I recommend not doing that to start with. Use ordinary &mut access. If you decide to write a type like PersonView for mutation, then its design needs to be slightly different:
struct PersonViewMut<'a> {
database: &'a mut Database,
person: PersonId,
}
Because mutation requires exclusive access, you can't use multiple references that would overlap, so you should instead use one &mut reference and IDs for all other designation of what data the view type is referring to.
But having PersonViewMut at all is a complication; the simple form is that mutations are done by methods on the database.
That’s a reasonable option, but you could also use a Vec<Option<Person>> to allow it to be “sparse”.
Thanks. Yes, Option would seem simpler.
I am missing something in your comments on interior mutability. As I understand the approach we are discussing, we would ahve a lot of references to the database (which seems a helpful pattern). But then, how do I add entries to the fields of the database. I though I understood from the book that Interior Mutability was the recommended way to achieve that. Sorry if I am being dense.
Again, thanks for the assistance,
Joel
Interior mutability would be what you use to allow modifications while there are many references. What I am saying is, the basic strategy you should use by default is to not have those references. That is, whenever the the database is being modified (through some exclusive borrow), no PersonViews exist (there are no shared borrows), though you may have some PersonIds.
Using interior mutability instead would be quite complex. Or rather, it would be simple to, for example, allow changing a name, but it would be complex to allow changes to the structure of the database (adding or removing entries) and, if used in a way that actually allows that, would likely make the ownership ambiguous (e.g. it would become possible to accidentally make a change to a Person that no longer existed in the database). Sometimes it is in fact the best option, but until you are more familiar with the tradeoffs, it shouldn’t be your first choice.
Thank you for the further explanation. Seems to be time to build some experiments so I better understand what is possible. It may be that my mental operational model needs to be different. But at least I now know what I am looking at.
Yours,
Joel
Still building up test cases for the above. Meanwhile, another question occurred to me. Presume I want Persons only created with an id from the database. The pattern I am familiar with is a factory pattern. I am trouble figuring out the visiblity so that Persons are visible, but the Person::new(name, id) is only usable by the database? Where the database would have a makePerson method. Am I doing this non-rustful?
Thanks,
Joel
I would be inclined to create a Person without an Id at all, and then have the database assign it one on insertion.
Thanks. I wondered if that was the right answer in this case (use Option to allow for field absence). But I did wondere about the general patttern.
Yours,
Joel