Rustを実践的に学ぶには

6 min read読了の目安(約5700字 2

Stack Overflowの愛され言語ランキングでここ最近ずっと一位のRustを、2020年の年末から本気を出して学んでます。ようやくまともに書けるようになったので、どんなやり方で学んだかを簡単にまとめたいと思います。

著名なRust関連の書籍は買いそろえて学習していたのですが、実際にモノ作ったほうが早いよねということで、ULID生成器を実装してみました。ちょっと前にScalaでも同様に作っていたので、ちょうどいい習作課題でした。

実際の実装は以下。

https://github.com/j5ik2o/ulid-generator-rs

学習の進め方

書籍などの情報は、以下を中心に読みました。というか、一通りの概念を把握するという感じです。すでに何かしらのプログラミング言語を扱える人なら(Rustを第一言語にする人少なそうですが…)、どういった機能があるか、浅く理解しておくとよいと思います。そのうえで、実装の際に具体的な知識が必要ならば、「あ、このあたりはあの書籍に書いてあったな」とか「えーっとこれは所有権というやつか」ということでキーワードを想像できれば、深掘りできるので、そういう状態になっておけば大丈夫です。

実装するにあたって

言わずがな、何か題材を決めましょう。これがないと始まらない。新しい課題や問題を見つけてそれをRustで実装してもよいですが、Rustの学習だけに集中できなくなるのでお勧めしません。僕は既存の言語なりで実装したライブラリをRustで書き直すことにしました。

Scalaで実装したULID生成器をRustで書き直すことにしました。

https://github.com/chatwork/scala-ulid

性能評価はこの記事に書いています。ULIDを生成して文字列にするまでのレイテンシは95%tile:460 ns
ULID生成器をScalaで実装してみた

Rust版では 117.26 ns になりました。ほぼ4倍ぐらいのパフォーマンスを発揮します。

実装方法

  • 普通にScalaの実装を真似たRust実装を書いていく
    • 行き詰まったら前述の情報やrustdocなどを読む
  • 既存のRust実装を読んで参考にする
  • ベンチマークツール criterion を使って他のクレートとの性能差を確認する
    • これはMUSTではないと思いますが、性能差があるということは実装に差異があるということなので調査して、Rustでの効率のよい方法を学ぶ

躓いたところ

  • 🤔 メソッドのレシーバー部分の選択肢には、self, &self, mut self, &mut selfがある
  • 🤔 メソッド名の命名規則がよくわからない。as_, to_, into_などいろいろある。
  • 🤔 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のコレクションAPIやJavaのStream APIと同じようなIteratorのAPIがある
    • filter, map, foldなど普通に使えます
  • 👍 From traitを実装するとInto traitも実装される
  • 👍 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フラグが便利
  • 👍 ベンチマークツール criterion が使いやすい
    • 前回との違いをグラフで確認できたりする
  • 👍 u128, u64などULIDに向いた数値型が扱える
  • 👍 rand::rngs::ThreadRng::genが使いやすい
    • ULID生成時に乱数を生成している。乱数生成APIThreadRng::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)))
    

まとめ

最初なので試行錯誤はかなりしましたが、Scalaと似ている部分がかなりあったので、そんなに苦労しなかったです。

みなさんが難しいという所有権システムはそれほど難しくないというか、本当に難しいのはライフタイムのほうです。今回のようにシンプルなモジュールであればさほどライフタイムに悩む部分はなかったです。ライフタイムについては、一通り例題を解けるのであれば、実際にコードを書きながら学んだほうがよさそうです。

ということで、結果的に不満なところはあまりなかったです。やはりツール類とドキュメントが揃っていると捗りますね。