I think there are better idioms to do what I want than I am using.
In order to avoid having lots of Rc between types, in my rolodex project (and now in the next toy I am trying), I am using identifiers through a parent which owns the long term instances. In the parent, I use a HashMap, and then the lookup methods can jsut return the
Option<T>
to reflect that a caller may have messed up getting a good identifier (even though in the usual case it will have worked. The extra check seems beneficial.)
I am current using a usize for the identifiers. What I would like to do in the new program is somehow have a type BookId, and a type AuthorId, each of which wrap a usize, but which will only be used with the appropriate lookups. So the compiler will make sure I don't try to look up a book as an author.
I think I saw a means to do this. But I am not sure what it was.
Also, if I do that, I presume I should use the wrapper type as the HashMap index, rather than merely using it on the accessor functions? (I will hide the actual implementation in case I decide to use some other mechanism.)
Thank you,
Joel
What you're looking for is called the newtype pattern, and it looks like this:
// Don't do this
struct Book {
id: usize
}
// Do this instead
struct Book {
id: BookId
}
struct BookId(usize);
That looks like what I am after. How do I create a BookId instance? And for tracking the last used BookId, do I keep a usize and build a BookId when I need a new one, or is there a way to increment a BookId directly in that paradigm?
Thanks,
Joel
You can implement methods on BookId just like on any other type defined in your crate. For example:
struct BookId(usize);
impl BookId {
// You would call it like this:
// let book_id = BookId::new(42);
pub fn new(value: usize) -> Self {
Self(value)
}
}
The newtype pattern is meant to be used to give you compile-time safe APIs. First of all, you should use them as much as possible, second, I don't understand how are you modelling your domain, but to me an entity named BookId sounds like is meant to be immutable; I wouldn't like the identifier of the Book entity to be suddenly mutated.
Ah, I can do everything I want with methods on BookId! Perfect.
As for your second comment, you are correct, the BookId for a given book never changes. I need to mint new book Id's when I add a book to the library. My assumed approach is to have a next_book_id stored in the library. When I add a book. I grab that Id, and assign it to the new Book. And then increment the next_book_id so I have a next one to assign. I can also just store net_book_id as a usize, and just mint the BookId when I need to assign it. Is one approach better than the other?
(For completeness, the BookIds are all held in the Library. Books don't actually know their Ids. However, the UI (iced) will be passign around BookIds for listing books or knowing what book it is working on.)
Thanks,
Joel
I see, But again, I would go with mutating the Library entity as per your example rather than BookId, like this:
struct Library {
next_book_id: BookId,
}
impl Library {
pub fn set_next_book_id(&mut self, next_book_id: BookId) {
self.next_book_id: next_book_id
}
}
To create a the next BookId from an existing one, does the method in the BookId implementation look like:
//generate the next BookId
pub fn succ(self) -> Self {
Self(self.0+1)
}
when BookId is, as you suggested:
struct BookId(usize);
Thanks. Sorry if I am being too pedantic. I didn't use type wrappers in the first project.
Joel
What I'm trying to convey is that the way I would model the API would be to mutate Library and not BookId.
// impl the AsRef trait for BookId so that we can access the private inner usize value
impl AsRef<usize> for BookId {}
impl Library {
pub fn succ(&mut self) {
self.next_book_id = BookId::new(self.next_book_id.as_ref() + 1);
}
}
Of course you could model it as you want, but it's more a matter of convention. Newtypes are, in general, immutable, and as a developer I would certainly feel odd if I see an API that mutates one.
That's one way to do it, as is @firebits.io Library suggestion.
My go-to way of creating such id types is a method on BookId which generates a conceptually arbitrary (but in practice sequential) unused id., like so:
pub struct BookId(usize);
impl BookId {
/// Get an unused BookId.
///
/// Each time this method is called it returns a different BookId value
/// (unless you call it more than usize::MAX times of course).
pub fn new() -> Self {
use std::sync::atomic::{AtomicUsize, Ordering};
// this atomic value stores the next ID which will be returned, it must
// be an atomic so that it still returns unique values even if called
// simultaneously from multiple threads.
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
Self(NEXT_ID.fetch_add(1, Ordering::Relaxed))
}
}
Note however that this only works if it's your only way of making BookId values (i.e. you can't also make one from say reading it from a file or suchlike and expect there to be no overlaps). If you do need to read in BookId values or otherwise construct them via other methods then a solution more like Library would be better.
Thanks. You reminded me that I do need to be able to read these from a file (I assume I will use serde_json), and then continue the sequence from where it was last. Which is part of why I was thinking of a successor method. So I reload the library with its "next", and then create successors from tehre. I am assuming that serde_json can take care of the innards, as I will decrive searialize and deserialize for the BookId?
Yours,
Joel
Yea, in that case I would have the Library type keep track of the next id to use, and have methods for reading from file (using serde_json or otherwise) and also for incrementing the next id. Yes, serde_json can handle reading and writing BookId's as long as you derive Serialize and Deserialize on them.
Something like
struct Library {
next_book_id: BookId,
books: HashMap<BookId, Book>,
}
impl Library {
fn load(file: Path) -> Self {
// using serde_json read in books from file, then set next_book_id to
// the id you want to increment from.
}
fn next_book_id(&mut self) -> BookId {
let id = self.next_book_id;
self.next_book_id.0 += 1;
id
}
Given that I only use the successor in the Library, I can see why that is a better place for it (even my proposal didn't actually mutate a BookId instance, just created a new one from teh existing one. Still putting it in Library does seem better.
Thanks,
Joel
Thanks for the very helpful replies.
1 Like