本セッションの登壇者
私はマップボックスジャパン合同会社という会社でソフトウェアエンジニアをしているyukiです。Twitterでは@helloyuki_で、TechFeedではRustの公認エキスパートとして活動しています。最近は本や連載記事を書くことも多かったのですが、そのほかにはRust Tokyoの運営をしたり、「This Week in Rust」というRustの注目記事をピックアップしたWebマガジンの日本語記事レビュアーをしたり、TwitterでRustのお役立ち情報を発信したりしています。
MapboxでのRust
本日のイベントはMapboxがスポンサーをしていますので、どのようなことをしている会社かを簡単に説明すると、主に地図技術やナビゲーションのAPIを作っているアメリカの会社です。実は社内ではRustをメインで使用しているわけではなく、どちらかというとTypeScript、JavaScript、C++のコードが多く、一部コンポーネントでRustを使用しています。最近ベラルーシオフィスのC++エンジニアがRustを書いているのを見て少しテンションが上がりました。
弊社では高トラフィックなサーバや検索などにRustを利用しており、負荷が高い/速度が求められるところにRustを利用する傾向が高まりつつあります。また、Linux向けの組み込みソフトウェアをRustで書いてみてはどうかという提案が社内であり、このような使い方も広がるのではないかと思っています。
発表の前提と目的
本日は中上級者向けの発表として、Rustの基礎文法に習熟していることを前提としており、既存の文法や機能の解説はほぼしません。また、私自身の理解でお話しますので、もし間違っているところがあればご指摘ください。
今回の発表では、私個人が「入って嬉しかった」、またはRFCなどを読んだうえで「入った背景がおもしろかった」と思った機能/変更を紹介しようと思います。
2022年の変更をおさらいする - 標準ライブラリ/文法機能面/周辺ツール
2022年は2021年に比べると多くの機能が入ったという印象があります。「GATs (Generic Associated Types)がよかった」「let-else
が便利すぎる」「cargo add
が嬉しい」「Mutex
周りがちょっとおもしろかった」など2022年は総じて豊作な1年だったと思います。
そこで、2022年の変更を次の3つのグループに分けてご紹介し、おさらいしたいと思います。
- 標準ライブラリへの変更
- 文法機能面への変更
- 周辺ツール(
cargo
など)への変更
標準ライブラリへの変更
標準ライブラリへの変更の中で個人的に大きいと思ったのは、次の2つです。
- [1.62, 1.63]
Mutex
の内部実装への変更とconst
文脈化 - [1.63] Scoped Threadの導入
[1.62, 1.63] Mutex
の内部実装への変更とconst
文脈化
Mutex
(およびCondvar
/RwLock
)の内部実装はバージョン1.62で変更され、同時にLinuxのシステムコールのひとつであるfutex
を使うように修正されました。また他のOSでも相応に修正されていて、修正の恩恵を受けられます。これにより、メモリ使用量の削減とパフォーマンスの向上が見込めます。
さらにこの修正の副産物として、ヒープ領域を使う必要がなくなるため、Mutex
がconst
文脈で使えるようになりました。従来はMutex
をOnceCell
やlazy_static
で囲ってstatic
にする必要がありましたが、その必要がなくなります。
従来、Mutex
はBox<pthread_mutex_t>
をラップしたものでしたが、pthread_mutex_t
を直接利用することはやめ、Linuxにおいてはfutex
を直接扱うように調整したことでBox
を剥がすことに成功しています。左の修正前のコードではBox
がありましたが、右の修正後の実装ではAtomicI32
に変わっており、Box
が剥がれています。この点については今日は詳しく解説しませんが、後ほどご紹介するZennのスクラップに詳しく書いています。
この修正はRustコアチームのMara Bos氏が中心になって実施されましたが、そのときの話をRustConf 2021の動画で知ることができるのでおすすめです。課題を細かく分割し、解決できそうなものから解決するというエンジニアリングのアプローチをとった話があり、プルリクエストの差分と合わせて見ていくと大変おもしろいかと思います。ちなみにこの修正にはけっこうな年月がかかっており、大変な苦労もうかがえます。
この修正で内部実装にBox
が必要なくなったため、従来はOnceCell
などのクレートと組み合わせてstatic
でMutex
を利用していましたが、その必要はなくなり、Mutex
を使うコードがきれいに書けるようになりました。これは個人的にも大きな修正だと感じていて、 経緯も含めて自分の開発でも参考にしたいものでした。
[1.63] Scoped Threadの導入
Scoped Threadはバージョン1.63で導入されました。従来のstd::thread::spawn
によるスレッドの利用では、キャプチャする変数にstatic
ライフタイムが求められるため不便な場面がありました。この修正により、std::thread::scope
ならびにスコープのspawn
を経由したスレッドの利用が可能になり、これらを利用するとstatic
ライフタイムは求められなくなりました。あまり実装を深く追えていませんが、ライフタイム周りをきれいに整理して設計し直した良い例だと思います。
こちらは従来の実装例です。std::thread::spawn
はクロージャのライフタイムにstatic
を求める(スレッドはローカル変数を借用できない)ため、clone
するかArc
に包んで渡す必要がありました。
scoped_thread
を利用すると、このケースにおいてはclone
は不要になり、スコープの終わりで勝手にjoin
するようになっているので、join
の呼び出しも同時に不要になります。
余談ですが、実はRust1.0時点でもScoped Threadは存在していました。しかし、「Leakpocalypse」と呼ばれる問題が発見されたことで削除され、数年の時を経て標準ライブラリに戻ってきたという経緯があります。
このLeakpocalypse (Leak + Apocalypse)はRustでは有名な話で、「参照カウンタで循環参照を作ってJoinGuard
をリークさせると、Scoped Threadが解放されたメモリにアクセスできてしまう」というものです。現在のRustは静的検査などでメモリリークを防ぐことを保証していませんが、Leakpocalypseが理由のひとつといわれています。詳しくはPre-Pooping Your Pants With Rustや当時の焦り具合がよくわかるIssueをご参照ください。
文法機能面への変更
文法機能面への変更からは次の4つをご紹介します。
- [1.65] Generic Associated Typesが利用可能に
- [1.65]
let-else
が利用可能に - [1.62]
enum
のデフォルトのヴァリアントを指定可能に - [1.64]
await
はIntoFuture::into_future
に脱糖されるように
これらのうち1と2は最近入ったもので、これから使っていきたいと思える変更です。
[1.65] Generic Associated Typesが利用可能に
Generic Associated Types(GATs)は関連型にジェネリクス(型引数や型パラメータという言い方をすることもあります)、あるいはライフタイム注釈を付与できるようになるもので、バージョン1.65から利用可能になりました。GATsにより、Rust全体の型まわりの抽象化の表現力が向上しました。
次の例は、イテレータをGATsを使って実装し直すもので、従来はtrait
の関連型にライフタイム注釈や型引数をつけることはできなかったのですが、それがGATsで可能になったことで、関連型単位でのライフタイムを持たせるように実装できるようになりました。
私は実務でGATsを使ったことはまだないのですが、どのような使い方ができるかは考えているところです。
このような機能は「高階カインド型(Higher-Kineded Types)」と呼ばれるもので、関数型プログラミングがお好きな方からすると「モナドが実装できる?」と思われるかもしれません。しかし、最近GATsが安定してからいろいろと実装しながら調査している方もいて、その結果によるとGATsだけでモナドを実装するのはまだ難しそうということです。また、RFCにも「GATsがすぐさまモナドを実装可能にするものではない」という記述があります。
[1.65] let-else
が利用可能に
let-else
も1.65から利用可能になり、if let
構文(例: if let Some(b) = a {} else {}
)をlet Some(b) = a else {}
と書けるようになりました。この変更により、ネストが減ってコードの見た目がスッキリしたり、処理の流れが読みやすくなったりというメリットを享受できるようになります。
こちらのコードはif let Some
をlet-else
で書き換えた例です。コメントアウトされた元のコードにはネストがありますが、let-else
を使うことでスッキリと書けていて、便利なのでこれから使っていけると思います。
ただし注意点として、else
の中には発散する型(never型; !
)を返す式を書く必要があります。先ほどの例では、else
内でreturn Ok()
としていますが、これをOk()
だけにすることはできません。let-else
はあくまでif let
の拡張/類種のようなものですので、else
側が返す型がlet
側の型と合っていなければならず、型を合わせるために発散する式(never型)を使うことになっています。
ここでnever型について解説しておきます。Rustでは!
として表現される型をnever型と呼び、loop
/ continue
/ return
は実はこの型を返しています。never型は値を何も返さないことを表現します。雑な言い方をすると、never型を返すとどの型にでもなることができるということで、たとえば返り値の型としてi32型を要求する関数内でnever型を返しても問題はありません(never型がi32型と同等とみなされます)。todo!
マクロもnever型を返し、まだ実装していない・これから実装することを示すために利用されています。let-else
のelse
側でも同様の理屈で型合わせが行われています。
このnever型はコンパイル時のデータフロー解析に使われ、never型を返す関数のあとにある処理には決して到達しないことを示しておき、以降の命令を成果物に含める必要がない、などの判断のために存在しています。
[1.62] enum
のデフォルトのヴァリアントを指定可能に
この変更はバージョン1.62で利用可能になり、#[default]
というアトリビュートをヴァリアントに指定して、Default::default
に対応するヴァリアントを指定できるようになりました。ただし、ユニット型のヴァリアントのみに付与できるという制約があります。
この例のように、State::default()
を呼び出すと#[deafult]
がついたヴァリアントが取得できます。
[1.64] await
はIntoFuture::into_future
に脱糖されるように
この変更はバージョン1.64で導入され、個人的に気になりました。for
式は内部でIterator
に脱糖されますが、同様に**await
でもIntoFuture
トレイトを実装しておけば脱糖される**ようになります。HTTPクライアントのクレートなどで、send
を呼び出してFuture
化していた部分はawait
と書くだけでよくなります(現時点ではreqwest
はまだ直っていないようですが、このような効果が期待できます)。
なお、脱糖とは、左のコードのように書くと、Future
で処理をするように内部で右のようなコードに直してくれるようなイメージです。
周辺ツールへの変更
私が注目しているのは次の3つです。
- [1.60]
cargo build –timings
が安定化 - [1.62]
cargo add
でクレートの依存を追加できるように - [1.64] ワークスペースの共通設定が書けるように
[1.60] cargo build –timings
が安定化
cargo build –timings
はどのクレートのビルドにどれくらい時間がかかったか計測したり、ビルド時のリソースの使用状況などを計測したりできる機能です。nightlyにはすでにありましたが、バージョン1.60で安定化しました。
このコマンドを使うと、各クレートのビルドにかかった時間を把握できるので、ビルドのパフォーマンスチューニングなどに利用できます。
[1.62] cargo add
でクレートの依存を追加できるように
これは嬉しかった機能で、コマンドひとつでクレートの依存をCargo.toml
に追記してくれるものです。もともとcargo-edit
というプラグインでしたが、その一部のコマンドをRust本体に取り込んだという経緯があります(ただし、cargo-edit本体の挙動とは微妙に異なるものもあります。また、そもそもまだ移植されていないコマンドもあります)。
[1.64] ワークスペースの共通設定が書けるように
cargo
にはワークスペース機能がありますが、ワークスペースのルート側のCargo.toml
に共通設定や共通依存クレートの設定を定義できるようになりました。たとえば、anyhow
などはプロジェクトをまたいでも利用するクレートですが、プロジェクト間で共通の設定を利用できるようになるので便利です。
こちらはルート側のCargo.toml
にanyhow
とtokio
を共通依存クレートとして設定しておき、使用するプロジェクト側のCargo.toml
ではanyhow.workspace = true
として共通設定を使用することを指定しています。もちろん、プロジェクト個別で使用するクレートも記述できます。
実務での新機能への対応
最後に、私が実務で新機能にどのように対応しているかを共有したいと思います。
正直なところあまりまじめにやっているわけではなく、取り入れたいものがあったら取り入れるようにしていて、無理に新機能を取り入れることはありません。たとえば、先日let-else
を使いたくなったので、チームでRustのバージョンを1.65に上げて、これからはlet-else
に変えられるところは変えていこう、ということになりました。
新機能への対応/キャッチアップのスタンスは下記の通りで、基本的にはリリースノートに軽く目を通しておいて、使いたいときに使う程度で、積極的にキャッチアップしているわけではないです。
- 標準ライブラリへの変更 - docくらいは読んでおいて、使えるときに思い出して使う
- 文法や機能への変更 - リリースノートは読んでおいて、使えるときに思い出して使う
- 周辺ツールへの変更 - 何が入ったかは覚えておいて、使えるときに思い出して使う
なお、Mutex
の変更のようにパフォーマンス面で恩恵を受けられる修正が入ることもあるので、バージョン自体はこまめに上げるようにしておいたほうがいいかもしれません。ただしバージョンを上げると一部のクレートがビルドできないなどの問題がでることもあるので、受けられる恩恵とのトレードオフがあります。
まとめ
今回の発表では、2022年にRustに入った機能/変更を駆け足で振り返りました。時間の都合上、細かい議論は省略しましたが、詳しくはZennのスクラップにまとめていますので、よろしければご参照いただき、本日のスライドに含まれる各資料へのリンクもたどってみてください。
以上で発表を終了します。ありがとうございました。