0

I have a list of books and a list of authors, where a book is written by a single author and an author may have written many books. How do I encode it in a Rust program?

The requirements:

  1. For any given object, accessing the objects in a relation with it should be of low complexity.

  2. Impossible states should be prevented. If the data is redundant, incoherence can appear.

An approach:

Each author and book is identified by a unique value (here the index in the table).

struct Book {
    author: usize,
}

struct Author {
    books: Vec<usize>,
}

let books = vec![
    Book { author: 0 },
    Book { author: 0 },
    Book { author: 1 },
];

let authors = vec![
    Author { books: vec![0, 1] },
    Author { books: vec![2] },
];

This approach satisfies the first requirement but not the second. In an implementation, a hash map may be used or the Slab collection to ensure the ids remain valid.

I don't see how to use the Rust type system with the different tools available to make a relation between object satisfying my two requirements.

This is a recurrent pattern in programming, and I might be reinventing the wheel.

philipxy
  • 14,867
  • 6
  • 39
  • 83
uben
  • 1,221
  • 2
  • 11
  • 20
  • Are you trying to do this so that you can easily fetch the books of an author? – Ahmed Masud Mar 05 '23 at 18:07
  • Exactly. Every time there is a relation between two object, accessing one from the other should be of constant complexity. (at least ideally) – uben Mar 05 '23 at 18:18
  • Not always you can have everything you want. If the data is immutable once created, you can go with `Rc`/`Weak`. Otherwise, I'd recommend the first approach. – Chayim Friedman Mar 05 '23 at 18:43
  • 1
    I cannot see how the second approach fulfils the second requirement better than the first approach. If we have many books and authors referenced with Rc/Weak, nothing prevents us from registering inconsistent relations between them, in the same way as we may introduce confusions between the numerical ids of the first approach. Consistency must be enforced by an algorithmic process in both cases: register reciprocal RC/Weak or ids when a new pair book/author is introduced. – prog-fh Mar 05 '23 at 21:29
  • 1
    Numerical ids are simple and can be enough if nothing is removed. If we have to remove some books or authors, these numerical indices will become incorrect; [generational indices](https://crates.io/crates/generational-arena) could help in this case. – prog-fh Mar 05 '23 at 21:33

1 Answers1

0

I don't know how much help you need; maybe you will find that the following example is trivial.

The ownership of the resources (books and authors) is fully under the control of the library. Because the scenario is very simple (no book or author removal) the relations between these resources only rely on numerical indices. If we needed the ability to remove the resources, some generational indices could help; in this case, some possible failures would have to be considered (Option/Result) when accessing these resources.

In the present scenario, all the details about the indices are hidden from the public interface. Thus, we provide iterators (impl Iterator...) in order to inspect the resources. If the indices had to be exposed in the public interface, a random access would still be possible via the iterators (although providing a slice would probably be more natural).

Note that, because of impl, iterator types are precisely known at the compilation step and will probably be optimized away as if we directly worked on slices with numerical indices in the main program.

// edition = 2021
mod example {
    pub struct Book {
        title: String,
        author_id: usize,
    }
    impl Book {
        pub fn title(&self) -> &str {
            &self.title
        }
    }

    pub struct Author {
        name: String,
        book_ids: Vec<usize>,
    }
    impl Author {
        pub fn name(&self) -> &str {
            &self.name
        }
    }

    pub struct Library {
        books: Vec<Book>,
        authors: Vec<Author>,
    }
    impl Library {
        pub fn new() -> Self {
            Self {
                books: Vec::new(),
                authors: Vec::new(),
            }
        }

        pub fn register_book(
            &mut self,
            book_title: String,
            author_name: String,
        ) {
            let author_id = if let Some(pos) =
                self.authors.iter().position(|x| x.name == author_name)
            {
                pos
            } else {
                self.authors.push(Author {
                    name: author_name,
                    book_ids: Vec::new(),
                });
                self.authors.len() - 1
            };
            self.authors[author_id].book_ids.push(self.books.len());
            self.books.push(Book {
                title: book_title,
                author_id,
            })
        }

        pub fn books(&self) -> impl Iterator<Item = &Book> {
            self.books.iter()
        }

        pub fn authors(&self) -> impl Iterator<Item = &Author> {
            self.authors.iter()
        }

        pub fn author_of(
            &self,
            book: &Book,
        ) -> &Author {
            &self.authors[book.author_id]
        }

        pub fn books_by<'a>(
            &'a self,
            author: &'a Author,
        ) -> impl Iterator<Item = &Book> + 'a {
            author.book_ids.iter().map(|id| &self.books[*id])
        }
    }
}

fn main() {
    let mut library = example::Library::new();
    library.register_book("title A".to_owned(), "name X".to_owned());
    library.register_book("title B".to_owned(), "name X".to_owned());
    library.register_book("title C".to_owned(), "name Y".to_owned());
    println!("~~ inspect by authors ~~");
    for author in library.authors() {
        let titles = library
            .books_by(author)
            .map(|b| format!("{:?}", b.title()))
            .collect::<Vec<_>>();
        println!("author {:?} wrote {}", author.name(), titles.join(", "));
    }
    println!("~~ inspect by books ~~");
    for book in library.books() {
        println!(
            "book {:?} written by {:?}",
            book.title(),
            library.author_of(book).name()
        );
    }
    println!("~~ random access ~~");
    for idx in [2, 3] {
        if let Some(book) = library.books().nth(idx) {
            println!("book {} is {:?}", idx, book.title());
        } else {
            println!("book {} does not exist", idx);
        }
    }
}
/*
~~ inspect by authors ~~
author "name X" wrote "title A", "title B"
author "name Y" wrote "title C"
~~ inspect by books ~~
book "title A" written by "name X"
book "title B" written by "name X"
book "title C" written by "name Y"
~~ random access ~~
book 2 is "title C"
book 3 does not exist
*/
prog-fh
  • 13,492
  • 1
  • 15
  • 30