シンプルなCLIのTodoアプリ作りを通して、rustの勉強をした
はじめに
CLI でシンプルな Todo App を作る事が出来ました。
こちら
の記事を参考にさせて頂きました。非常に分かりやすく説明されていて、とても勉強になりました。ありがとうございます。
完成後のリポジトリも貼っておきます。
記事執筆時点で実装した機能は 追加 と 更新 、ファイルへの保存 のみです。
ただし、今後私自身が勉強のために機能を追加していく予定です。上記リポジトリも更新されると思います。下記のコミット内容が今回の記事に該当します。
バージョン
rustup: 1.25.1
cargo: 1.63.0
ディレクトリ作成
今回の作業ディレクトリを作ります。
$ cargo new todoapp-cli
作成したディレクトリに移動します。
$ cd todoapp-cli
src というディレクトリと、Cargo.toml というファイルがあると思います。 src ディレクトリ配下に main.rs というファイルが入っていると思います。今回はこちらのファイルにコードを書いていきます。
現時点で下記コマンドを実行すると、Hello world! と返ってくると思います。
$ cargo run
引数をどうやって読み取るか
今回目指す Todo アプリは、引数 [1] を 2 つ入力します。
1 つ目がアクション、2 つ目がアイテム(Todo の内容)です。今回は下記の標準クレートを利用します。
main.rs の main() に下記コードを記述します。
let action = std::env::args().nth(1).expect("Please specify an action");
let item = std::env::args().nth(2).expect("Please specify an item");
println!("{:?}, {:?}", action, item);
nth() で、各場所の引数を読み取る事が出来るそうです。
nth(0) はプログラムそのものを指すので、nth(1) が第一引数( action )になり、 nth(2) が第二引数( item )となります。
expect() [2] [3] は、値を返す、または存在しない場合(ここで言うと引数を書かなかった場合)は、プログラムを終了して下記のような指定のメッセージを返すものです。
thread 'main' panicked at 'Please specify an item', src/main.rs:52:40
rust では引数無しでプログラムを実行出来ます。なので、実際に引数に値を入力したかどうか確認する必要があります。
それでは実際に引数を 2 つ渡してみます。引数は -- の後ろに入力します。
第一引数に hello、第二引数に world と入力して実行してみます。
$ cargo run -- hello world
下記のように入力した引数が表示されていれば成功です。
"hello", "world"
引数を入力して、それを表示する処理を行いました。自分が思ったように動いてくれると嬉しいものですね。
構造体を作り、なおかつメソッドを定義する
先ほどの引数の処理は比較的簡単に実装出来ました。問題はここからです。構造体 [4] という箱?、型の集合体?みたいな物を作って、その構造体の振る舞い(メソッド) [5] を定義します。
The Rust Programming Language 日本語版
では、下記のように記されていました。
私はここで何度も頭が爆発しました。おそらくまだ理解出来ていないと思いますが、私の中では前述の通り 構造体が箱みたいなもので、その箱がどういう振る舞いをするのか定義 するのが メソッドの定義 と解釈しました。
他のプログラミング言語知識がある方は、似たような処理に覚えがあるかもしれません。
今回作成する Todo アプリは、Todoの追加 & Todoの更新 、そしてその Todo をファイル形式でローカルストレージに保存します。
それら 追加 更新 保存 の各振る舞いを定義する構造体をまずは作る必要があります。
構造体の作り方
標準ライブラリにある HashMap を利用するので、main.rs の一番上に下記コードを追記します。
use std::collections::HashMap;
main 関数の外に下記コードを入力します。
構造体名の頭文字は大文字で記述します。
struct Todo {
map: HashMap<String, bool>,
}
いきなり出てきた HashMap に驚いている方いると思います。私は何が起きてるのか分かりませんでした。構造体よりも意味が分かっていません。Rust By Example 日本語版 や、rust 公式ドキュメントを見てみました。
- ハッシュマップ
- Struct std::collections::HashMap
- Hash table
key で値を参照するのがハッシュテーブルらしいです。このハッシュテーブルが、rust では HashMap で実装されているようです。
HashMap<String, bool> という書き方は、String と boolean のキーで値を保持すると解釈しました。
下記のような出力結果をイメージしました。
Learning rust や Shopping が String キー、true が boolean キーです。
{
"Learning rust": true,
"Shopping": true
}
これで、構造体が出来ました。次はどのような振る舞いをさせるか、メソッドを定義していきます。この時点で頭から煙が出てしまいますね。rust 楽しいです。
構造体にメソッドを定義する
メソッドは通常の関数 fn のように記述出来ます。
記述は似ていますが、書く場所は構造体の外で、第一引数は self [6] パラメータを受け取ります。
self は呼び出されたメソッドの構造体自身を指す、と解釈しています。
型も構造体と同じになるので、記述しなくて済んでいるのではないかなと思います。
impl ブロックにメソッドの定義を記述します。
impl Todo {
fn insert(&mut self, key: String) {
self.map.insert(key, true);
}
}
impl は、振る舞いを定義する際に利用する、と解釈しています。今回の Todo アプリでは、構造体の振る舞い、つまりメソッドを定義していて、下記の
The Rust Programming Language 日本語版
では、型にトレイトの振る舞い [7] を定義しています。
これを踏まえた上で、上記コードを見ると、Todo 構造体に insert メソッドを定義しています。
上記コードは、構造体への参照 &mut self とキー key: String を受け取り、HashMap の insert メソッド [8] を利用して map に値を入れている、と解釈しています。
構造体は新しい値を追加して変更されていくので、mut [9] で可変にしています。
そして、& は参照 [10] を意味します。ここでは、値そのものではなく、値が格納されている場所へのポインタ をイメージします。
なので、厳密には値を保持しておらず、値が格納されている場所を示してるだけ、だと思っています。
&mut self は、私の中で 2 つのどちらが正しい意味なのか分かっていません。2 つとも正しくない場合もあります。orz
値の更新が可能な構造体への参照更新可能な値への参照を持つ構造体
& が mut に掛かってるように見えるが・・・
-
&+mut self -
&mut+self執筆時点(2022/8/26)では理解できていません。
参照や、値の借用と合わせて 所有権 [11] という rust 独自のルールがあります。これは、変数が値の所有者という意味で捉えています。
所有権には規則があります。
- rust の各値は、所有者と呼ばれる変数と対応している。
- いかなるときも所有者は一つ。
- 所有者がスコープから外れたら、値は破棄される。
他のプログラミング言語をまともに触った事がないので、この所有権がどうユニークなのか、調べてみました。
- C 言語などは、メモリ管理をプログラマー自身が全て管理する。
- Java 言語などは、ガベージコレクション?というシステムが導入されており、プログラミング言語側で良い感じにメモリを管理してくれる。
メモリ管理をプログラマーが全て行う。そうなると、メモリの解放忘れ?みたいなヒューマンエラーが起こりうる。
ガベージコレクションはすごく楽。プログラミング言語側で全てやってくれるので。しかし、低速・・・楽だが、C 言語などと比べると遅いらしいですね。
上記 2 つの問題点を解決するべく、所有権という機能が生まれたみたいです。
rust はガベージコレクションを導入していません。なので Java 言語などよりは早いです。
所有者がスコープから外れたら、値は破棄される という rust の規則があります。なので、解放忘れなどが発生してもコンパイル時に分かる。
ここらへん、なんとなくイメージは出来てきましたが、理解が出来ていないようでコンパイルで殴られています。rust 楽しいです。
所有権に関する詳しい内容は前述した The Rust Programming Language 日本語版 を見てみてください。今回の Todo アプリ作りの過程で 値を所有して削除するか、もしくは保持するために参照するか 考えながら進みます。
ファイルに保存する
DB などを利用せずにデータを残すために、ローカルに プレーンテキストファイル を生成してそこに記録を保存していきます。
impl ブロック内に下記のコードを追記してください。先ほど記述した insert メソッドの外に記述します。
impl Todo {
// ここに insert メソッドがある
+ fn save(self) -> Result<(), std::io::Error> {
+ let mut content = String::new();
+ for (k, v) in self.map {
+ let record = format!("{}\t{}\n", k, v);
+ content.push_str(&record)
+ }
+ std::fs::write("db.txt", content)
+ }
}
-> は、関数から返される型を注釈しています。上記コードでは Result を返します。
map を繰り返し処理し、format! マクロで各文字列を整えています。\t がタブ出力で \n が改行を意味します。
その後、content.push_str でフォーマットされた文字列をコンテンツにプッシュしています。
std::fs::write("db.txt", content) は、db.txt というテキストファイルに先ほどのコンテンツを書き込んでいくという意味です。この書き方だと、todoapp-cli ディレクトリ直下に db.txt というファイルが生成されます。
私は data というディレクトリを作成し、その中に生成するようにしたかったので、下記のように書き直しました。
impl Todo {
// ここに insert メソッドがある
fn save(self) -> Result<(), std::io::Error> {
let mut content = String::new();
for (k, v) in self.map {
let record = format!("{}\t{}\n", k, v);
content.push_str(&record)
}
- std::fs::write("db.txt", content)
+ std::fs::write("./data/db.txt", content)
}
}
ディレクトリ構成のルールなどは全く分かっていません。プロジェクト直下にデータファイルをそのまま保存するのに個人的違和感があったためこのようにしています。
私はこの save メソッドを定義する際に引数の self が参照ではなく実体であることに気付きました。
save が self の所有権を持つことで、マップによる誤更新を防ぐのが目的かと思っています。
main 関数で構造体を利用する
冒頭で記述した引数の操作の下に下記を記述します。
fn main() {
let action = std::env::args().nth(1).expect("Please specify an action");
let item = std::env::args().nth(2).expect("Please specify an item");
println!("{:?}, {:?}", action, item);
+ let mut todo = Todo {
+ map: HashMap::new(),
+ };
+ if action == "add" {
+ todo.insert(item);
+ match todo.save() {
+ Ok(_) => println!("todo saved"),
+ Err(why) => println!("An error occurred: {}", why),
+ }
+ }
}
Todo 構造体で todo をインスタンス化しています。そして mut で変更可能な状態としています。
if action == "add" は、第一引数が add の場合の処理という意味です。
todo.insert(item); は、先ほどの todo インスタンスの insert メソッドをドット記法で呼んでいます。引数に item を渡しています。insert メソッドはだいぶ序盤に書いたのでここでもう一度下記に記載しています。insert の第一引数 &mut self は構造体自身なので、第二引数の key に先ほどの item が渡されます。
fn insert(&mut self, key: String) {
self.map.insert(key, true);
}
match todo.save() は、成功したらOk、 失敗したら Err を返します。
match [12] [13] とは、一連のパターンに対して値を比較し、マッチしたパターンに応じてコードを実行させてくれる制御フロー演算子だそうです。
この時点で、Todo の追加とファイルへの保存が出来ます。
$ cargo run -- add "test todo1"
todo saved と返ってきたら追加処理はうまく動いています。
$ cat ./data/db.txt
test todo1 true と返ってきたら、テキストファイルへの保存もうまく動いています。私は data ディレクトリにテキストファイルを保存しているので上記コマンドでしたが、保存先に応じて適宜書き直してください。
自分の書いたコード通りに処理が進んでいるので楽しいですね。私は、この時点で insert メソッドの定義内に save メソッドを定義してしまったり、タイピングミスなどを繰り返し長い道のりでした。コンパイル時に細かく指摘してくれるので一つのエラーに長時間悩まされる事は今のところありません。rust 楽しいです。
Todo を上書きせずに新しく作る
今の状態だと、追加 Todo を追加する度に上書きしています。これでは不便ですね。Todo は複数ある状況が多いと思います。次は、上書きしないようにしていきます。
下記のコードを impl ブロック内に追記します。
impl Todo {
fn insert(&mut self, key: String) {
// insertメソッド中身
}
fn save(self) -> Result<(), std::io::Error> {
// saveメソッド中身
}
+ fn new() -> Result<Todo, std::io::Error> {
+ let mut list = std::fs::OpenOptions::new()
+ .write(true)
+ .create(true)
+ .read(true)
+ .open("./data/db.txt")?;
+ let mut content = String::new();
+ list.read_to_string(&mut content)?;
+ let map: HashMap<String, bool> = content
+ .lines()
+ .map(|line| line.splitn(2, '\t').collect::<Vec<&str>>())
+ .map(|v| (v[0], v[1]))
+ .map(|(k, v)| (String::from(k), bool::from_str(v).unwrap()))
+ .collect();
+ Ok(Todo {map})
+ }
}
上記コードは
参考
にしている記事の内容によると、結構無理やりいろんな書き方を導入したみたいです。他言語で目にするクロージャー、イテレータ、ラムダ関数などを rust がサポートしていますという紹介が目的のようです。私は他言語の知識をほとんど持っていないので分かりませんでしたが・・・
fn new() -> Result<Todo, std::io::Error> は、Todo もしくは、std::io::Error を返す new 関数を定義しています。引数に self が無いので、これはメソッドではありません。impl ブロック内で記述した理由は、impl が implementation (実装) という意味にもある通り、Todo 構造体の機能として new() を実装するからです。
下記の部分で、db.txt をどのように開くかを設定しています。
let mut list = std::fs::OpenOptions::new()
.write(true)
.create(true)
.read(true)
.open("./data/db.txt")?;
-
.write(true)で、ファイルの書き込みを許可 -
.create(true)で、ファイルが存在しなかった場合に作成する事を許可 -
.read(true)で、ファイルの読み込みを許可 -
.open("./data/db.txt")?;で、./data/db.txtファイルを開く事を許可
OpenOptions [14] は、デフォルトで false です。
また、.open("./data/db.txt")?; の ? [15] [16] が気になったので調べてみたところ、エラー処理の一種です。例によって The Rust Programming Language 日本語版 に記載があったので、下に URL 貼ってます。詳しい内容はご確認ください。
-
Resultを返す関数内でしか使えない -
match式とほぼ同じように動作する -
ResultがOkならOkの中身を返す、Errなら、returnと同じような動作をして、エラーは呼び出し元のコードに委譲される
let mut content = String::new();
list.read_to_string(&mut content)?;
list.read_to_string(&mut content)?; は、ファイル内を全バイト読み込み、content String に追加します。
read_to_string メソッドを利用するために、ファイル先頭に use std::io::Read; を記述します。
次は下記コードの解説です
let map: HashMap<String, bool> = content
.lines()
.map(|line| line.splitn(2, '\t').collect::<Vec<&str>>())
.map(|v| (v[0], v[1]))
.map(|(k, v)| (String::from(k), bool::from_str(v).unwrap()))
.collect();
.lines() [17] は、文字列の各行に対するイテレータを作成します。
.map で 3 行処理していますが、この動きが理解出来ませんでした。| | これがクロージャ?という機能らしいです。
以下Rust By Example 日本語版 よりクロージャの特徴を抜粋 [19] [20]
- その場限りの用途で使われる
- 呼び出しは関数の呼び出しと全く同じ
- 入力変数の名前は必ず指定しなければならない (入力変数を囲むのに
||を利用) - 外側の環境にある変数を捕捉する事が出来る (
捕捉だからスコープ外の変数を取り込む事ができる?)
んー、難しい・・・
line.splitn(2, '\t') [21] は、タブ文字で 2 つに分けるという意味です。
メソッドに collect::<Vec<&str>>() [22] を追加して、分割された文字列を借用した文字列スライスの Vector 型に変換するように map関数に指示しています。
map(|v| (v[0], v[1])) は、タプルに変換していると思います。
map(|(k, v)| (String::from(k), bool::from_str(v).unwrap())) は、先ほど変換したタプルの 2 つの要素を String と boolean に変換しています。 from_str() メソッドを利用するために、ファイルの先頭に use std::str::FromStr; を追記します。
Ok(Todo {map}) で、 呼び出し元に構造体を返します。
下記コードを main 関数内に追記します。
fn main() {
let action = std::env::args().nth(1).expect("Please specify an action");
let item = std::env::args().nth(2).expect("Please specify an item");
// こちらのコードは引数の動作確認で利用しただけなのでコメントアウトか削除
// println!("{:?}, {:?}", action, item);
+ let mut todo = Todo::new().expect("Initialisation of db failed");
let mut todo = Todo {
map: HashMap::new(),
};
if action == "add" {
todo.insert(item);
match todo.save() {
Ok(_) => println!("todo saved"),
Err(why) => println!("An error occurred: {}", why),
}
}
}
早速 2 回以上 add アクションで Todo を追加してみます。
$ cargo run -- add "todo1 cooking"
todo saved
$ cargo run -- add "todo2 shopping"
todo saved
$ cat ./data/db.txt
todo1 cooking true
todo2 shopping true
上記のような結果になったと思います。上書きしなくなるだけで Todo アプリっぽくなりますね。もうお腹いっぱいかもしれませんが、せっかく true or false で Todo のフラグを立ててるので、完了した Todo は false に更新したいです。rust 楽しいです。
完了した Todo を false に更新する
complete というメソッドを新しく追加します。
impl Todo {
fn insert(&mut self, key: String) {
// insertメソッド中身
}
fn save(self) -> Result<(), std::io::Error> {
// saveメソッド中身
}
fn new() -> Result<Todo, std::io::Error> {
// new中身
}
+ fn complete(&mut self, key: &String) -> Option<()> {
+ match self.map.get_mut(key) {
+ Some(v) => Some(*v = false),
+ None => None,
+ }
+ }
}
Option<()> [23] 型は、値がある場合は Some() 、値がない場合は None を返します。 None はエラーという意味ではなく、値が無い事を示しているだけです。エラーを表す場合は、 Result<T,E> を使う場合が多いらしいです。
self.map.get_mut(key) [25] は、コレクション内に値が存在しなければ None を返します。
Some(*v = false) の * は、参照外し [26] です。参照ではなく実体を false に更新しています。
complete メソッドの定義は出来ました。次は main 関数で利用するために、コードを追記します。 insert メソッドの書き方同じく、第一引数に与えられる action を見て処理を行うようにします。if を使って条件分岐します。
fn main() {
let action = std::env::args().nth(1).expect("Please specify an action");
let item = std::env::args().nth(2).expect("Please specify an item");
// こちらのコードは引数の動作確認で利用しただけなのでコメントアウトか削除
// println!("{:?}, {:?}", action, item);
let mut todo = Todo::new().expect("Initialisation of db failed");
let mut todo = Todo {
map: HashMap::new(),
};
if action == "add" {
todo.insert(item);
match todo.save() {
Ok(_) => println!("todo saved"),
Err(why) => println!("An error occurred: {}", why),
}
+ } else if action == "complete" {
+ match todo.complete(&item) {
+ None => println!("'{}' is not present in the list", item),
+ Some(_) => match todo.save() {
+ Ok(_) => println!("todo saved"),
+ Err(why) => println!("An error occurred: {}", why),
+ }
+ }
+ }
}
todo.complete(&item) を match 式で処理します。 None の場合はエラーメッセージを表示するようにしています。 todo.complete メソッドに渡すのは item の参照である &item です。元の所有者は save だが、今この瞬間だけ complete が item を借りている状況になると解釈しています。
なので、 次の None => println!("'{}' is not present in the list", item) で、 item を利用出来ます。
&item があれば Some(_) を処理します。todo.save() を呼び出して、ファイルに保存します。保存が成功したら Ok(_) を表示、失敗したら Err(why) でエラーメッセージを表示します。
これで Todo のトグルを切り替える処理が完了しました。試してみるために、一度 ./data/db.txt を削除します。
$ rm ./data/db.txt
そして実際に、複数 Todo を追加して、そのうち一つの complete してみます。
$ cargo run -- add "todo1 javascript"
$ cargo run -- add "todo2 rust"
$ cargo run -- complete "todo2 rust"
$ cat ./data/db.txt
todo1 javascript true
todo2 rust false
上記のように、2 つ追加して上書きされておらず、そのうち一つを complete アクションで完了とすると、 false に変わっています。最後に、 cat コマンドで確認しています。
下記のようにコマンドを打つと完了ステータスから保留ステータスに更新出来ます。
$ cargo run -- add "todo1 rust"
$ cat ./data/db.txt
todo1 javascript true
todo2 rust true
保存先を JSON ファイル形式に変更
これで追加、更新、保存が出来る Todo アプリを作れました。すでに形にはなっていますが、プレーンテキスト形式で保存しているデータを json ファイルにしたいと思います。この時 serde_json を利用するので、 Cargo.toml ファイルの dependencies に下記を追記します。
[dependencies]
serde_json = "1.0.60"
impl ブロックの new() 関数を更新します。
impl Todo {
fn insert(&mut self, key: String) {
self.map.insert(key, true);
}
fn save(self) -> Result<(), std::io::Error> {
let mut content = String::new();
for (k, v) in self.map {
let record = format!("{}\t{}\n", k, v);
content.push_str(&record)
}
std::fs::write("db.txt", content)
}
fn new() -> Result<Todo, std::io::Error> {
- let mut list = std::fs::OpenOptions::new()
+ let list = std::fs::OpenOptions::new()
.write(true)
.create(true)
.read(true)
- .open("./data/db.txt")?;
+ .open("./data/db.json")?;
- let mut content = String::new();
- list.read_to_string(&mut content)?;
- let map: HashMap<String, bool> = content
- .lines()
- .map(|line| line.splitn(2, '\t').collect::<Vec<&str>>())
- .map(|v| (v[0], v[1]))
- .map(|(k, v)| (String::from(k), bool::from_str(v).unwrap()))
- .collect();
- Ok(Todo {map})
+ match serde_json::from_reader(list) {
+ Ok(map) => Ok(Todo { map }),
+ Err(e) if e.is_eof() => Ok (Todo {
+ map: HashMap::new(),
+ }),
+ Err(e) => panic!("An error occurred: {}", e),
+ }
}
fn complete(&mut self, key: &String) -> Option<()> {
match self.map.get_mut(key) {
Some(v) => Some(*v = false),
None => None,
}
}
}
list.read_to_string(&mut content)?; で、コンテンツを String に割り当てる必要がないので、 let list = std::fs::OpenOptions::new() で mut を外しています。
.open("./data/db.json")?; のファイル拡張子を json へ変更しました。
OpenOptions で read(true) にしているため serde_json::from_reader(list) [27] で、ファイルなどから読み込む事が可能です。
この match 式は、最初にファイルの存在有無を確認し、ファイルが無ければ新しくハッシュマップを作り返す処理に見えます。
Err(e) => panic!("An error occurred: {}", e) で利用している panic! マクロ [28] は、エラーメッセージを表示して処理を巻き戻すそうです。そんな事が出来るとはすごいですね。Cargo.toml ファイルに追記する事で、巻き戻しではなく異常終了に切り替えることも出来るそうですが、今回は特に設定していません。
save メソッドを更新
map を json ファイルで保存するときにも、 serde を使いたいので save() の中身も更新します。
impl Todo {
fn insert(&mut self, key: String) {
self.map.insert(key, true);
}
- fn save(self) -> Result<(), std::io::Error> {
- let mut content = String::new();
- for (k, v) in self.map {
- let record = format!("{}\t{}\n", k, v);
- content.push_str(&record)
- }
- std::fs::write("db.txt", content)
- }
+ fn save(self) -> Result<(), Box<dyn std::error::Error>> {
+ let content = std::fs::OpenOptions::new()
+ .write(true)
+ .create(true)
+ .open("./data/db.json")?;
+ serde_json::to_writer_pretty(content, &self.map)?;
+ }
fn new() -> Result<Todo, std::io::Error> {
let list = std::fs::OpenOptions::new()
.write(true)
.create(true)
.read(true)
.open("./data/db.json")?;
match serde_json::from_reader(list) {
Ok(map) => Ok(Todo { map }),
Err(e) if e.is_eof() => Ok (Todo {
map: HashMap::new(),
}),
Err(e) => panic!("An error occurred: {}", e),
}
}
fn complete(&mut self, key: &String) -> Option<()> {
match self.map.get_mut(key) {
Some(v) => Some(*v = false),
None => None,
}
}
}
Box<dyn std::error::Error> の Box [29] [30] は、ヒープ領域に値を確保する最も簡単な方法で、簡単に言うとヒープ上に置かれた値へのポインタです。ファイルを開く際、ファイルシステムのエラーか、もしくは Serde のエラーか、どちらを返すか分からないので、呼び出し元がエラーを処理出来るようにエラーそのものではなく、エラーへのポインタを返します。 dyn [31] は、トレイトを使う際の構文です。
serde_json::to_writer_pretty(content, &self.map)?; [32] で、第一引数で write(true) を確認して、第二引数のデータ構造を json として出力します。
最後に、 use std::io::Read; と use std::str::FromStr; は必要無いので、ファイルから削除してください。
終わりに
お疲れ様でした。javascript を某プログラミング学習サービスでかじった程度のプログラミング知識なので、的外れな解釈があるかもしれません。他の言語習得時にも、チュートリアルとして Todo アプリを作るケースが多そうだったので、今回 rust の入門として行いました。完成ソースコードも冒頭に貼っているので、コード全体の確認にご利用ください。rust 楽しいです。
Discussion
素晴らしい記事をありがとうございます!
この記事に沿ってコーディングを進めていく過程で、
どういう仕組みだ?これはなんなんだ!?
という疑問が出てきて、Rustの理解度が少し深まったように感じます。
この記事を読み終えたあとに、メゾットの追加とそれに伴って必要になったsave()メゾットの若干の仕様変更を行ってみました!
途中、所有権やjson周りの扱いで躓きましたが、それを乗り越えて少し自信がつきました!
私のような初心者には、本当に有り難い記事でした!
Daikiさん、ありがとうございました!
今後も精進していきます!
記事の閲覧とコメントありがとうございます!
励みになるお言葉まで頂き光栄です。ありがとうございます!
記事執筆当時は趣味プログラミングでRustを触れていましたが、現在は業務でTSを触る機会を頂きなかなかRustを触れていないのが現状でした...
ろふぇん様にコメント頂きまた僕もモチベーションが湧いてきたので頑張りたいと思います😊
一緒に頑張りましょう🔥