Trouble defining structs

This is my usual workflow for Rust projects:

  1. I write a big chunk of code.
  2. I split that code into functions.
  3. I turn those functions intro struct methods (to add structure).

The following (mock) code is step 2 of my workflow. Brief explanation: If the user presses a key that matches the key field in a Keymap, run the command that belongs to that Keymap. The command can have a prompt, and ask for the user for input, but it's optional.

use std::io::Write;

struct Keymap<'a> {
    key: char,
    command: &'a str,
    prompt: Option<&'a str>,
}

fn show_cursor(mut stdout: impl Write) {
    write!(stdout, "{}", "The cursor is visible now!").unwrap();
}

fn show_prompt(prompt: &str, mut stdout: impl Write) {
    write!(stdout, "{}", prompt).unwrap();
    stdout.flush().unwrap();
}

// In the actual application, this turns pressed keys into text input
fn get_input(stdin: &str, mut stdout: impl Write) -> String {
    write!(stdout, "{}", stdin).unwrap();
    stdin.to_owned()
}

fn handle_input(key: char, keymaps: &[Keymap], stdin: &str, mut stdout: impl Write) {
    if let Some(keymap) = keymaps.iter().find(|k| k.key == key) {
        match keymap.prompt {
            Some(_) => {
                show_prompt(keymap.prompt.unwrap(), &mut stdout);

                let input = get_input(stdin, &mut stdout);

                println!("{} executed with input: {}", keymap.command, input);
            }
            None => {
                println!("{} executed without input", keymap.command);
            }
        }
    }
}

fn main() {
    let mut stdout = Vec::new();

    stdout.flush().unwrap();

    let keymaps = vec![Keymap {
        key: 't',
        // in the actual application, `{}` is replaced by the input
        command: "echo {}",
        prompt: Some("Enter your input:"),
    }];

    show_cursor(&mut stdout);

    // Here I hardcoded the `t` character. In the real application, the user presses it.
    handle_input('t', &keymaps, "Pressed keys", stdout);
}

All those functions work with stdout, so I figured I could put them in a struct called Screen (step 3 of my workflow):

use std::io::Write;

struct Keymap<'a> {
    key: char,
    command: &'a str,
    prompt: Option<&'a str>,
}

struct Screen {
    stdout: Vec<u8>,
}

impl Screen {
    fn new(stdout: Vec<u8>) -> Self {
        Screen { stdout }
    }

    fn show_cursor(&mut self) {
        write!(self.stdout, "{}", "The cursor is visible now!").unwrap();
    }

    fn show_prompt(&mut self, prompt: &str) {
        write!(self.stdout, "{}", prompt).unwrap();
        self.stdout.flush().unwrap();
    }

    // In the actual application, this turns pressed keys into text input
    fn get_input(&mut self, stdin: &str) -> String {
        write!(self.stdout, "{}", stdin).unwrap();
        stdin.to_owned()
    }

    fn handle_input(&mut self, key: char, keymaps: &[Keymap], stdin: &str) {
        if let Some(keymap) = keymaps.iter().find(|k| k.key == key) {
            match keymap.prompt {
                Some(_) => {
                    self.show_prompt(keymap.prompt.unwrap());

                    let input = self.get_input(stdin);

                    println!("{} executed with input: {}", keymap.command, input);
                }
                None => {
                    println!("{} executed without input", keymap.command);
                }
            }
        }
    }
}

fn main() {
    let stdout = Vec::new();
    let mut screen = Screen::new(stdout);

    screen.stdout.flush().unwrap();

    let keymaps = vec![Keymap {
        key: 't',
        // in the actual application, `{}` is replaced by the input
        command: "echo {}",
        prompt: Some("Enter your input:"),
    }];

    screen.show_cursor();

    // Here I hardcoded the `t` character. In the real application, the user presses it.
    screen.handle_input('t', &keymaps, "Pressed keys");
}

Now, these are the problems I usually have:

  1. Sometimes I'm not sure which variables should be fields of the struct. In this case, stdin and &[Keymap].
  2. Sometimes I'm not sure if a function belongs to that struct. For example, handle_input isn't modifying stdout. Should it be a method of Screen?
  3. Sometimes I'm not sure if I should have created the struct at all.

I'd appreciate some advice on the matter.

There are in fact no inherently correct answers to those questions. Rust's borrowing rules mean that you frequently have to design your structs around how they are used.

In particular, if you put some things in one struct, that means that an &mut self method on it exclusively borrows all of them even if it only needs some of them. This may be innocuous, or it may prevent you from writing some code that you need. But, of course, a program should not be written with no structs just to avoid hypothetical problems.

My advice would be to stay flexible: think about these questions, pick a solution that seems okay, and be prepared to change it. As you write code, you will discover constraints you didn't think of, and sometimes these constraints conflict with the structs you designed previously. Then, change the structs.


I also see a problem that isn't what you asked about, and might be an artifact of your mock code, but I'll mention anyway: data structures most often should not have lifetimes; they should own their data. When you have data that is going to be held over a long period in your application, it should not borrow anything, because if it does, that obligates the code to be structured so the borrowed data continues to exist in its original location. This is a subtle mistake, because it works fine in simple main()s but breaks as soon as you want abstractions (MyApplication::new().run()), inversion of control, or even to mutate the data in the structure.

Concretely, instead of writing

struct Keymap<'a> {
    key: char,
    command: &'a str,
    prompt: Option<&'a str>,
}

you should replace the borrows with ownership:

struct Keymap {
    key: char,
    command: String,
    prompt: Option<String>,
}

or if the struct will always be used with string literals and no data created at run time, use &'static references:

struct Keymap {
    key: char,
    command: &'static str,
    prompt: Option<&'static str>,
}

Similarly, use Vec<Keymap> instead of &[Keymap].

Now, there are uses for structs with lifetimes; some structs are acting as complex borrows, and sometimes programs wish to avoid as much heap allocation as possible and make use of references in lieu of owned types in order to achieve that. But to all beginners, I say: if you are putting a lifetime parameter on your struct, it's very likely to be a mistake.

6 Likes

Thanks a lot for the suggestions!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.