こんにちは😉 @ryokotmngです。 今日は社内ドキュメントの、Rust初心者向けのクックブックを公開しようと思います。 私自身コードを書くのに四苦八苦していた頃にとても助けられたので、Rustをはじめたばかりの方の参考になれば嬉しいです。
目次
[toc]
はじめに
この記事では、The Bookに記載されている知識を前提としています。 Rustを全く書いたことがない方は、先に読んでみることをお勧めします。
サンプルコードが結構長いこと、実行環境があった方が良い内容も多いことから、サンプルコードは大体Rust Playgroundのリンクとなっています。 ぜひご自身で修正して遊んでみてください。
単位つきの計算を型で厳格に縛る
例えば複数の長さの単位 (mm, cm, mなど) を扱う場合に、単位が合っていない長さ同士の計算をする場合、単位を揃える必要がありますね。 この時、最終的に欲しいのは1つの「長さ」、つまりプリミティブな数字のデータになるでしょう。 計算を行うとき、最終的に得たい「長さ」の単位は1つになるので、コード上では単位を比較して異なる場合はエラーを返す、もしくは、異なる単位の長さ同士の計算の場合はある単位に換算したうえで計算できる状態にする、のどちらかの処理が必要になります。 しかし、単位を型として定義すると、計算の実装をする際に単位をチェックするようなコードを書くことなく、異なる単位の長さ同士の計算を実装しようとしたらコンパイルエラーを出すことで、意図しない挙動を防いでくれます。 また、下記のサンプルコードのように、traitを使って単位の変換処理を実装することもできます。
上記のサンプルコードには、型の書き方の他にも、以下のような多くの知識が詰め込まれています。
- Rustのenumが、JavaScriptでいうUnion型のような使い方もできること
- ジェネリクス
- PhantomData (幽霊型)
これらの概念を知らなくてもさらっと読んで雰囲気を掴むこともできますが、よく使うテクニックのはずなので、慣れていない方はそのような概念をひとつずつ調べながら読むことをお勧めします。The Bookにも記載されているので、ぜひ読んでみてください。
参考 (The Book):Enumを定義する、ジェネリクス、幽霊型パラメータ 参考 (外部ブログ):Rust で Phantom Type (幽霊型)
なお、RustベースのWebエンジンであるServoの内部実装でも、このパターンが使われています。よければ参考にしてみてください。
エラーハンドリング
Rustは基本的に、f() -> Result<T, E>
型でエラーを伴う処理を表します。
T
が成功した場合の値, E
がエラーだった場合の型です。
Result<T, E>
型は、パターンマッチでT
とE
のどちらに値が入っているのか判定できるので、以下のように使うことができます。
match f() {
Err(e) => //エラー処理(eはE型の値),
Ok(r) => // 成功したときの処理(rはT型の値),
}
これを利用してエラーハンドリングすると、以下のような処理を書くことが出来ます。
更にRustでは、?
オペレーターを利用して以下のように書くことも出来ます。
?
を利用するために下準備で必要となるコード量が多いため、実際には以下のcrateを利用したりして使いやすくすることができます。弊社では errer
crateを利用しています。
OptionとResultに対する処理にcombinatorを使う
Option
とResult
について処理を行う場合、match式で場合分けを行いながら処理を進めることも出来ますが、combinatorを使うと短く書くことができて便利です。
なお、Productionでは、unwrap()
, expect()
は処理が失敗した場合にpanicを返すので原則使わない方が良いでしょう。
(テストやサンプルコードで使うのは問題ありません。
また紛らわしいですが、unwrap_or***()
系のpanicを出さないものは問題なく使えます。)
Productionコードでは、Result
を返す小さい関数を、and_then()
などのcombinatorを使って合成し、大きい処理を表したりするのに使います。
また単純なエラーハンドリングの場合は、combinatorで頑張らなくても、上記に挙げた?
オペレーターで処理もできるので、読みやすいように適宜調整すると良さそうです。
Rustでは厳密にはMonadはありませんが、考え方は使えるので以下の記事などで少しでも理解しておくと理解しやすいでしょう。 参考:箱で考えるFunctor、ApplicativeそしてMonad
なお弊社では、anyhow
クレートを使用しています。エラーにコンテキスト情報を含める機能 (with_context
) や便利なマクロ (bail!
など),独自のエラー型などを提供しています。
collect() で Vec<Result<_>> と Result<Vec<_>> を相互に変換できる
イテレータの処理でcollect()
を実行し、返り値としてResult<Vec<>>
がほしいと仮定します。
単純にcollect()
を実行した結果、Vec<Result<>>
が返り値となった場合でも、その逆の形に簡単に変換することができます。
以下引用
fn main() {
// 全てSomeならSome(配列)を返し、どれかがNoneなら全体もNoneになる
assert_eq!([Some(1), Some(2)].iter().cloned().collect::<Option<Vec<_>>>(),
Some(vec![1, 2]));
assert_eq!([None, Some(2)].iter().cloned().collect::<Option<Vec<_>>>(),
None);
}
引用元:RustでOptionやResultの配列ができてしまったときの一般的なテク4つ
このテクニックを知らないままVecの複雑な処理に直面すると絶望的な気持ちになるので、すぐには使う場面がなくても、「こんなことができるんだな」くらいに覚えておく価値はあると思います。
_
の表す意味
- 変数名に使う => 変数が未使用であることを宣言する
- 型の一部として使う => 型推論してねとRustにお願いする
コードの公開範囲(public/private)
Rustで定義したものはデフォルトでprivateで定義されます。定義されたモジュールの外で利用する場合はpub
キーワードで公開することを宣言する必要があります。
また、pub(crate)などと指定することにより、公開する範囲を限定することができます。 参考:Visibility and Privacy
注意点 タプルも同様に、デフォルトの公開範囲はprivateです。
// Sampleタプルは外に公開されている
// この場合、タプル内部のStringは非公開
pub Sample(String);
// このように内部にpubをつけることで公開すことができる
pub Sample(pub String);
PRを出す前にやっておきたいcargoコマンド
プッシュする前に、下記のコマンドを実行し、エラーがないことを確認しておきましょう。
cargo build
アプリケーションをビルド
cargo test
テストコードを実行
cargo clippy
Linterで構文チェック rust-lang/rust-clippy
cargo fmt
formatterにかける rust-lang/rustfmt
副作用のある処理をMock化して、実装を切り替えられるようにする
弊社では、ビジネスロジック(ドメイン層など)などは、クリーンアーキテクチャで言う外側の層に影響されないように記述しています。 例えば、ビジネスロジックの単体テストを行うのに、DBやAPIなど外部のシステムと連携したテストを作成するのは、環境構築等色々な前工程を行う必要が生じるため辛いことになります。 このため、外部リソースを使って計算を行うロジックはロジック部分と外部の連携部分を切り分けたくなります。
以下のサンプルコードでは、実際のビジネスロジックは、その外部リソースを扱う処理に直接依存するのではなく、「外部リソースの扱い方を定義したtraitに依存するように記述する」ことで処理の分離を実現しています。
turbofish(::<>)
turbofishとは型注釈の一種で、型を引数のように関数に対して与える表現方法です。
例えば、strに対するparse()
メソッドの型定義は以下の通りです。
pub fn parse<F>(&self) -> Result<F, <F as FromStr>::Err>
where
F: FromStr,
ここで F
は、FromStr
を実装している型となるように抽象化されています。
つまり、実際に使う時にはコンパイラが F
の型を推定できないと、str
型を何に変換すればいいか特定できないでしょう。
この時、推定できるように記述する方法が2つあります。
// 1. 型が決まるように束縛するxに対して型注釈をつける
// なお、xの型注釈は Result<i32, _> のように省略可能
// => 理由は以下のturbofishの例に記述
let x: Result<i32, ParseIntError> = "10".parse()
// 2. turbofishを使う
// この際turbofishとしてはi32になることが特定できれば
// エラー型はFromStrの定義からError型の具体型が特定できる
let x = "10".parse::<i32>()
(おまけ) cargoの独自コマンドを作る
cargo bookに、「$PATHにcargo-XXX
というバイナリが入っていたらcargo XXX
でcargoのサブコマンドのように実行できる」との記述がありますが、これはバイナリに限った話ではなく実行権限がついていれば大丈夫です。なので、簡単なshellを登録しておいてcargoから実行することも可能です。
(例)
#!/bin/sh
rg -l todo:
上記のshellを$PATHの通る場所に置いておくと、cargo todolist
でtodo:
のあるファイルを探してくれます。出力がPATHになるので、弊社では、VSCodeのターミナルで実行 -> PathをCtrl+クリックで該当ファイルに飛ぶなどに利用している人もいます。
shell自体をPATHにあるところに置いて呼び出せばいいじゃんという声が聞こえてきそうですが、cargo --list
で利用できるサブコマンドの一覧を取れるところが強みです。
いかがでしたでしょうか?
Rustはすらすら書けるようになるまでが難しい言語だと思います。私の場合は、Rustを書き始めて2ヶ月くらいの間は、他の言語ではスラスラと書けたようなロジックでも全然書けなくて、とても悲しい気持ちになりました。 ですが、書けるようになってみるとやっぱり良いところもたくさんありますし、勉強すればするほどその強力さがわかって楽しくなってくるなと感じています。
最後に、弊社でRustを長く書いているエンジニアに勉強のコツを聞いてみたのですが、「The Bookは論理から非常によくまとまっているため、何度も読むと良い」というアドバイスをいただきました。本記事のようなテクニック的なところではなく、Rustのコード自体読んでもよくわからないと言う方は、The Bookを読み直すと良いかもしれません。何度読んでも学びがある内容なので、損はないと思います。
この記事を読んでいる皆様が、Rustを楽しんでくれることを願っております!
We’re hiring!!!
キャディでは、エンジニアを含め全職種積極採用中です! Rustを使って開発がしたい方、会社に興味を持ってくださった方、気になるから話を聞いてみたいという方、ぜひ面談にお越しください。
弊社がRustを採用している背景や、実際に開発してみてのメリットデメリットなどは、「Rust についてカジュアル面談で頻繁に訊かれる質問と、それに対する個人的な回答」をご参考ください。
ご応募はこちら カジュアル面談のお申し込みはこちら 募集職種一覧
長文お読みいただき、ありがとうございました!