お題
以前、1つのお題に対して、いくつかのプログラミング言語で実装してみるという試みをした。
https://qiita.com/sky0621/items/32c87aed41cb1c3c67ff
また、その改善編として、言語別にKeyValueストアもどきを作ってみた。
例えば、以下。
今回はRustを追加。
前提
- Rustについてはビギナーレベル(※)の人間が書いています。
※ 以下のチュートリアルを第18章まで写経してみたレベル
https://doc.rust-jp.rs/book/second-edition/
- Rustで書いてみたという記事であり、Rustについていろいろ説明したものではないです。(そもそもまだ説明なんてできるレベルじゃない。)
- 別の言語と比較して、書きっぷりにちょっとした気付きくらいはあるかもしれません。
試行Index
- 第1回:簡単なツール作成を通して各プログラミング言語を比較しつつ学ぶ
- 第2回:【改善編】簡単なツール作成を通してRubyを学ぶ
- 第3回:【改善編】簡単なツール作成を通してPython3を学ぶ
- 第4回:【改善編】簡単なツール作成を通してGolangを学ぶ
- 第5回:【改善編】簡単なツール作成を通してJavaを学ぶ
- 第6回:【改善編】簡単なツール作成を通してScalaを学ぶ
- 第7回:簡単なツール作成を通してRustを学ぶ
- 第8回:【改善編】簡単なツール作成を通してRustを学ぶ
実装・動作確認端末
# OS - Linux
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.5 LTS (Bionic Beaver)"
# 言語バージョン
$ rustc --version
rustc 1.46.0 (04488afe3 2020-08-24)
$
$ cargo --version
cargo 1.46.0 (149022b1d 2020-07-17)
# IDE - IntelliJ IDEA
IntelliJ IDEA 2020.2.1 (Ultimate Edition)
Build #IU-202.6948.69, built on August 25, 2020
実践
要件
アプリを起動すると、キーバリュー形式でテキスト情報をJSONファイルに保存する機能を持つコンソールアプリ。
ソース全量
解説
全ソースファイル
Rustソース | 説明 |
---|---|
main.rs | アプリ起動エントリーポイント |
store_info.rs | キーバリュー情報を保存するストア(JSONファイル)に関する情報を扱う。 現状は「ファイル名」だけ保持 |
commands.rs | キーバリューストアからの情報取得や保存、削除といった各コマンドを管理。 コマンドの増減に関する影響は、このソースに閉じる。 |
command.rs | 各コマンドに共通のインタフェース |
save.rs | キーバリュー情報の保存を担う。 |
get.rs | 指定キーに対するバリューの取得を担う。 |
list.rs | 全キーバリュー情報の取得を担う。 |
remove.rs | 指定キーに対するバリューの削除を担う。 |
clear.rs | 全キーバリュー情報の削除を担う。 |
help.rs | ヘルプ情報の表示を担う。 |
end.rs | アプリの終了を担う。 |
注釈
-
use
,mod
は省略。
[main.rs]アプリ起動エントリーポイント
fn main() {
let commands: Commands = Commands::new();
loop {
let mut input = String::new();
// 標準入力から input へ
stdin().read_line(&mut input).unwrap();
input.retain(|c| c != '\n'); // 改行コードの除去
// 半角スペースで分割
let seps: Vec<&str> = input.split_ascii_whitespace().collect();
commands.exec(seps);
}
}
簡易説明
save
, get
, help
といったコマンド群は Commands
で管理させている。
loop
の無限ループ中で都度つど標準入力からの文字列インプット待ち。
受け取った文字列(例: save key01 value01
)を半角スペースで分割してコマンド処理役(= Commands
)に渡す。
[store_info.rs]ストア情報の管理
use serde::{Deserialize, Serialize};
const STORE_FILE: &str = "store.json";
// アトリビュートにてJSONパーサーのシリアライズ機能を持たせる
#[derive(Serialize, Deserialize, Debug)]
pub struct StoreInfo {
pub kvs: HashMap<String, String>,
}
pub fn re_write_store(json_str: String) {
let mut file = match OpenOptions::new()
.write(true)
.truncate(true)
.open(STORE_FILE)
{
Ok(f) => f,
Err(e) => {
eprintln!("{}", e);
process::exit(-1);
}
};
file.write(json_str.as_bytes()).unwrap();
}
pub fn read_store() -> String {
fs::read_to_string(STORE_FILE).unwrap()
}
pub fn read_store_info() -> StoreInfo {
let previous = read_store();
if previous.is_empty() {
return StoreInfo {
kvs: HashMap::new(),
};
}
serde_json::from_str(&previous).unwrap()
}
pub fn get_serialized(si: &StoreInfo) -> String {
serde_json::to_string(si).unwrap()
}
簡易説明
JSONの読み書きに Serde
というライブラリ(Rustではクレートと言うらしい)を使用。
https://crates.io/crates/serde
https://serde.rs/
各種コマンドの中で使う、JSON文字列のファイル読み書きを行う関数を用意。
[commands.rs]各コマンドの管理
pub struct Commands {
pub commands: HashMap<&'static str, Box<dyn Command>>,
}
impl Commands {
pub fn new() -> Commands {
let mut commands: HashMap<&'static str, Box<dyn Command>> = HashMap::new();
commands.insert(END, Box::new(End {}));
commands.insert(HELP, Box::new(Help {}));
commands.insert(CLEAR, Box::new(Clear {}));
commands.insert(SAVE, Box::new(Save {}));
commands.insert(GET, Box::new(Get {}));
commands.insert(REMOVE, Box::new(Remove {}));
commands.insert(LIST, Box::new(List {}));
Commands { commands }
}
pub fn exec(&self, args: Vec<&str>) {
if let Some(order) = args.get(0) {
if let Some(cmd) = self.commands.get(order) {
cmd.exec(args)
}
}
}
}
簡易説明
事前に各種コマンドをハッシュマップに保持しておいて、コマンド実行文字列が届いたら対応するコマンドを拾って実行。
まあ、よくあるコマンドパターン。
[command.rs]各コマンドの親クラス
pub trait Command {
fn exec(&self, args: Vec<&str>);
}
各コマンドクラス
■保存
pub const SAVE: &str = "save";
pub struct Save {}
impl Command for Save {
fn exec(&self, args: Vec<&str>) {
if args.len() != 3 {
return;
}
let mut kvs: HashMap<String, String> = HashMap::new();
// 今回分をKVSに格納
kvs.insert(
args.get(1).unwrap().to_string(),
args.get(2).unwrap().to_string(),
);
// 既存分を今回分に続けてKVSに格納
for (k, v) in read_store_info().kvs {
kvs.insert(k, v);
}
// JSONファイルに書き込み
re_write_store(get_serialized(&StoreInfo { kvs }));
}
}
■1件取得
pub const GET: &str = "get";
pub struct Get {}
impl Command for Get {
fn exec(&self, args: Vec<&str>) {
if args.len() != 2 {
return;
}
// JSONファイルから既存分を取得
let saved_si = read_store_info();
if let Some(v) = saved_si.kvs.get(*args.get(1).unwrap()) {
println!("{}", v);
}
}
}
■全件取得
pub const LIST: &str = "list";
pub struct List {}
impl Command for List {
fn exec(&self, _: Vec<&str>) {
// JSONファイルから既存分を取得
let saved_si = read_store_info();
for (k, v) in saved_si.kvs {
println!("{{ {}, {} }}", k, v);
}
}
}
■1件削除
pub const REMOVE: &str = "remove";
pub struct Remove {}
impl Command for Remove {
fn exec(&self, args: Vec<&str>) {
if args.len() != 2 {
return;
}
// JSONファイルから既存分を取得
let mut saved_si = read_store_info();
// 既存分から指定された要素を削除
saved_si.kvs.remove(*args.get(1).unwrap());
// JSONファイルに書き込み
re_write_store(get_serialized(&saved_si));
}
}
■全件削除
pub const CLEAR: &str = "clear";
pub struct Clear {}
impl Command for Clear {
fn exec(&self, _: Vec<&str>) {
re_write_store(String::from(""));
}
}
■ヘルプ
pub const HELP: &str = "help";
pub struct Help {}
impl Command for Help {
fn exec(&self, _: Vec<&str>) {
let msg = r#"
[usage]
キーバリュー形式で文字列情報を管理するコマンドです。
以下のサブコマンドが利用可能です。
save ... keyとvalueを渡して保存します。
get ... keyを渡してvalueを表示します。
remove ... keyを渡してvalueを削除します。
list ... 保存済みの内容を一覧表示します。
clear ... 保存内容を全て削除します。
help ... ヘルプ情報(当内容と同じ)を表示します。
end ... プログラムを終了します。
"#;
println!("{}", msg);
}
}
■アプリ終了
pub const END: &str = "end";
pub struct End {}
impl Command for End {
fn exec(&self, _: Vec<&str>) {
println!("End");
process::exit(0);
}
}
動作確認
1. 起動
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.11s
Running `target/debug/book_rust`
2. ヘルプ
help
[usage]
キーバリュー形式で文字列情報を管理するコマンドです。
以下のサブコマンドが利用可能です。
save ... keyとvalueを渡して保存します。
get ... keyを渡してvalueを表示します。
remove ... keyを渡してvalueを削除します。
list ... 保存済みの内容を一覧表示します。
clear ... 保存内容を全て削除します。
help ... ヘルプ情報(当内容と同じ)を表示します。
end ... プログラムを終了します。
3. 一覧表示 〜 1件追加 〜 一覧表示
list
save key01 val01
list
{ key01, val01 }
4. キー指定して取得
get key01
val01
get key02
5. キー指定して削除
remove key01
get key01
list
6. 全件削除
save keyAAA valBBB
save キー1 バリュー2
list
{ keyAAA, valBBB }
{ キー1, バリュー2 }
clear
list
7. アプリ終了
end
End
$
まとめ
いろいろ雑。ただ、これでも、そもそもコンパイル通すのだけでも一苦労で、かなり苦戦した。
チュートリアルを1回だけ写経しただけだと、「こういうふうに書いたらいけるだろう」がことごとくRustコンパイラにダメ出しされる。
でも、Rustコンパイラの出すメッセージ、けっこうやさしいので、それは嬉しい。
あと、それなりにリファクタはしたけど、Rustらしさはまだ全然わからない。。。