はじめに
興味本位で The Rust Programming Language を読んでいました。8章3節まで読んだところ、面白そうな問題があったので、Rust 始めて 1~2 日の素人が挑戦してみました。問題は以下の通りです。
ハッシュマップとベクタを使用して、ユーザに会社の部署に雇用者の名前を追加させられるテキストインターフェイスを作ってください。 例えば、"Add Sally to Engineering"(開発部門にサリーを追加)や"Add Amir to Sales"(販売部門にアミールを追加)などです。 それからユーザに、ある部署にいる人間の一覧や部署ごとにアルファベット順で並べ替えられた会社の全人間の一覧を扱わせてあげてください。
環境
プログラムは Linux 環境で行いました。
バージョン | |
---|---|
OS | Ubuntu 20.04.3 |
cargo | 1.56.0 (4ed5d137b 2021-10-04) |
rustc | 1.56.1 (59eed8a2a 2021-11-01) |
プログラム作成
端末からの入力文字列を取得
テキストインターフェイスを作成するわけですから、まずは端末に入力された文字列を読み取る部分を作ってみましょう。これは 2章 数当てゲームをプログラムする が参考になります。
use std::io;
fn main() {
loop {
println!("Input Command:");
let mut input_value = String::new();
io::stdin().read_line(&mut input_value)
.expect("Failed to read");
println!("{}", input_value);
}
}
実行してみると、入力したものがちゃんとプログラムで受け取って、println!
文で表示されていることがわかります。現状プログラムを終了する方法がないので、Cntl + C で終了します。
cargo run
Input Command:
Add Sally to Engineering
Add Sally to Engineering
Input Command:
^C
入力文字を分割
次に入力文字列をスペースで分割し、ベクター型のコレクションに代入してみましょう。
use std::io;
fn main() {
loop {
println!("Input Command:");
let mut input_value = String::new();
io::stdin().read_line(&mut input_value)
.expect("Failed to read");
let v: Vec<&str> = input_value.trim().split_whitespace().collect();
println!("{:?}", v);
}
}
split_whitespace()
はスペースごとに単語を切り、それぞれ &str
型で返します。ここで注意しなければいけないのは、input_value
と v
の各要素の実体が同じヒープ領域を指しているということです。このことは上のプログラムの println! 文の後に以下を追加すると確認することができます。
println!("&input: {:?}", input_value.as_ptr());
println!(" &v[0]: {:?}", v[0].as_ptr());
試しに実行してみましょう。
cargo run
Input Command:
Add Sally to Engineering
["Add", "Sally", "to", "Engineering"]
&input: 0x55a4bb93fae0
&v[0]: 0x55a4bb93fae0
Input Command:
^C
これは 4章1節 所有権とは? に掲載されている図4.2のような状態であることを意味しています。ここまでのプログラムでは特に気にしなくてもいいのですが、以降でこの点が重要な意味を持ちますので頭の片隅に入れておいてください。
入力コマンドに従った処理の実装
実装方針
まずは次の3種類のコマンドを実装してみましょう。
コマンド | 引数の数 | コマンド例 | コマンドの意味 |
---|---|---|---|
Add | 3 | Add Sally to Engineering | Sally を Engineering に追加する |
0 | 会社の全従業員の一覧を表示する | ||
1 | Print Engineering | Engineering 部署の従業員一覧をアルファベット順に表示する | |
Quit | 0 | Quit | 終了する |
まずは側だけ作ってみましょう。
v
の一番目の要素はコマンドに相当しますから、command
に代入しておきます。そして match 制御フロー演算子を使い、command の値によって処理を分岐させます。Add の場合、2番目の値が従業員名、4番目の値が部署名なので、それぞれ分かりやすい名前の変数に代入しておきます。Quit の場合にはループから抜けます。これでプログラムを終了させることができますね。Print の場合に出力するものがまだないので、単に println! 文を置いておきます。また上記以外のコマンド等が最初の文字列として入力された時にはメッセージを出力して、ループ処理を続行させることにします。
use std::io;
fn main() {
loop {
println!("Input Command:");
let mut input_value = String::new();
io::stdin().read_line(&mut input_value)
.expect("Failed to read");
let v: Vec<&str> = input_value.trim().split_whitespace().collect();
let command = v[0];
match command {
"Add" => {
if v.len() != 4 {
println!("Add command requires 3 arguments");
continue;
}
let employee_name = v[1];
let division_name = v[3];
println!("Added {} to {}", employee_name, division_name);
},
"Quit" => { break; },
"Print" => {
println!("Print all employees.");
},
_ => {
println!("Unknown command is specified.");
},
}
}
}
さて実行してみましょう。
cargo run
Input Command:
Add foo
Add command requires 3 arguments
Input Command:
Add Sally to Engineering
Added Sally to Engineering
Input Command:
Print
Print all employees.
Input Command:
Quit
大丈夫そうですね。では、入力された従業員名と部署名をハッシュマップで管理するように追加していきましょう。ここでハッシュマップのキーには部署名を、値には複数の従業員名を要素とするベクター型のコレクションを入れることにします。
&str 型による失敗
use std::io;
use std::collections::HashMap;
fn main() {
let mut employees: HashMap<&str, Vec<&str>> = HashMap::new();
loop {
println!("Input Command:");
let mut input_value = String::new();
io::stdin().read_line(&mut input_value)
.expect("Failed to read");
let v: Vec<&str> = input_value.trim().split_whitespace().collect();
let command = v[0];
match command {
"Add" => {
if v.len() != 4 {
println!("Add command requires 3 arguments");
continue;
}
let employee_name = v[1];
let division_name = v[3];
let v = employees.entry(division_name).or_insert(vec![]);
v.push(employee_name);
println!("Added {} to {}", employee_name, division_name);
},
"Quit" => { break; },
"Print" => {
println!("Print all employees.");
},
_ => {
println!("Unknown command is specified.");
},
}
}
}
コンパイルしてみましょう。
cargo run
error[E0597]: `input_value` does not live long enough
--> src/main.rs:14:28
|
14 | let v: Vec<&str> = input_value.trim().split_whitespace().collect();
| ^^^^^^^^^^^ borrowed value does not live long enough
...
25 | let v = employees.entry(division_name).or_insert(vec![]);
| --------- borrow later used here
...
40 | }
| - `input_value` dropped here while still borrowed
失敗しましたね。input_value
は loop ブロック内でのみ意味を持つ変数です。したがってループブロックの最後の括弧で drop が実行されてしまいます。これに対して、input_value と同じ実体を見ている v(division_name も同じです)も input_value が drop された時に実体を失います。そのため「drop される変数は借用し続けられないですよ」と怒られてるわけですね。さらに &str
には Copy トレイトが存在しないため複製することもできません。したがってハッシュマップのキーや値に &str
型を使ったのが間違いだったことになります。
String 型に修正
use std::io;
use std::collections::HashMap;
fn main() {
let mut employees: HashMap<String, Vec<String>> = HashMap::new();
loop {
println!("Input Command:");
let mut input_value = String::new();
io::stdin().read_line(&mut input_value)
.expect("Failed to read");
let v: Vec<&str> = input_value.trim().split_whitespace().collect();
let command = v[0];
match command {
"Add" => {
if v.len() != 4 {
println!("Add command requires 3 arguments");
continue;
}
let employee_name = v[1].to_string();
let division_name = v[3].to_string();
let v = employees.entry(division_name).or_insert(vec![]);
v.push(employee_name);
println!("Added {} to {}", employee_name, division_name);
},
"Quit" => { break; },
"Print" => {
println!("Print all employees.");
},
_ => {
println!("Unknown command is specified.");
},
}
}
}
cargo run
error[E0382]: borrow of moved value: `employee_name`
--> src/main.rs:27:44
|
23 | let employee_name = v[1].to_string();
| ------------- move occurs because `employee_name` has type `String`, which does not implement the `Copy` trait
...
26 | v.push(employee_name);
| ------------- value moved here
27 | println!("Added {} to {}", employee_name, division_name);
| ^^^^^^^^^^^^^ value borrowed here after move
error[E0382]: borrow of moved value: `division_name`
--> src/main.rs:27:59
|
24 | let division_name = v[3].to_string();
| ------------- move occurs because `division_name` has type `String`, which does not implement the `Copy` trait
25 | let v = employees.entry(division_name).or_insert(vec![]);
| ------------- value moved here
26 | v.push(employee_name);
27 | println!("Added {} to {}", employee_name, division_name);
| ^^^^^^^^^^^^^ value borrowed here after move
またエラーが出ましたね。これは entry
や push
メソッドを呼ぶ際に引数の所有権が移動してしまったため、後続の println! 文で借用できないよと怒られているわけです。たとえば entry について見てみると、確かに引数 key
は所有権の移動のようです(明示的に記載されていませんが、次の get
では借用と記載されているので、T
で受け取るのか、&T
で受け取るのかで違うということがわかります 1)。
27行目の println! 文を削除すれば問題は解消しますが、何も出力しないのも不親切なので、ここは clone()
を呼んで複製したものを渡すことにします。
String 型を複製してメソッド呼び出し
use std::io;
use std::collections::HashMap;
fn main() {
let mut employees: HashMap<String, Vec<String>> = HashMap::new();
loop {
println!("Input Command:");
let mut input_value = String::new();
io::stdin().read_line(&mut input_value)
.expect("Failed to read");
let v: Vec<&str> = input_value.trim().split_whitespace().collect();
let command = v[0];
match command {
"Add" => {
if v.len() != 4 {
println!("Add command requires 3 arguments");
continue;
}
let employee_name = v[1].to_string();
let division_name = v[3].to_string();
let v = employees.entry(division_name.clone()).or_insert(vec![]);
v.push(employee_name.clone());
println!("Added {} to {}", employee_name, division_name);
},
"Quit" => { break; },
"Print" => {
println!("Print all employees.");
},
_ => {
println!("Unknown command is specified.");
},
}
}
}
これでコンパイルできるようになりました。
既に登録された従業員の除外
このままだと既に登録された従業員が同じ部署に何度も登録されてしまうので、除外する処理も入れてみましょう。ついでに Print でハッシュマップの内容が出力されるようにしておきます。
use std::io;
use std::collections::HashMap;
fn main() {
let mut employees: HashMap<String, Vec<String>> = HashMap::new();
loop {
println!("Input Command:");
let mut input_value = String::new();
io::stdin().read_line(&mut input_value)
.expect("Failed to read");
let v: Vec<&str> = input_value.trim().split_whitespace().collect();
let command = v[0];
match command {
"Add" => {
if v.len() != 4 {
println!("Add command requires 3 arguments");
continue;
}
let employee_name = v[1].to_string();
let division_name = v[3].to_string();
let v = employees.entry(division_name.clone()).or_insert(vec![]);
match v.iter().position(|x| *x == employee_name) {
Some(_) => {
println!("{} is already registered.", employee_name);
},
None => {
v.push(employee_name.clone());
println!("Added {} to {}", employee_name, division_name);
}
}
},
"Quit" => { break; },
"Print" => {
println!("{:?}", employees);
},
_ => {
println!("Unknown command is specified.");
},
}
}
}
それでは実行してみましょう。
cargo run
Input Command:
Add Sally to Engineering
Added Sally to Engineering
Input Command:
Add Sally to Engineering
Sally is already registered.
Input Command:
Print
{"Engineering": ["Sally"]}
Input Command:
Quit
二重登録されないようになりました。Add コマンドについては満足のいくものになった気がします。
Print コマンドの実装
要件は以下の2つになります。
- ある部署にいる人間の一覧
- 部署ごとにアルファベット順で並べ替えられた会社の全人間の一覧
use std::io;
use std::collections::HashMap;
fn main() {
let mut employees: HashMap<String, Vec<String>> = HashMap::new();
loop {
println!("Input Command:");
let mut input_value = String::new();
io::stdin().read_line(&mut input_value)
.expect("Failed to read");
let v: Vec<&str> = input_value.trim().split_whitespace().collect();
let command = v[0];
match command {
"Add" => {
if v.len() != 4 {
println!("Add command requires 3 arguments");
continue;
}
let employee_name = v[1].to_string();
let division_name = v[3].to_string();
let v = employees.entry(division_name.clone()).or_insert(vec![]);
match v.iter().position(|x| *x == employee_name) {
Some(_) => {
println!("{} is already registered.", employee_name);
},
None => {
v.push(employee_name.clone());
println!("Added {} to {}", employee_name, division_name);
}
}
},
"Quit" => { break; },
"Print" => {
if v.len() > 2 {
println!("Too many arguments.");
continue;
}
println!("----------------------------------------");
if v.len() == 1 {
for (division_name, members) in &employees {
println!("{}:", division_name);
for employee_name in members {
println!(" {}", employee_name);
}
}
} else if v.len() == 2 {
let division_name = v[1].to_string();
match employees.get(&division_name) {
Some(v) => {
let mut vv = v.clone();
vv.sort();
println!("{}:", division_name);
for employee_name in vv {
println!(" {}", employee_name);
}
}
None => {
println!("{} does not have the specified member", division_name);
}
}
}
println!("----------------------------------------");
},
_ => {
println!("Unknown command is specified.");
},
}
}
}
Print の実装はちょっと複雑になってしまいました。全従業員の一覧を表示する部分(引数がない)については、二重の for
文で println! を実行するだけなので簡単です。一方、指定した部署に所属している従業員を アルファベット順に 表示するのはちょっと面倒です。というのも、sort()
メソッドはレシーバを変更してしまうので、ハッシュマップを変更させないように、一時的にVec型コレクションを作って、そちらをソートするようにしています。また指定した部署名が存在しない場合の対応も入れてみました。
それでは実行してみましょうか。
cargo run
Input Command:
Add Sally to Engineering
Added Sally to Engineering
Input Command:
Add Amir to Sales
Added Amir to Sales
Input Command:
Add George to Sales
Added George to Sales
Input Command:
Add Aaron to Sales
Added Aaron to Sales
Input Command:
Print
----------------------------------------
Sales:
Amir
George
Aaron
Engineering:
Sally
----------------------------------------
Input Command:
Print Sales
----------------------------------------
Sales:
Aaron
Amir
George
----------------------------------------
Input Command:
Print Corporate
----------------------------------------
Corporate does not have the specified member
----------------------------------------
Input Command:
Quit
想定通り動いているようですね。
おまけ
Delete コマンドの実装
指定部署から従業員を削除するコマンドも実装してみましょう。
コマンド | 引数の数 | コマンド例 | コマンドの意味 |
---|---|---|---|
Delete | 3 | Delete Sally from Engineering | Sally を Engineering から削除する |
use std::io;
use std::collections::HashMap;
fn main() {
let mut employees: HashMap<String, Vec<String>> = HashMap::new();
loop {
println!("Input Command:");
let mut input_value = String::new();
io::stdin().read_line(&mut input_value)
.expect("Failed to read");
let v: Vec<&str> = input_value.trim().split_whitespace().collect();
let command = v[0];
match command {
"Add" => {
if v.len() != 4 {
println!("Add command requires 3 arguments");
continue;
}
let employee_name = v[1].to_string();
let division_name = v[3].to_string();
let v = employees.entry(division_name.clone()).or_insert(vec![]);
match v.iter().position(|x| *x == employee_name) {
Some(_) => {
println!("{} is already registered.", employee_name);
},
None => {
v.push(employee_name.clone());
println!("Added {} to {}", employee_name, division_name);
}
}
},
"Delete" => {
if v.len() != 4 {
println!("Delete command requires 3 arguments");
continue;
}
let employee_name = v[1].to_string();
let division_name = v[3].to_string();
match employees.get_mut(&division_name) {
Some(v) => {
match v.iter().position(|x| *x == employee_name) {
Some(idx) => {
v.remove(idx);
if v.len() == 0 {
employees.remove(&division_name);
}
}
None => {
println!("Specified member is not found.");
}
}
}
None => {
println!("{} does not have any member", division_name);
}
}
},
"Quit" => { break; },
"Print" => {
if v.len() > 2 {
println!("Too many arguments.");
continue;
}
println!("----------------------------------------");
if v.len() == 1 {
for (division_name, members) in &employees {
println!("{}:", division_name);
for employee_name in members {
println!(" {}", employee_name);
}
}
} else if v.len() == 2 {
let division_name = v[1].to_string();
match employees.get(&division_name) {
Some(v) => {
let mut vv = v.clone();
vv.sort();
println!("{}:", division_name);
for employee_name in vv {
println!(" {}", employee_name);
}
}
None => {
println!("{} does not have the specified member", division_name);
}
}
}
println!("----------------------------------------");
},
_ => {
println!("Unknown command is specified.");
},
}
}
}
必要な要素は Add コマンドで使っているのでそれほど難しいことはないと思います。新たに追加された部分は、コレクションの要素が空になった場合、ハッシュマップから該当するキーも削除するところになります。
cargo run
Input Command:
Add Sally to Engineering
Added Sally to Engineering
Input Command:
Print
----------------------------------------
Engineering:
Sally
----------------------------------------
Input Command:
Delete Sally from Engineering
Input Command:
Print
----------------------------------------
----------------------------------------
Input Command:
Quit
さいごに
初めて書いた Rust プログラムなので、あまりできの良いものじゃないかもしれません。またモジュール化することなく、main に全てを押し込んだひどいプログラムなので、次回はモジュール化してすっきりさせようかと思います。
このプログラムを作るにあたって、所有権と借用 についてはだいぶ悩みました。他のプログラミング言語にはない概念なので、慣れるまでにはまだ時間がかかりそうです。
-
4章1節の所有権と関数 にちゃんと記載されていますね。 ↩