100 Exercises To Learn Rust を題材に Qiita Engineer Festa 2024 投稿マラソン に参加していました!
7月17日に仮置きを残しながら投稿したのち、 ようやく全記事完成しました! ので、その記念として、色々話したいと思います!(え、7/17ってもう3週間前...?!)
目次
※ 前半は100 Exercisesの話題が中心です。早く勘違い3選を見たいという方は こちら
もう一本まとめ記事を書きましたので良かったら読んでみてください!
【🙇 懺悔 🙇】Qiitanグッズ欲しさに1日に33記事投稿した話またはQiita CLIとcargo scriptを布教する的な何か
全記事一覧
どの記事も熱量を込めて書きましたので読んでいただけたら恐悦至極です
- 【0】 準備
- 【1】 構文・整数・変数
- 【2】 if・パニック・演習
- 【3】 可変・ループ・オーバーフロー
- 【4】 キャスト・構造体 (たまにUFCS)
- 【5】 バリデーション・モジュールの公開範囲 ~ → カプセル化!~
- 【6】 カプセル化の続きと所有権とセッター ~そして不変参照と可変参照!~
- 【7】 スタック・ヒープと参照のサイズ ~メモリの話~
- 【8】 デストラクタ(変数の終わり)・トレイト ~終わりと始まり~
- 【9】 Orphan rule (孤児ルール)・演算子オーバーロード・derive ~Empowerment 💪 ~
- 【10】 トレイト境界・文字列・Derefトレイト ~トレイトのアレコレ~
- 【11】 Sized トレイト・From トレイト・関連型 ~おもしろトレイトと関連型~
- 【12】 Clone・Copy・Dropトレイト ~覚えるべき主要トレイトたち~
- 【13】 トレイトまとめ・列挙型・match式 ~最強のトレイトの次は、最強の列挙型~
- 【14】 フィールド付き列挙型とOption型 ~チョクワガタ~
- 【15】 Result型 ~Rust流エラーハンドリング術~
- 【16】 Errorトレイトと外部クレート ~依存はCargo.tomlに全部お任せ!~
- 【17】 thiserror・TryFrom ~トレイトもResultも自由自在!~
- 【18】 Errorのネスト・慣例的な書き方 ~Rustらしさの目醒め~
- 【19】 配列・動的配列 ~スタックが使われる配列と、ヒープに保存できる動的配列~
- 【20】 動的配列のリサイズ・イテレータ ~またまたトレイト登場!~
- 【21】 イテレータ・ライフタイム ~ライフタイム注釈ようやく登場!~
- 【22】 コンビネータ・RPIT ~ 「
Iterator
トレイトを実装してるやつ」~ - 【23】
impl Trait
・スライス ~配列の欠片~ - 【24】 可変スライス・下書き構造体 ~構造体で状態表現~
- 【25】 インデックス・可変インデックス ~インデックスもトレイト!~
- 【26】 HashMap・順序・BTreeMap ~Rustの辞書型~
- 【27】 スレッド・'staticライフタイム ~並列処理に見るRustの恩恵~
- 【28】 リーク・スコープ付きスレッド ~ライフタイムに技あり!~
- 【29】 チャネル・参照の内部可変性 ~Rustの虎の子、mpscと
Rc<RefCell<T>>
~ - 【30】 双方向通信・リファクタリング ~返信用封筒を入れよう!~
- 【31】 上限付きチャネル・PATCH機能 ~パンクしないように制御!~
- 【32】
Send
・排他的ロック(Mutex
)・非対称排他的ロック(RwLock
) ~真打Arc<Mutex<T>>
登場~ - 【33】 チャネルなしで実装・Syncの話 ~考察回です~
- 【34】
async fn
・非同期タスク生成 ~Rustの非同期入門~ - 【35】 非同期ランタイム・Futureトレイト ~非同期のお作法~
- 【36】 ブロッキング・非同期用の実装・キャンセル ~ラストスパート!~
- 【37】 Axumでクラサバ! ~最終回~
- 【おまけ1】 Rustで勘違いしていたこと3選 🏄🌴 【100 Exercises To Learn Rust 🦀 完走記事 🏃】 ← 本記事
- 【おまけ2】 【🙇 懺悔 🙇】Qiitanグッズ欲しさに1日に33記事投稿した話またはQiita CLIとcargo scriptを布教する的な何か
100 Exercises To Learn Rust を完走した感想
100 Exercises は比較的最近できたRustチュートリアルサイトです!100 Exercises自体の説明は初回に譲ります。(こちらでもそんなにしていないですが...)
マラソンの題材として完走した感想について、6月10日から約2ヶ月に渡り取り組んできたこともあり、エクササイズに対する思いを全て書き出すのは難しいですが、一言で表すと
『 無理のないRust再入門 』1
だったなぁと思います。つまり、筆者にとって「ちょうどいい内容」でした!
多くのRustaceanは至高の公式ドキュメント The Rust Programming Language (通称 TRPL) でRust入門するかと思います。TRPLは、Rustに留まらずコンピュータサイエンスにも足を突っ込んだ、かなり網羅的で詳細な入門書でとてもためになりますが、ただひたすら 長い です。Rustlings があれど全てを理解しながら進めるのは難しく、「とりあえず今は飛ばす」とした人が多いのではないでしょうか...?その後Rustプログラミングをしていく中で、我流で色々な知識を身につけていくわけです。
100 Exercises To Learn Rust はそんな我流で知識を身につけたRustacean(要は筆者)に復習の機会を与えてくれたのです!100 Exercisesの魅力を以下に挙げます。
100 Exercisesの魅力
1. そこそこの長さ
TRPLと比べて 長くない です。決して短いとまではいいませんが(何しろマラソン記事書き上げには2ヶ月かかりましたから... )、内容を読み問題を解いていくだけならば、体感でA Tour of Go 1.5周分ぐらい(?)(適当)の、復習に最適な長さに感じました!
2. 無理のない学習順序
TRPLは公式ドキュメントである以上、トピックの「必要性」よりは「重要性」を優先した順序で書かれていたような気がします。一方で、100 Exercisesは「 必要になったら 」解説するというスタイルなのが特徴的でした。まるで定義していない知識は使ってはいけない大学数学のよう...
特に顕著だったのは配列・コレクションとトレイト(他言語でいうインターフェースの機能です)の登場タイミングでしょうか...?Rustの理解において これがないと始まらないというのは、配列よりはどっちかというとトレイト です。それを踏まえてなのかはわからないですが、TRPLは先にコレクションを扱っている一方、100 Exercisesでは トレイトを先に 扱っていました。他言語から新しくRustをやる人にはTRPLの並びの方が親切かもしれませんが、解説していない内容を極力避けて、必要になった時に解説を行う、という100 Exercisesのスタイルは、特に再入門者である筆者にとって読みやすい順番だったと思います。
3. 各エクササイズ間に関連がある
前節に関係した内容ですが、エクササイズ全体を通した題材が用意されており、「 どういうシーン でその回で扱っている機能を使うのか?」というのがわかりやすい内容だったと思います。
特に3章から7章では、「チケット管理システムを作る」という内容でした。各章で以下のような機能を作ります。
章 | 章名 | 内容 |
---|---|---|
3 | Ticket v1 | チケット構造体の定義。OOP的な内容を通して所有権・参照について扱う |
4 | Traits | 主要なトレイトを扱い、チケット構造体への演算子オーバーロード等を行う |
5 | Ticket v2 | 列挙体を利用したバリデーション・回復可能なエラーハンドリング( Option 型や Result 型)を扱う |
6 | Ticket Management | 静的・動的配列やその他コレクション( HashMap 等 )・イテレータ等を用いた複数のチケットを管理できるシステムの構築を行う |
7 | Threads | 並行処理により、チケット管理システムをサバクラ化する |
「前章・前節までで作ったものを元に新たな機能を実装する」というスタイルは、「課題(必要性)が出てきたから取り組む」という学びやすい仕組みを生み出しており、これも取り組みやすい理由でした!
4. 効率よく"ミソ"を摂取できる
いわば「 夏期講習 !!!これだけは絶対に押さえておきたい要点n選!」みたいな感じで、解説対象がよく練られている印象でした。
例えば4章のトレイトでは Clone
・ Copy
・ Drop
等絶対に押さえておきたいトレイトによくページが割かれていたり、5章 ではRust言語それ自体の仕様ではないのにも関わらず、デファクトスタンダードとなっており避けては通れない thiserror クレートを紹介したりと、Rustaceanを名乗るには知っておかねばならないであろう知識を効率よく確認できる内容となっていました!
...というわけで、復習のまとめとして、次節では筆者が恥ずかしながら勘違いしており今回のマラソンで知ることができた知識を3つ紹介したいと思います!
Rustで今まで勘違いしていたこと3選
100 Exercisesを通して初めて知ったことは多々ありましたが、今回はその中でも厳選して3つ紹介します。
1. Copy
は Clone::clone
を...呼び出さなかった!
該当回: 【12】 Clone・Copy・Dropトレイト ~覚えるべき主要トレイトたち~
Rustにおいて「(容易に)複製できる値(型)」は、プリミティブ型かデータクラスか...ではなくて Clone
トレイトを実装しているか否か ただその一言だけで表されます。筆者はこれに関連した記事を書いたことがありました。
Rustでは、Clone
トレイトを実装している型の値 v
に対して、その値を複製したい時は、 v.clone()
と呼ぶことでディープコピーが可能です。
そして、この Clone
トレイトに関連するトレイトとして、 Copy
トレイトというものがあります。こちらを付けた型は「 .clone()
を付けなくても複製を行なってくれる」ようになります。また、 Copy
トレイトを実装する型は、必ず Clone
トレイトを実装している必要があります。
以上をまとめましょう。関係性はこうです。
-
Clone
トレイト実装型は.clone()
で複製できる -
Copy
トレイトという.clone()
なしでも複製してくれるトレイトもとい機能がある Copy
はClone
を前提とする
これらの事実より、筆者は勝手に「 Copy
トレイトが付いていれば、複製時には .clone()
を付けなくても裏で勝手に .clone()
が呼ばれるんだろうなぁ」と思っていました。 違いました。
Copy
トレイトはヒープ保存を必要としない型にしか付けられないのですが、Copy
が付いている型が複製される場合は「スタック上のメモリの値がそのまま別なメモリに転記される」ように最適化されており、 .clone()
は呼ばれない というのが真実でした...100 Exercisesで知るまで勘違いしていた...
#[derive(Debug)]
struct Hoge(u32);
impl Clone for Hoge {
fn clone(&self) -> Self {
println!("beep!: {}", self.0);
Hoge(self.0)
}
}
impl Copy for Hoge {}
fn main() {
let h = Hoge(10);
let h2 = h;
dbg!(h);
dbg!(h2);
dbg!(h);
h.clone();
}
Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=de1113871471d8e7deb6aeae09c48dc7
上記拙著にて間違った説明をしてしまっていたのですが、今回修正を入れました
また関連して、勘違いしていたわけではなかったのですが、「 Copy
を実装する型は Drop
トレイトを実装不可である」...というのも今回初めて知りました。
2. 不変参照は...実は不変ではなかった!
該当回: 【29】 チャネル・参照の内部可変性 ~Rustの虎の子、mpscと Rc<RefCell<T>>
~
勘違いというよりはよく知らなかったことになります。Rustの不変参照は名前通り値を変更できない参照ですが、 RefCell
や Mutex
を用いることで不変参照でも一時的に可変となることができます。これを 内部可変性 と言います。
筆者が知らなかったのは、不変参照が可変になれる分水嶺となる型 UnsafeCell
の存在でした... RefCell
や Cell
、 Rc
の内部では皆 UnsafeCell
を使っている らしいのです!
というのも、この UnsafeCell
が ついていない不変参照は、コンパイラが最適化を施してくれる そうで、 内部可変性を持つ不変参照が最適化されてしまわないためには、 UnsafeCell
の使用が必須 そうなのです!
ちなみに不変参照ではなく「不変な値(構造体)」なら、内部に可変参照を持てば(屁理屈的に)内部可変?になれます。その事実より不変参照も同様だろう(ただ、知り得ないちょっと特殊な unsafe
をしているんだろう)と思い込んでいたので、このことを知った時は目から鱗でした...
3. 値がスレッドを跨ぐには 'static
は...不要だった!
該当回:
こちらも勘違いというよりはよく分かっていなかった部分が鮮明になったという感じです。
Rustには、スレッド間で値を送り合えることを示す Send
トレイトというものがあります。このマーカートレイトがついていない型は、レースコンディション等の問題でスレッド間転送ができません!そこまでは理解していました。
そして値(や参照)が普通の(←伏線です)スレッド間を跨ぐにはもう一つ条件がありまして、まさしくRustらしい話題なのですが、 'static
ライフタイムを持つ...言い換えると、「以降プログラム終了まで永遠に生き続けられる」必要があります。なぜなら「お互いのスレッドがいつまで生きているかはわからない」ためです!
そして大体の場合、片方のスレッドが持つ値への参照というのは、その参照を受け取った側のスレッドから見るといつまで生きているかわかりません。 &'static T
ならプログラム終了時まで生きている保証がされているのですが、 大体の場合参照は 'static
ではない ので、「参照(あるいは 'static
ではない値)はスレッド間を跨げない」と考えていました。
そう理解していたので、 Sync
トレイトという Send
に似た並行処理の話題で出てくるトレイトが理解できませんでした。というのも、 Sync
の定義が以下だからです...
The precise definition is: a type T is Sync if and only if &T is Send.
和訳: 「型 T
が Sync
であることと、その参照 &T
が Send
であることは同値である」
... ...???
「 どうせスレッドを跨げない参照が Send
でも意味を為さなくないか ...????ドユコト...?!?」
ここで理解が止まってしまい、永遠に謎だったのですが、第28回 に登場した スコープ付きスレッド std::thread::scope
がこの疑問を解決してくれました...
スコープ付きスレッドなら、「そのスコープより長寿な参照」を渡せます!
use std::thread;
fn main() {
let v = String::from("Hello!");
let r = &v;
thread::scope(|scope| {
scope.spawn(|| {
println!("{} 1", r);
});
scope.spawn(|| {
println!("{} 2", r);
});
});
}
Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=8a7c01287e4bfcf64bb700573ce2b2ee
「参照であるかどうか」と「転送可能であるか否か (Send
か)」には関係がないことをこのメソッドのお陰で理解することができました! ('static
でない) 参照でもスレッドを跨いで良い のです!
そう理解してから改めて「 Sync
トレイトとはなんなのか...?」についても色々考えたのですが、それはぜひ第33回を読んでみてほしいです!
所感
100 Exercisesを通して、どうして自分がRustを好んでいるのか、また一段と理解できた気がします。
Rustが好きな理由は、「特にトレイトを始めとした型に関する知識・説明だけでどのような挙動をするかを端的に伝えられる」ことです。勘違い3選1つ目の Copy
トレイトがわかりやすいですね。他言語だと新しい概念や構文を知る時に新しい文法や背景を知る必要があったりしますが、Rustでは「 ◯◯トレイトが実装されているから 」「 ◯◯型が使われているから 」の一言だけで理解できたり、伝えられたりすることが多いです!トレイトや型を通じて言語仕様を学べるという、この一貫したUIが好きなんじゃないかなと思います。
100 Exercisesを一通り完了させましたが、このエクササイズでは語られていないRustの深淵はまだまだたくさんあります、今後も精進していきたいです!(急な小並感)
ここまで読んでいただき、ありがとうございました!