0

I'm new to Rust and, coming from Java world, I wanted to play with Rust trait as I would do with Java interfaces. I imagined the following need :

  • I must be able to save User (firstname, lastname) somewhere (in a db, a file)
  • I can fetch all of them

I started to define the trait that I wanted to have :

trait UserDb {
    fn get_all(&self) -> Result<Vec<User>, io::Error>;

    fn save(&mut self, user: &User) -> Result<(), io::Error>;
}

You can see that when I declare get_all function, I don't mention the need to have a mutable borrow on self (i.e &mut self).

Then I decided to implement this trait with File capabilities (please find the full code at the end).

What surprised me is that, when I read the content of the file, I have to declare self as mutable. (here's a reason why : Why does a File need to be mutable to call Read::read_to_string?)

It annoys me because if I do that, I must declare in the trait self as mutable, even if I'm reading data. I feel like there is a leak of implementation detail in the trait.

I think my approach is not valid or not idiomatic in Rust. How would you achieve this ?

Here is the full code :

///THIS CODE DOESNT COMPILE
///THE COMPILER TELLS TO MAKE self AS MUTABLE
use std::fs::File;
use std::fs::OpenOptions;
use std::io;
use std::path::Path;
use std::io::Read;
use std::io::Write;

struct User {
    pub firstname: String,
    pub lastname: String,
}

trait UserDb {
    fn get_all(&self) -> Result<Vec<User>, io::Error>;

    fn save(&mut self, user: &User) -> Result<(), io::Error>;
}

struct FsUserDb {
    pub file: File,
}

impl FsUserDb {
    fn new(filename: &str) -> Result<FsUserDb, io::Error> {
        if Path::new(filename).exists() {
            let file = OpenOptions::new()
                .append(true)
                .write(true)
                .open(filename)?;

            Ok(FsUserDb { file })
        } else {
            Ok(FsUserDb {
                file: File::create(filename)?,
            })
        }
    }
}

impl UserDb for FsUserDb {
    fn get_all(&self) -> Result<Vec<User>, io::Error> {
        let mut contents = String::new();

        self.file.read_to_string(&mut contents)?;

        let users = contents
            .lines()
            .map(|line| line.split(";").collect::<Vec<&str>>())
            .map(|split_line| User {
                firstname: split_line[0].to_string(),
                lastname: split_line[1].to_string(),
            })
            .collect();

        Ok(users)
    }

    fn save(&mut self, user: &User) -> Result<(), io::Error> {
        let user_string =
            format!("{},{}", user.firstname, user.lastname);

        match self.file.write(user_string.as_bytes()) {
            Ok(_) => Ok(()),
            Err(e) => Err(e)
        }
    }
}

fn main() {
    let db = FsUserDb::new("/tmp/user-db");
}

Maxime
  • 570
  • 3
  • 18

1 Answers1

1

read required mutable borrow, there is not much you can do about that.

To solve you problem, there are three options I can think of:

  • Modify your signature on the trait to &mut self as the compiler recommends to. This is the clearest solution, I'm not sure why you don't like it.

  • Use internal mutability like RefCell and get the mutable File where you need it. With this solution you don't even need to declare save as mutable, but adds some runtime cost. I do recommend reading about RefCell since that can introduce other kind of errors later on.

  • Store the filename, not the File handler itself and open/close it when appropriate. With this, you can also use immutable save.

  • 1
    While `RefCell` has a small runtime cost, that cost is going to be negligible compared to reading a file. – mcarton Feb 08 '20 at 11:39
  • 1
    It's not like I really dislike to put `&mut self` in the signature, I just found it weird to say there is a mutation involved when reading data. I think I'll go for the first solution you mention, to follow the compiler hints. Thank you also for giving other options :). – Maxime Feb 08 '20 at 21:08