[社内勉強会レポート]『The Rust Programming Language』勉強会#2

クラスメソッド 福岡オフィスで iOS アプリケーションエンジニアとして働いている田辺です。先週に続き、社内で Rust の勉強会が行われたのでそのレポートを記事にします。

前回の記事。

数当てゲーム

引き続き学習を進めつつ残したメモや理解するために別で調べる必要のあった概念などを列挙していきます。業務では Swift でコーディングしているので Rust の説明をしているのに Swift の話が頻繁に出てきます。ご容赦ください。

match

Swift でいう switch と同じようなことができます。ただ Swift の switch は文なので値を返しません。それに対して match は式です。式なので代入できます。

数当てゲームの実装では列挙型 Result に対して match 式が使われています。

String に対するtrim()メソッドで、両端の空白を取り除いて parse()メソッドで文字列を解析して数値型にします。

Rust の数値型は多数ありますが、型アノテーションに u32 を指定しているのでそれに基づいて u32 に変換されます。この動きから parse()メソッドがジェネリックな関数だとわかります。

そういう関数は Rust ではどういう風に定義するのか気になるのでシグネチャを見てみるとparse<F: FromStr>(&self) -> Result<F, F::Err>とありました。

FromStr はトレイトなのでジェネリックな関数を理解するにはトレイトについて知ることが必要だとわかります。まだ勉強会ではトレイトの詳細な仕様に入っていないのでいったんトレイトには触れずにおきますが、型パラメーターに FromStr を指定しています。<>で型パラメーターを指定する方法は、Swift でのジェネリックな関数の定義方法と同じです。

余談ですが Swift ではジェネリックな関数を定義するために Generics という言語機能が提供されています。それを使って定義した関数の型や戻り値に class や struct、protocol の型を割り当てたり、protocol の associatedtype で汎用的な型で関数を定義したりできます。

parse の戻り値はResult<F, F::Err>で F はFromStrなので match によるパターンマッチが可能で、数字型へ変換できたらそのまま guess に値を代入します。変換に失敗した場合は、後述する loop によって繰り返し入力させられるように continue としています。

Rust における式と文について

Rust は式がベースになっています。式は値を返します。;で終わると文になり、値を返しません。関数の戻り値の行に;をつけないのは式として扱い、値を返したいからです。たとえば、値を返さない let は、let a = 1 let b = 2のように書き方ができません。1 行で書く場合、正しい書き方はlet a = 1; let b =2;

Cargo に関する追記事項

前回、記事に書き忘れていたことが2つあります。1つ目は Cargo のバージョン管理についてです。

Cargo は Cargo.toml の[dependencies]セクションヘッダの一番下に導入したいクレートを追記していきます。そして cargo build コマンドを実行するとレジストリから最新バージョンを取得して、取得していないクレートをダウンロードします。Rust のコンパイラは依存ファイルをコンパイルして利用可能な状態でプロジェクトをコンパイルします。

このとき、Cargo.lock というファイルが生成されます。Swift のプロジェクトで依存ライブラリの導入・管理に使われる CocoaPods というツールもpod installというコマンドをたたくと Podfile.lock というものが生成されます。これらの.lockという拡張子がついたファイルはそれぞれのツールで同じ役割を担っています。

Cargo ではビルドの際に依存のバージョンを計算して Cargo.lock に記述します。その後は cargo build を叩いてもその時点での利用可能な最新のバージョンを計算する作業は行わず、Cargo.lock ファイルを参照して再現可能なビルドを構成します。

もしクレートを更新したければ cargo update というコマンドを実行します。そのときに Cargo.lock ファイルは更新されます。それ以降は再度 cargo build すると…ここからは同じ説明になるので省略します。

2 つ目は便利な cargo コマンドについてです。ローカルで依存しているクレートのドキュメントをビルドしてブラウザで閲覧できる機能が cargo に用意されています。cargo doc —openです。実行すると次のようにブラウザからドキュメントを閲覧できます。

loop

loop を使用することで何らかの終了状態に到達するまでブロック内の処理をループし続けます。これに各処理を入れて数当てゲームを何度でも繰り返せるようにします。

これまで書いたユーザーの入力を受け取って出力するだけの処理をブロックの中に書くと入力してその文字列を出力して、という操作を繰り返せます。

Rust の型推論

数取りゲームの最終的なコードは次のようになりました。

このうち 20 行目の num を実数にしてみると次のようなエラーが返ってきてビルドが失敗します。

Compiling guessing_game v0.1.0
...
error[E0284]: type annotations required: cannot resolve `<_ as std::str::FromStr>::Err == _`
  --> src/main.rs:19:45
   |
19 |         let guess: u32 = match guess.trim().parse() {
   |                                             ^^^^^

error: aborting due to previous error

error: Could not compile `guessing_game`.

To learn more, run the command again with --verbose.
HL00486:guessing_game tanabe.nobuyuki$

type annotations neededというメッセージがコンパイラにより出力されています。guess の型、つまり parse()の戻り値の型を推論できなくなっています。

実数を num に戻す以外だと parse の戻り値を直接指定する方法でビルドさせることができます。 parse()parse::<u32>()にします。

parse())に明示的に型パラメーターを与えることによりコンパイルが成功します。メソッドの戻り値の型をこんな風に指定できるのは Swift では馴染みがないので少し驚きました。parse()の定義元にジャンプしてみると、parse()メソッドの戻り値の型を指定する方法はturbofishと呼ばれていることがわかります。

このシンタックスは TRPL ではGenerics - The Rust Programming Language に言及があります。

まとめ

今回で数取りゲームの実装が終わりました。 チュートリアルをただ写経して読んで終わるのと違って、疑問を皆で解決したり、経験者の人が補足情報を加えてくれることで少し深く理解できるなと感じました。個人的にはこのような記事を書くのはやったことを整理したり、理解があいまいなところをドキュメントやソースコードを調べたりできるよい機会だと思っています。

最後に社内向けの情報です。この社内勉強会は途中参加の人が困らないように作業ログ用のリポジトリを用意していたり、次回から前回の振り返りの時間を少し設ける予定ですので、Rust に興味がある社員の方は参加してみてください。