Rustを実践的に学ぶには
Stack Overflowの愛され言語ランキングでここ最近ずっと一位のRustを、2020年の年末から本気を出して学んでます。ようやくまともに書けるようになったので、どんなやり方で学んだかを簡単にまとめたいと思います。
著名なRust関連の書籍は買いそろえて学習していたのですが、実際にモノ作ったほうが早いよねということで、ULID生成器を実装してみました。ちょっと前にScalaでも同様に作っていたので、ちょうどいい習作課題でした。
実際の実装は以下。
学習の進め方
書籍などの情報は、以下を中心に読みました。というか、一通りの概念を把握するという感じです。すでに何かしらのプログラミング言語を扱える人なら(Rustを第一言語にする人少なそうですが…)、どういった機能があるか、浅く理解しておくとよいと思います。そのうえで、実装の際に具体的な知識が必要ならば、「あ、このあたりはあの書籍に書いてあったな」とか「えーっとこれは所有権というやつか」ということでキーワードを想像できれば、深掘りできるので、そういう状態になっておけば大丈夫です。
- 書籍
- Web
実装するにあたって
言わずがな、何か題材を決めましょう。これがないと始まらない。新しい課題や問題を見つけてそれをRustで実装してもよいですが、Rustの学習だけに集中できなくなるのでお勧めしません。僕は既存の言語なりで実装したライブラリをRustで書き直すことにしました。
Scalaで実装したULID生成器をRustで書き直すことにしました。
性能評価はこの記事に書いています。ULIDを生成して文字列にするまでのレイテンシは95%tile:460 ns。
ULID生成器をScalaで実装してみた
Rust版では 117.26 ns になりました。ほぼ4倍ぐらいのパフォーマンスを発揮します。
実装方法
- 普通にScalaの実装を真似たRust実装を書いていく
- 行き詰まったら前述の情報やrustdocなどを読む
- 既存のRust実装を読んで参考にする
- ULIDに関しては以下
- 他にもよく使われる
rand,uuidなどのクレートのコードも読んでみる
- ベンチマークツール criterion を使って他のクレートとの性能差を確認する
- これはMUSTではないと思いますが、性能差があるということは実装に差異があるということなので調査して、Rustでの効率のよい方法を学ぶ
躓いたところ
- 🤔 メソッドのレシーバー部分の選択肢には、
self,&self,mut self,&mut selfがある- 呼び出し側がデータをコピーするタイミングを決める (C-CALLER-CONTROL)が参考になった。
- これは
Selfに限った話ではないですが、&selfで受けてself.clone()するならば最初からselfのほうが、呼び出し側がクローンするかしないかを決められるので柔軟性がある。逆に借用で済む処理なのに所有権を奪うのはよくない。
- 🤔 メソッド名の命名規則がよくわからない。
as_,to_,into_などいろいろある。- 変換メソッドにas_, to_, into_を使っている (C-CONV)の表が参考になった。メソッドを定義する際もこれを参考にするとよい
- 🤔
std::fmt::Displayを実装することで自動生成されるToString::to_stringの実装が遅かった…-
Displayを実装しつつ、to_stringを自動生成させないようにするのが面倒だった
// Displayを実装する impl std::fmt::Display for ULID { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.to_string()) } } // ... impl ULID { // 自動生成されるto_stringをshadowするようにULID::to_string(&self)を実装する #[allow(clippy::inherent_to_string_shadow_display)] pub fn to_string(&self) -> String { String::from_utf8(append_crockford_u128(self.0).to_vec()).unwrap() } // ... } -
- 🤔 公開するクレート名はユニークでなければならない。domainとかgroup_idみたいなものがほしい
-
🤔 cargo publishでリリースするのは面倒特別な設定なしにクレートを公開できるのは素晴らしいが、リリース作業はクレートを公開するだけではない。タグを作ったりバージョン番号をbumpしたりする必要があるhttps://github.com/sunng87/cargo-release が使えそうだが、まともに動作しない…- バージョン番号のbumpだけなら、https://github.com/wraithan/cargo-bump で可能
- みなさん、どうされているのだろうか…
- 👍 最新版のcargo-releaseで無事リリースできました。(1/5付)
- 🤔 ローカルの開発環境にあれこれインストールすると、何かあったときに再構築が面倒なので、Docker内に開発環境を作った方がいいかもしれない??
その他の感想など
- 👍 環境構築がラク。
rustupだけあればなんでも揃う - 👍 ビルドエラーがわかりやすい。エラーに対する対処方法の提案もセットになっている
- 👍 Scalaの
Option型と同じようなOption型がある!nullが型として存在しない - 👍/🤔 Scalaの
Either型と同じようなResult型がある!でもResultって名前微妙だなぁ - 👍
Option型やResult型で使える?演算子が便利すぎる- Scalaの
for-yeild式のように、正常系の処理だけを記述できる
- Scalaの
- 👍 ScalaのコレクションAPIやJavaのStream APIと同じような
IteratorのAPIがある-
filter,map,foldなど普通に使えます
-
- 👍
Fromtraitを実装するとIntotraitも実装される - 👍 rustdocが素晴らしい
- docに普通にMarkdownが使える
- Markdown中に記述したコードにlintがかかる。IDEでも補完が可能。コンパイルできないExampleは存在しないことを意味する。これはすごい
https://twitter.com/j5ik2o/status/1346319326521905153
- 👍
cargo-readmeを使うとdocの情報を使って、README.mdを自動生成できる。{{version}}の展開とか便利すぎる$ cargo readme >! README.md # README.tplからREADME.mdを生成できる - 👍
clippyの指摘が有益- 実際にパフォーマンス劣化する部分の指摘とコードの改善提案まであった
- 👍 Featureフラグが便利
- オプションで提供する機能をFeatureフラグで有効にすることができる。つまりデフォルトのビルドに何でもかんでもいれない→ビルドの高速化に繋がる
- Features - The Cargo Book
- [Rust] フィーチャーフラグの使い方
- 👍 ベンチマークツール criterion が使いやすい
- 前回との違いをグラフで確認できたりする
- 👍
u128,u64などULIDに向いた数値型が扱える - 👍
rand::rngs::ThreadRng::genが使いやすい- ULID生成時に乱数を生成している。乱数生成API
ThreadRng::genは一度にu16,u64に分けて乱数を取り出せる
let (most_rnd, least_significant_bits): (u16, u64) = self.rng.gen(); let most_significant_bits = timestamp << 16 | u64::from(most_rnd); Ok(ULID::from((most_significant_bits, least_significant_bits))) - ULID生成時に乱数を生成している。乱数生成API
まとめ
最初なので試行錯誤はかなりしましたが、Scalaと似ている部分がかなりあったので、そんなに苦労しなかったです。
みなさんが難しいという所有権システムはそれほど難しくないというか、本当に難しいのはライフタイムのほうです。今回のようにシンプルなモジュールであればさほどライフタイムに悩む部分はなかったです。ライフタイムについては、一通り例題を解けるのであれば、実際にコードを書きながら学んだほうがよさそうです。
ということで、結果的に不満なところはあまりなかったです。やはりツール類とドキュメントがそろっていると捗りますね。
Discussion
ちょっと気になったのですが、cargo-releaseはどの辺がダメでしたか?
自分がメンテナンスしているクレートは全部cargo-releaseでリリースしていますし
手元の環境ではulid-generator-rsリポジトリでも動きそうでした。
もし不具合がありそうなら直してみようと思います。
最新のバージョンで確認したら、問題なくリリースできました。ご指摘ありがとうございます!本文のほうは取消線いれておきます!