🧊

RustのType Stateパターンについて

これまでもなんとなくは見てきてたけど、こういう名前があるとも知らんかったので。

こういうやつ

use std::marker::PhantomData;

// 状態をそれぞれstructにして
struct Open;
struct Closed;
struct Locked;

// ジェネリクスで指定し
struct Door<State> {
    // マーカーを置く
    _state: PhantomData<State>,
}

// その型ごとに実装する
impl Door<Open> {}
impl Door<Closed> {}
impl Door<Locked> {}

PhantomDataはその名の通り幻影のような存在感で、コンパイルされると消えるそうな。

PhantomData - The Rustonomicon https://doc.rust-lang.org/nomicon/phantom-data.html

基盤としてのstructは共通しつつ、状態ごとに実装を明確に分けられるのが便利というパターンらしい。

共通の実装がしたい

分けられるのが嬉しいのはわかったけど、分けたくないシーン、サボりたいシーンもあるはず・・・。

共通のstruct側に実装は書くけど、ちょっと分岐したい、みたいな。

こうすればできた。

impl<State: 'static> Door<State> {
    fn can_enter(&self) -> bool {
        if TypeId::of::<State>() == TypeId::of::<Open>() {
            return true;
        }
        false
    }
}

ただこれは、TypeIdでの比較がランタイムコストになるので、PhantomDataの意味が薄れてしまう。

調べる限り、traitにしちゃうのがベターらしい。

trait DoorState {
    const CAN_ENTER: bool;
}

impl DoorState for Open {
    const CAN_ENTER: bool = true;
}
impl DoorState for Closed {
    const CAN_ENTER: bool = false;
}
impl DoorState for Locked {
    const CAN_ENTER: bool = false;
}

impl<State: DoorState> Door<State> {
    fn can_enter(&self) -> bool {
        State::CAN_ENTER
    }
}

コードは増えるけど、よりRustyらしい。

このコード例が簡単すぎてあまり説得力はない(個別に実装したらええやんとなるから)けど、まあやり方の一つということで。

ライブラリもある

共通の実装の話はさておき、本来のType Stateだけに目を向けると、

  • PhantomDataのためのstructが冗長
  • 状態遷移ももっと宣言的に書きたい

となるはずで、それに立ち向かってるcrateを公開してる人もいた。

ozgunozerk/state-shift: Macros for implementing Type-State-Pattern on your structs and methods https://github.com/ozgunozerk/state-shift