Программа REPL базы данных компании на Rust для главы 8 книги

Используя хэш-карту и векторы, создайте текстовый интерфейс, позволяющий пользователю добавлять имена сотрудников в отдел в компании. Например, «Добавить Салли в инжиниринг» или «Добавить Амира в отдел продаж». Затем позвольте пользователю получить список всех сотрудников отдела или всех сотрудников компании по отделам, отсортированный в алфавитном порядке.

Реализовал решение для третье упражнение главы 8 Книги. Я все еще пытаюсь сформировать интуицию в отношении того, что делает хорошую программу на Rust, и поэтому был бы очень признателен за некоторые советы, которые помогут мне улучшить. Вот мое текущее решение:

// Using a hash map and vectors, create a text interface to allow a user to
// add employee names to a department in a company. For example, “Add
// Sally to Engineering” or “Add Amir to Sales.” Then let the user retrieve
// a list of all people in a department or all people in the company by
// department, sorted alphabetically.

// @todo Add functions to remove people, departments and companies. Add a clear function.

use std::collections::HashMap;
use std::collections::HashSet;
use std::io;
use std::io::Write;

#[derive(Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
struct Employee(String);
#[derive(Debug)]
struct Department(HashSet<Employee>);
#[derive(Debug)]
struct Company(HashMap<String, Department>);

fn make_company() -> Company {
    Company(HashMap::new())
}

#[derive(Debug)]
struct ExistingDepartment(String);

// This assumes all department names are unique.
fn add_department(company: &mut Company, department: &str) -> Result<(), ExistingDepartment> {
    let department_name = String::from(department);
    match company.0.contains_key(&department_name) {
        true => Err(ExistingDepartment(department_name)),
        false => {
            company
                .0
                .insert(department_name, Department(HashSet::<Employee>::new()));
            Ok(())
        }
    }
}

#[derive(Debug)]
enum AddEmployeeError {
    NonexistentDepartment(String),
    ExistingEmployee(Employee),
}

// This assumes that all employee names in a department are unique.
fn add_employee(
    company: &mut Company,
    department_name: &str,
    employee: Employee,
) -> Result<(), AddEmployeeError> {
    match company.0.get_mut(department_name) {
        None => Err(AddEmployeeError::NonexistentDepartment(String::from(
            department_name,
        ))),
        Some(department) => match department.0.insert(employee.clone()) {
            true => Ok(()),
            false => Err(AddEmployeeError::ExistingEmployee(employee)),
        },
    }
}

#[derive(Debug)]
struct NonexistentDepartment(String);

fn get_employees_department(
    company: &Company,
    department_name: &str,
) -> Result<Vec<Employee>, NonexistentDepartment> {
    match company.0.get(department_name) {
        None => Err(NonexistentDepartment(String::from(department_name))),
        Some(department) => {
            let mut result: Vec<Employee> = department.0.iter().cloned().collect();
            result.sort_unstable();
            Ok(result)
        }
    }
}

fn get_employees_company(company: &Company) -> Vec<Employee> {
    let mut result = company
        .0
        .keys()
        .map(|department| get_employees_department(company, department).unwrap())
        .collect::<Vec<Vec<Employee>>>()
        .concat();
    result.sort_unstable();
    result
}

fn format_slice(slice: &[Employee]) -> Option<String> {
    match slice.len() {
        0 => None,
        _ => Some(
            slice
                .iter()
                .map(|employee| employee.0.to_string())
                .collect::<Vec<String>>()
                .join("n"),
        ),
    }
}

#[derive(PartialEq)]
enum GetInput {
    Print,
    Read,
    Parse,
}

#[derive(PartialEq)]
enum Mode {
    Greet,
    GetInput(GetInput),
    Quit,
}

struct State {
    mode: Mode,
    company: Company,
    input: String,
}

fn make_state() -> State {
    State {
        company: make_company(),
        mode: Mode::Greet,
        input: String::new(),
    }
}

fn print_flush(input: &str) -> Result<(), std::io::Error> {
    print!("{}", input);
    io::stdout().flush()
}

fn read_line(string: &mut String) -> Result<usize, std::io::Error> {
    string.clear();
    io::stdin().read_line(string)
}

// This assumes that department and employee names don't have whitespace.
fn process_input(state: &mut State) {
    let company = &mut state.company;
    match state.input.len() {
        0 => {
            state.mode = Mode::GetInput(GetInput::Print);
        }
        _ => {
            let tokens: Vec<&str> = state.input.split_whitespace().collect();
            match tokens.get(0) {
                None => {
                    state.mode = Mode::GetInput(GetInput::Print);
                }
                Some(token) => match *token {
                    "quit" => {
                        println!("Bye for now!");
                        state.mode = Mode::Quit;
                    }
                    command => {
                        match command {
                            "add-department" => match tokens[1..].len() {
                                0 => println!("Command "{}" requires at least 1 argument.", command),
                                _ => tokens[1..].iter().for_each(|element| match add_department(company, element) {
                                    Ok(_) => (),
                                    Err(error) => println!("Error: {:?}", error)
                                })
                            },
                            "add-employee" => match tokens[1..].len() {
                                0 ..= 1 => println!("Command "{}" requires at least 2 arguments.", command),
                                _ => match company.0.contains_key(tokens[1]) {
                                    true => tokens[2..].iter().for_each(|employee| match add_employee(company, tokens[1], Employee(employee.to_string())) {
                                        Ok(_) => (),
                                        Err(error) => println!("Error: {:?}", error)
                                    }),
                                    false => println!("Department "{}" doesn't exist.", tokens[1])
                                }
                            },
                            "get-employees" => match tokens[1..].len() {
                                0 => println!("Command "{}" requires at least 1 argument.", command),
                                _ => tokens[1..].iter().for_each(|department| match company.0.contains_key(*department) {
                                    true => match format_slice(&get_employees_department(company, department).unwrap()) {
                                        None => println!("Department "{}" has no employees.", department),
                                        Some(employees) => println!(
                                            "Employees in "{}":
{}", department, employees)
                                    },
                                    false => println!("Department "{}" doesn't exist.", department)
                                })
                            },
                            "get-employees-all" => match format_slice(&get_employees_company(company)) {
                                None => println!("Company has no employees."),
                                Some(employees) => println!(
                                    "Employees:
{}", employees)
                            },
                            "help" => println!(
                                "Commands:

add-department <department_name [department_name ...]>
  Add one or more departments to the company.

add-employee <department_name> <employee_name [employee_name ...]>
  Add one or more employees to a department.

get-employees <department_name [department_name ...]>
  Print a list of employees from one or more departments.

get-employees-all
  Print a list of all employees in the company.

help
  Print this message.

quit
  Quit."
                            ),
                            command => {
                                println!("Invalid command "{}".", command);
                            }
                        }
                        state.mode = Mode::GetInput(GetInput::Print);
                    }
                },
            }
        }
    }
}

fn state_machine(state: &mut State) {
    match &state.mode {
        Mode::Greet => {
            println!(
                "Database program.
Enter "help" for a list of commands."
            );
            state.mode = Mode::GetInput(GetInput::Print);
        }
        Mode::GetInput(get_input_step) => match get_input_step {
            GetInput::Print => {
                println!("Company: {:?}", state.company);
                match print_flush("> ") {
                    Ok(_) => {
                        state.mode = Mode::GetInput(GetInput::Read);
                    }
                    Err(error) => println!("Error {:?}", error),
                }
            }
            GetInput::Read => match read_line(&mut state.input) {
                Ok(_) => {
                    state.mode = Mode::GetInput(GetInput::Parse);
                }
                Err(error) => println!("Error: {:?}", error),
            },
            GetInput::Parse => process_input(state),
        },
        Mode::Quit => (),
    }
}

fn repl(function: fn(&mut State), state: &mut State) {
    while state.mode != Mode::Quit {
        function(state)
    }
}

fn main() {
    repl(state_machine, &mut make_state())
}

Ранее я пытался реорганизовать process_input функция, чтобы попытаться уменьшить вложенность, перемещая ее части в разные функции, но я столкнулся с проблемами заимствования, поэтому перестал пытаться. Я также не смог придумать хороший способ реализовать его, чтобы он принимал входные данные, использующие тот же формат, что и пример (например, «Добавить Амира в продажи»), поэтому я сделал это немного по-другому.

Я также не уверен, что использую Result введите правильный тип, и если структуры кортежей и перечисление, которые я передал им, являются хорошим способом обработки ошибок.

Я также чувствую, что некоторые из клонирования, которое я сделал, вероятно, не нужны, но я не уверен, как я могу их реорганизовать, поэтому было бы неплохо узнать, действительно ли это так, и как я мог бы провести рефакторинг. На самом деле, я думаю, было бы полезно знать, как я могу определить, когда уместно клонировать, а когда брать взаймы, поскольку я думаю, что я немного сомневаюсь в этой теме.

В связи с этим, как мне писать функции? Как правило, лучше изначально писать функции так, чтобы они клонировали свои аргументы, а затем попытаться реорганизовать клонирование позже или попытаться заимствовать все заранее, а затем попытаться исправить любые возникающие проблемы с проверкой заимствований и клонировать только в том случае, если я не могу их исправить?

В любом случае, я буду очень признателен за любую конструктивную критику и советы.

1 ответ
1

Рада снова тебя видеть, никоты 🙂

Представление данных

#[derive(Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
struct Employee(String);
#[derive(Debug)]
struct Department(HashSet<Employee>);
#[derive(Debug)]
struct Company(HashMap<String, Department>);

Структуры кортежей следует использовать только тогда, когда смысл полей ясен, особенно когда структура представляет собой не более чем тонкую оболочку вокруг внутреннего значения, которая настраивает определенные свойства уровня типа — std::num::Wrapping и std::cmp::Reverse хорошие примеры. В этом случае я бы назвал поля name,
employees, и departments, соответственно.

Department и Company могу иметь derive(Default). Мне не нравится то что Employee орудия Ordоднако, поскольку имена — не единственный логичный способ сравнения сотрудников. Вместо этого я предпочитаю явно указывать этот порядок при сортировке (через
sort_unstable_by_key, среди других методов). Полагаю, это нормально — выводить их из прагматических соображений.

Код организации

Некоторые из ваших функций должны быть преобразованы в методы и связанные функции:

ИзК
make_companyCompany::new
add_departmentCompany::add_department
add_employeeCompany::get_department_mut + Department::add_employee

и т.д., с обычными аргументами, преобразованными в self по мере необходимости.
Company::new, например, могут быть реализованы с использованием Default
получить:

impl Company {
    fn new() -> Self {
        Self::default()
    }
}

Поток управления

Ранее я пытался реорганизовать process_input функция, чтобы попытаться уменьшить вложенность, перемещая ее части в разные функции, но я столкнулся с проблемами заимствования, поэтому перестал пытаться.

Это прискорбно — разделение его на более мелкие функции определенно выполнимо и рекомендуется.

В целом ваш подход выглядит сложным — я не ожидал увидеть машину состояний. Попробуйте более простой подход. Я представляю
Command структура, которая анализирует первый токен и отправляет его различным методам.

См. Переписанную версию в конце этого поста для вдохновения.

обработка ошибок

#[derive(Debug)]
struct ExistingDepartment(String);

Полноценный тип ошибки будет реализовывать Debug, Display, и
Error:

use std::fmt;

#[derive(Debug)]
struct ExistingDepartment(String);

impl fmt::Display for ExistingDepartment {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        format_args!("the department `{:?}` already exists", self.0).fmt(f)
    }
}

impl std::error::Error for ExistingDepartment {}

В качестве альтернативы thiserror ящик может помочь. Кроме того, чаще встречаются типы ошибок. enums, как AddEmployeeError:

use thiserror::Error;

#[derive(Debug, Error)]
enum AddEmployeeError {
    #[error("the department `{0}` doesn't exist")]
    NonexistentDepartment(String),
    #[error("the employee `{}` already exists", .0.0)]
    ExistingEmployee(Employee),
}

Это, наверное, перебор для простой программы.

HashMap вставка

fn add_department(
    company: &mut Company,
    department: &str,
) -> Result<(), ExistingDepartment> {
    let department_name = String::from(department);
    match company.0.contains_key(&department_name) {
        true => Err(ExistingDepartment(department_name)),
        false => {
            company.0.insert(
                department_name,
                Department(HashSet::<Employee>::new()),
            );
            Ok(())
        }
    }
}

company.0 ищется дважды — один раз в contains_key, однажды в
insert. В Entry интерфейс является предпочтительным решением:

use std::collections::hash_map::Entry;

match company.0.entry(department_name) {
    Entry::Occupied(entry) => {
        Err(ExistingDepartment(entry.key().to_owned()))
    }
    Entry::Vacant(entry) => {
        entry.insert(Department::new());
        Ok(())
    }
}

Вместо клонирования department внутри функции принимайте аргумент по значению, чтобы вызывающая сторона могла передавать значения, которые им больше не нужны. Если вставка не удалась, право собственности может быть возвращено вызывающей стороне в типе ошибки. (К сожалению, похоже, что ключ сбрасывается, если запись занята, поэтому мне пришлось использовать to_owned в таком случае.)

Клонирование

Я также чувствую, что некоторые из клонирования, которое я сделал, вероятно, не нужны, но я не уверен, как я могу их реорганизовать, поэтому было бы неплохо узнать, действительно ли это так, и как я мог бы провести рефакторинг. На самом деле, я думаю, было бы полезно знать, как я могу определить, когда уместно клонировать, а когда брать взаймы, поскольку я думаю, что я немного сомневаюсь в этой теме.

В принципе, мы должны клонировать, когда это логически целесообразно, и избегать клонирования в противном случае. Все сводится к семантике.

На практике можно время от времени спамить .clone() когда производительность не имеет большого значения — в конце концов, это то, что многие другие языки делают автоматически под капотом.

Возьмем два явных clones в вашем коде, например:

// in add_employee

match department.0.insert(employee.clone()) {
    true => Ok(()),
    false => Err(AddEmployeeError::ExistingEmployee(employee)),
}

Здесь clone можно избежать, если вы используете replace вместо
insert. (На самом деле, я не уверен, почему HashSet::insert возвращает
bool а не Result<(), T> на первом месте.)

// in get_employees_department
let mut result: Vec<Employee> = department.0.iter().cloned().collect();
result.sort_unstable();
Ok(result)

Теоретически вместо этого можно работать со ссылками, где
Vec<&Employee> возвращается. (Вы можете вернуть ссылку на
Vec сотрудников и оставьте сортировку вызывающей стороне, но это не решает проблему.)

В связи с этим, как мне писать функции? Как правило, лучше изначально писать функции так, чтобы они клонировали свои аргументы, а затем попытаться реорганизовать клонирование позже или попытаться заимствовать все заранее, а затем попытаться исправить любые проблемы с проверкой заимствований, которые возникают и клонировать только в том случае, если я не могу их исправить?

Лично я обычно сначала использую семантику — клонирование и заимствование в зависимости от семантики. По мере того, как вы приобретете больше опыта в управлении памятью в Rust, вы почувствуете себя более комфортно, переводя свои внутренние мысли в код без того, чтобы программа проверки заимствований была врагом (по крайней мере, из моего опыта).

flat_map

fn get_employees_company(company: &Company) -> Vec<Employee> {
    let mut result = company
        .0
        .keys()
        .map(|department| {
            get_employees_department(company, department).unwrap()
        })
        .collect::<Vec<Vec<Employee>>>()
        .concat();
    result.sort_unstable();
    result
}

Вы могли догадаться, что использование вложенных Vecs запутан и неэффективен — flat_map приходит на помощь.

Форматирование коллекций

fn format_slice(slice: &[Employee]) -> Option<String> {
    match slice.len() {
        0 => None,
        _ => Some(
            slice
                .iter()
                .map(|employee| employee.0.to_string())
                .collect::<Vec<String>>()
                .join("n"),
        ),
    }
}

format от itertools crate упрощает функцию и устраняет ненужные выделения.

Проверка пустых списков тоже здесь неуместна.

use декларации

use std::collections::HashMap;
use std::collections::HashSet;
use std::io;
use std::io::Write;

Вы можете конденсировать use декларации:

use std::{
    collections::{HashMap, HashSet},
    io::{self, Write},
};

куда self (в пути) преобразуется в текущий модуль.

Разное

Я также не мог придумать хороший способ реализовать его, чтобы он принимал входные данные, использующие тот же формат, что и пример (например, «Добавить Амира в продажи»), поэтому я сделал это немного по-другому.

Это нормально — формулировка упражнения, похоже, не требует определенного синтаксиса.

Я также не уверен, что использую Result введите правильный тип, и если структуры кортежей и перечисление, которые я передал им, являются хорошим способом обработки ошибок.

См. Разделы «Представление данных» и «Обработка ошибок» выше.

Переписанная версия

Вот и обещанная переписанная версия. Я сделал некоторые упрощения для простоты демонстрации. (Используемые внешние ящики: indoc itertools thiserror)

src / main.rs

pub mod data;
pub mod repl;

use repl::Repl;

fn main() {
    Repl::new().run()
}

src / data.rs

use {
    itertools::Itertools,
    std::collections::{hash_map, HashMap, HashSet},
    thiserror::Error,
};

#[derive(Clone, Debug, Default)]
pub struct Company {
    departments: HashMap<String, Department>,
}

impl Company {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn add_department(&mut self, department: String) -> Result<()> {
        use hash_map::Entry;

        match self.departments.entry(department) {
            Entry::Occupied(entry) => {
                Err(Error::ExistingDepartment(entry.key().to_owned()))
            }
            Entry::Vacant(entry) => {
                entry.insert(Department::new());
                Ok(())
            }
        }
    }

    pub fn add_employee(
        &mut self,
        department: &str,
        employee: String,
    ) -> Result<()> {
        match self
            .departments
            .get_mut(department)
            .ok_or_else(|| Error::InvalidDepartment(department.to_owned()))?
            .employees
            .replace(employee)
        {
            Some(employee) => Err(Error::ExistingEmployee(employee)),
            None => Ok(()),
        }
    }

    pub fn get_departments(&self) -> Vec<String> {
        self.departments.keys().cloned().sorted_unstable().collect()
    }

    pub fn get_all_employees(&self) -> Vec<String> {
        self.departments
            .values()
            .flat_map(|department| department.employees.iter().cloned())
            .sorted_unstable()
            .collect()
    }

    pub fn get_employees(&self, department: &str) -> Result<Vec<String>> {
        Ok(self
            .departments
            .get(department)
            .ok_or_else(|| Error::InvalidDepartment(department.to_owned()))?
            .employees
            .iter()
            .cloned()
            .sorted_unstable()
            .collect())
    }
}

#[derive(Clone, Debug, Default)]
pub struct Department {
    employees: HashSet<String>,
}

impl Department {
    pub fn new() -> Self {
        Self::default()
    }
}

#[derive(Debug, Error)]
pub enum Error {
    #[error("department `{0}` already exists")]
    ExistingDepartment(String),
    #[error("employee `{0}` already exists")]
    ExistingEmployee(String),
    #[error("invalid department `{0}`")]
    InvalidDepartment(String),
}

type Result<T> = std::result::Result<T, Error>;

src / repl.rs

use {
    crate::data, indoc::indoc, itertools::Itertools, std::ops::RangeInclusive,
    thiserror::Error,
};

#[derive(Debug, Default)]
pub struct Repl {
    data: data::Company,
}

impl Repl {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn run(mut self) {
        loop {
            let directive = input("> ");
            let tokens: Vec<&str> = directive.split_whitespace().collect();

            let (command, args) = match tokens.split_first() {
                Some((command, args)) => (*command, args),
                None => continue,
            };

            let result = match command {
                "add_department" => self.add_department(args),
                "add_employee" => self.add_employee(args),
                "get_departments" => self.get_departments(args),
                "get_employees" => self.get_employees(args),
                "help" => {
                    eprint!("n{}nn", HELP_TEXT);
                    Ok(())
                }
                "quit" => return,
                _ => Err(Error::InvalidCommand(command.to_owned())),
            };

            if let Err(error) = result {
                eprintln!("! Error: {}", error);
            }
        }
    }

    fn add_department(&mut self, args: &[&str]) -> Result<()> {
        Self::check_arg_count(args, 1)?;

        self.data
            .add_department(args[0].to_owned())
            .map_err(|error| error.into())
    }

    fn add_employee(&mut self, args: &[&str]) -> Result<()> {
        Self::check_arg_count(args, 2)?;

        self.data
            .add_employee(args[0], args[1].to_owned())
            .map_err(|error| error.into())
    }

    fn get_departments(&self, args: &[&str]) -> Result<()> {
        Self::check_arg_count(args, 0)?;

        let departments = self.data.get_departments();
        println!("{}", departments.iter().format("n"));

        Ok(())
    }

    fn get_employees(&self, args: &[&str]) -> Result<()> {
        let employees = match &args[..] {
            [] => self.data.get_all_employees(),
            [department] => self.data.get_employees(department)?,
            _ => {
                return Err(Error::ArgumentCountRange {
                    expected: 0..=1,
                    found: args.len(),
                })
            }
        };
        println!("{}", employees.iter().format("n"));

        Ok(())
    }

    fn check_arg_count(args: &[&str], expected: usize) -> Result<()> {
        let arg_count = args.len();

        if arg_count == expected {
            Ok(())
        } else {
            Err(Error::ArgumentCount {
                expected,
                found: arg_count,
            })
        }
    }
}

#[derive(Debug, Error)]
enum Error {
    #[error("expected {expected} arguments, found {found}")]
    ArgumentCount { expected: usize, found: usize },
    #[error(
        "expected {} to {} arguments, found {found}",
        .expected.start(), .expected.end(),
    )]
    ArgumentCountRange {
        expected: RangeInclusive<usize>,
        found: usize,
    },
    #[error(transparent)]
    Database(#[from] data::Error),
    #[error("invalid command `{0}`")]
    InvalidCommand(String),
}

type Result<T> = std::result::Result<T, Error>;

fn input(prompt: &str) -> String {
    eprint!("{}", prompt);

    let mut input = String::new();
    std::io::stdin()
        .read_line(&mut input)
        .expect("cannot read input");

    input
}

const HELP_TEXT: &str = indoc! { "
    add_department <department_name>
        Add a department to the company.

    add_employee <department_name> <employee_name>
        Add an employee to a department.

    get_departments
        Print a list of all departments.

    get_employees [<department_name>]
        Print a list of all employees or employees from a department.

    help
        Print this message.

    quit
        Quit.
" };

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *