I'm working my way through the Rust Book:
Using a hash map and vectors, create a text interface to allow a user to add employee names to a department in the 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.
use std::collections::HashMap;
#[derive(Debug)]
enum Command {
Add(String, String),
List,
Unknown
}
fn command_filter(input: String) -> Command {
let mut command_string = input.split_whitespace();
match command_string.next() {
Some("Add") => {
match (command_string.next(), command_string.last()){
(Some(name), Some(dept)) => Command::Add(name.to_string(), dept.to_string()),
_ => Command::Unknown
}
},
Some("List") => Command::List,
_ => Command::Unknown
}
}
fn process_command(command: Command, employee_map: &mut HashMap<String, String>) {
match command {
Command::Add(name, dept) => {
employee_map.insert(name, dept);
()
},
Command::List => {
for (name, dept) in employee_map {
println!("Name: {} Dept: {}", name, dept);
};
},
Command::Unknown => println!("Unknown command!"),
};
}
fn main() {
let mut employee: HashMap<String, String> = HashMap::new();
let command = command_filter(String::from("Add John to Eng"));
process_command(command, &mut employee);
let command = command_filter(String::from("Add Devon to Bio"));
process_command(command, &mut employee);
let command = command_filter(String::from("List"));
process_command(command, &mut employee);
}
As with most devs starting to learn Rust I am finding difficulty with borrowing. I do not come from a C/C++ background and so I'm also finding pointers versus actual object a little tricky too.
Some specific thoughts:
- I am suspicious that match statements inside match statements is not good practice.
- The
Command::Add
branch inprocess_command
feels a little hacky with the()
return. Edit: After thinking more about this problem, I think if I wrapped the branch responses inOk()
then I could return aResult
from that function.
Do you have any feedback?
-
\$\begingroup\$ To get better feedback I suggest you add some comments of your own. What do you think might be lacking? Are there things you're not quite sure of? Which part did you find the hardest to implement? \$\endgroup\$Rene Saarsoo– Rene Saarsoo2018年05月03日 12:30:45 +00:00Commented May 3, 2018 at 12:30
2 Answers 2
General
- It feels strange to have functions just hanging around; why not create some methods on types? I moved
command_filter
toFrom
and movedprocess_command
toCommand::process
. The latter would probably make more sense as aprocess
method on aEmployeeMap
type, though.
Command
It's unclear what the two values are for
Command::Add
- use named fields instead.Could contain
&str
s since they live shorter than the string they parsed.Ah I see, and I would only need a new string (and hence allocation?) if the lifetime of the string was shorter than the slice?
You would need to use a
String
(which is an allocation, yes) if theCommand
needs to contain the string value longer than the input string value is available. In this case, all of your input strings are hard-coded literals so they are available "forever". In a different case, you might have read some user input into anotherString
. So long as theCommand
went out of scope before thatString
moved, it would be good.Lifetimes are hard
Yes... and no. The thing is that languages like C or C++ have the exact same problems, but the languages don't stop you from doing The Wrong Thing. Languages with a garbage collector don't allow you to achieve the same level of efficiency. Rust's lifetimes allow you to be efficient while preventing memory unsafety.
command_filter
Could take a
&str
as an argument since it doesn't make use of the allocation of theString
.This advances the
split_whitespace
iterator before checking the result of a previousnext
call:match (command_string.next(), command_string.last())
This isn't guaranteed to do what you want:
calling
next()
again may or may not eventually start returningSome(Item)
again at some point.You should use
Iterator::fuse
.Could choose to flatten out the
match
to one level, but this would require advancing the iterator when you don't need to. Nested matches aren't inherently bad.
process_command
- You don't need to say
()
as the return expression, that's the default value for a statement (lines ending in;
).
main
There's no reason to specify the type of
employees
, the compiler can infer it.employees
is a collection, so it should be a plural noun.
use std::collections::HashMap;
#[derive(Debug)]
enum Command<'a> {
Add { name: &'a str, dept: &'a str },
List,
Unknown,
}
impl<'a> From<&'a str> for Command<'a> {
fn from(input: &'a str) -> Self {
let mut command_string = input.split_whitespace().fuse();
match command_string.next() {
Some("Add") => {
match (command_string.next(), command_string.last()) {
(Some(name), Some(dept)) => Command::Add { name, dept },
_ => Command::Unknown,
}
},
Some("List") => Command::List,
_ => Command::Unknown,
}
}
}
impl<'a> Command<'a> {
fn process(self, employees: &mut HashMap<String, String>) {
match self {
Command::Add { name, dept } => {
employees.insert(name.to_owned(), dept.to_owned());
}
Command::List => {
for (name, dept) in employees {
println!("Name: {} Dept: {}", name, dept);
}
}
Command::Unknown => println!("Unknown command!"),
};
}
}
fn main() {
let mut employees = HashMap::new();
let command = Command::from("Add John to Eng");
command.process(&mut employees);
let command = Command::from("Add Devon to Bio");
command.process(&mut employees);
let command = Command::from("List");
command.process(&mut employees);
}
-
\$\begingroup\$ Wow, thank you so much! General: I forgot that you can implement methods on enums. That makes a ton of sense. Command: 1. Nice! Didn't know that could be done/was valid syntax. 2. Ah I see, and I would only need a new string (and hence allocation?) if the lifetime of the string was shorter than the slice? Lifetimes are hard. command_filter: 1. Does this mean memory allocation? 2. I don't understand, I thought I created the Iterator and immediately checked the
next
value - how doesfuse
protect me? process_command: Of course, whoops. \$\endgroup\$Mungo Dewar– Mungo Dewar2018年05月08日 13:35:56 +00:00Commented May 8, 2018 at 13:35 -
\$\begingroup\$ @MungoDewar updated to address some feedback. \$\endgroup\$Shepmaster– Shepmaster2018年05月08日 21:44:23 +00:00Commented May 8, 2018 at 21:44
-
1\$\begingroup\$ Wanted to share updated code. I've included the comments listed here and have also hidden the Hashmap implementation behind the App struct. github.com/DewarM/rust_manage_employee \$\endgroup\$Mungo Dewar– Mungo Dewar2018年05月30日 21:37:07 +00:00Commented May 30, 2018 at 21:37
-
1\$\begingroup\$ @MungoDewar I believe you are also encouraged to provide your own answer here, expanding on that comment and explaining why you made the changes you did. \$\endgroup\$Shepmaster– Shepmaster2018年05月31日 02:30:03 +00:00Commented May 31, 2018 at 2:30
Hide hashmap implementation
The code can be improved by removing the requirement for the user to operate on a hashmap or to even know that a hashmap is used.
How? This should be hidden from the user by an "interface".
Why? This is a good coding practice as it would allow the underlying structure/code to change (for example, the hashmap could be swapped for a db) but there would be no API changes to the interface.
I have implemented this change and the changes suggested by @Shepmaster here: github