どうも、たくチャレ(@takuchalle)です。
今「基礎から学ぶ 組み込み Rust」を読んでます。
そのなかで型状態プログラミング(Typestate Programming)
が気になったので、少し深堀りしてみました。
The Embedded Rust Book(日本語版1)によると
The concept of typestates describes the encoding of information about the current state of an object into the type of that object.
と書いてあります。
「現在のオブジェクトの状態をオブジェクトの型にエンコードする」みたいな意味だと私は理解しました。
The Embedded Rust Book
の最初の例では、Builder Pattern
が紹介されています。FooBuilder
が未設定状態
で、Foo
が設定済み状態
として表現されています。いきなりFoo
を作成することはできず、FooBuilder
経由で適切に初期化してからでないとFoo
を取得できないようになっています。それをRust
の強力な型付けによってコンパイル時に保証してくれます。
このシンプルな例でもなんとなくは分かるのですが、もう少し実践的なPeripherals as State Machineの例を詳しく見ていきたいと思います。
シンプルなGPIO
でも有効/無効や入力/出力などいろいろな状態を持ちます。
マイコンのペリフェラルを一種のステートマシンとして扱い、型状態プログラミングを適用していきます。
具体的なコードはDesign Contractsから抜粋していきます。全体像を見たい場合はDesign Contractsを確認してください。
まずGpioConfig
を定義します。
/// GPIO interface
struct GpioConfig<ENABLED, DIRECTION, MODE> {
/// GPIO Configuration structure generated by svd2rust
periph: GPIO_CONFIG,
enabled: ENABLED,
direction: DIRECTION,
mode: MODE,
}
struct Disabled;
struct Enabled;
struct Output;
struct Input;
struct PulledLow;
struct PulledHigh;
struct HighZ;
struct DontCare;
この時に有効無効をENABLED
、入出力をDIRECTION
、モードをMODE
としてジェネリクスで定義します。
そして状態を表す構造体を定義します。メンバがない構造体なのでサイズが0
で実行ファイルには残らずコンパイル時のマーカとして使います。
こうしておくことで型状態プログラミングを実現することができ、Rust
の強力な片付けを享受することができます。
次にGPIO
を有効化して出力に変更するメソッドを見てみましょう。
impl<EN, DIR, IN_MODE> GpioConfig<EN, DIR, IN_MODE> {
pub fn into_enabled_output(self) -> GpioConfig<Enabled, Output, DontCare> {
self.periph.modify(|_r, w| {
w.enable.enabled()
.direction.output()
.input_mode.set_high()
});
GpioConfig {
periph: self.periph,
enabled: Enabled,
direction: Output,
mode: DontCare,
}
}
}
返り値の型がGpioConfig<Enabled, Output, DontCare>
になってることがわかると思います。
self.periph.modify
で実際のハードウェアの設定を変更して、新しく遷移した状態GpioConfig<Enabled, Output, DontCare>
になっています。
引数が&self
ではなく、self
になってることがポイントかなと思っています。self
を消費しつつ新しい状態として返すことで、GPIO
のインスタンスがプログラム中に一つしか存在することができないので余計なバグを生む心配がなくなります。
最後に実際に出力を行う部分を見てみます。
impl GpioConfig<Enabled, Output, DontCare> {
pub fn set_bit(&mut self, set_high: bool) {
self.periph.modify(|_r, w| w.output_mode.set_bit(set_high));
}
}
GpioConfig<Enabled, Output, DontCare>
に対してメソッドを定義しているので、それ以外の状態では出力が行えないようになっています。
つまり「GPIO
が入力の設定になっているのに出力しちゃって正しく動かない」みたいなしょうもないバグは絶対に起きないことが保証されます。Rust
最高ですね。
C/C++
では出力行う時に状態をチェックして出力する、みたいなコードになるかと思います。
void set_bit(GPIO *gpio, int set_high) {
if(gpio->state != STATE_OUTPUT) return;
// 出力
}
型状態プログラミングを適用することでコンパイル時に状態をチェックできるので、実行時の状態チェックのオーバーヘッドが不要になります。ゼロコスト抽象化
と呼ばれていますね。
めちゃめちゃわかりやすい画像をツイッターで見つけたので、貼っておきます。
Rust愛を感じました😀
— nodamushi (@nodamushi) February 3, 2019
もし、最終的に普及の説得も兼ねているのなら、図で説明した方がいいですよ。俯瞰して分からないものは聞かないし、興味も持ってくれないんですよねぇ…
所有権周りも、unsafeだらけになる未来しか見えないなぁ…
結局は、良いプログラムを書く姿勢が一番重要なのでしょうかね pic.twitter.com/VzYenhHqE9
まさにこれがRust
を使うメリットの一つをわかりやすく表現していますね。
基礎から学ぶ 組み込み Rustに出てきた型状態プログラミング
についてちょっと調べてみました。
Rust
の強力な型付けやゼロコスト抽象化の特徴を効果的に利用した手法で、個人的にはすごく感動しました。特にプロセッサの性能やメモリサイズが限られている組み込みに非常に有用な手法だと感じました。
もちろん組み込み以外でも使える汎用的な手法だと思うので使える場面があればどんどん使っていきたいと思います。
基礎から学ぶ組み込み Rust の著者が日本語訳してくれてるじゃないですか。ありがたや… ↩︎