はじめに
『Servoにおける単位系の静的チェック』という記事を読んでから、これを実現する方法がずっと気になっていたのだが、現状、 std::marker::PhantomData
についての日本語文献が見当たらないので、メモ書きとして本稿を描いた次第である。
なお、使用したコンパイラのバージョンは rustc 1.10.0 (cfcb716cf 2016-07-03)
。
Phantom Type (幽霊型) について
幽霊型とは、型検査を利用したデザインパターンの一つ。インスタンスの状態を数値や文字列といった実際の値として持つのではなく、型パラメータとして持つことで状態の検査を実行時ではなくコンパイル時に行う。実行時にはこのパラメータが存在しないことから幽霊型と呼ばれる。(2016年9月2日訂正)改めて調べてみたところ、見た目上は現れない制約を付与するところがあたかも幽霊のようであることから幽霊型と呼ばれるようだ。
簡単に言えば、バグをコンパイル時に見つけるための手法である。
使い方
Rust では std::marker::PhantomData
を使用して幽霊型を実現する。構造体とタプル、どちらでも使用することができ、またその使い方は大きく変わらない。
具体的には、使用する構造体またはタプルに、 PhantomData
を代入するフィールド、すなわち幽霊型変数を追加することで使用する。
構造体で使う場合
この例では、 PhantomStruct<A, B>
構造体に phantom
というフィールド名で幽霊型変数を追加している。
use std::marker::PhantomData;
# [derive(Debug, PartialEq)]
struct PhantomStruct<A, B> {
value: A,
phantom: PhantomData<B>
}
# [derive(PartialEq)]
enum Milli {}
# [derive(PartialEq)]
enum Centi {}
fn main() {
let struct1: PhantomStruct<isize, Milli> = PhantomStruct {
value: 1000,
phantom: PhantomData,
};
let struct2: PhantomStruct<isize, Centi> = PhantomStruct {
value: 100,
phantom: PhantomData,
};
let struct3 = PhantomStruct {
value: 2000isize,
phantom: PhantomData::<Milli>,
};
let struct4 = PhantomStruct {
value: 100isize,
phantom: PhantomData::<Centi>,
};
// コンパイル時エラー ( error: mismatched types [E0308] )
//println!("{}", struct1 == struct2);
// コンパイル時エラー ( error: mismatched types [E0308] )
//println!("{}", struct3 == struct4);
// ok
assert!(struct1 != struct3);
// ok
assert!(struct2 == struct4);
// struct1 と struct4 は型が違うため、比較することができない。
// そのため、何らかの方法で変換処理を行う必要がある。
//println!("{}", struct1 == struct4); // <-- error
// `PhantomStruct<isize, Centi>` を `PhantomStruct<isize, Milli>` に変換
let struct5 = PhantomStruct {
value: struct4.value * 10,
phantom: PhantomData::<Milli>
};
// ok
assert!(struct1 == struct5);
}
タプルで使う場合
この例では、 PhantomTuple<A, B>
タプル構造体の末尾に幽霊型変数を追加している。
use std::marker::PhantomData;
# [derive(PartialEq)]
struct PhantomTuple<A, B>(A,PhantomData<B>);
# [derive(PartialEq)]
enum Milli {}
# [derive(PartialEq)]
enum Centi {}
fn main() {
let tuple1: PhantomTuple<isize, Milli> = PhantomTuple(1000, PhantomData);
let tuple2: PhantomTuple<isize, Centi> = PhantomTuple(100 , PhantomData);
let tuple3 = PhantomTuple(2000isize, PhantomData::<Milli>);
let tuple4 = PhantomTuple(100isize , PhantomData::<Centi>);
// コンパイル時エラー ( error: mismatched types [E0308] )
//println!("{}", tuple1 == tuple2);
// コンパイル時エラー ( error: mismatched types [E0308] )
//println!("{}", tuple3 == tuple4);
// ok
assert!(tuple1 != tuple3);
// ok
assert!(tuple2 == tuple4);
// tuple1 と tuple4 は型が違うため、比較することができない。
// そのため、何らかの方法で変換処理を行う必要がある。
//println!("{}", tuple1 == tuple4); // <-- error
// `PhantomTuple<isize, Centi>` を `PhantomTuple<isize, Milli>` に変換
let tuple5 = {
let PhantomTuple(val, _) = tuple4;
PhantomTuple(val * 10, PhantomData::<Milli>)
};
// ok
assert!(tuple1 == tuple5);
}
以下の例に示すように、ただのタプルでも同様に使用できる。ただし、 E0117
により std::ops::Add
などを実装することができないため、ただのタプルを使用するメリットはほとんどないだろう。
(なお、下記プログラムで std::ops::Add
を実装しようと試みたところ、 [E0210] や [E0117] エラーに引っかかり、うまくいかなかった。原因などわかる方がいれば、コメントなどで教えていただけると嬉しい)
use std::marker::PhantomData;
type Tuple1<A, B> = (A, PhantomData<B>);
type Tuple2<A, B> = (A, PhantomData<B>);
/* [E0210] や [E0117] に引っかかる
use std::ops::Add;
impl<A:Add, B> Add for (A, PhantomData<B>) {
type Output = (<A as Add>::Output, PhantomData<B>);
fn add(self, rhs: (A, PhantomData<B>)) -> (<A as Add>::Output, PhantomData<B>) {
(self.0 + rhs.0, PhantomData::<B>)
}
}
*/
# [derive(PartialEq)]
enum Milli {}
# [derive(PartialEq)]
enum Centi {}
fn main() {
let tuple1: (isize, PhantomData<Milli>) = (1000, PhantomData);
let tuple2: (isize, PhantomData<Centi>) = (100 , PhantomData);
let tuple3 = (2000isize, PhantomData::<Milli>);
let tuple4 = (100isize , PhantomData::<Centi>);
let tuple6: Tuple1<isize, Centi> = (100 , PhantomData);
let tuple7: Tuple2<isize, Centi> = (100 , PhantomData);
// コンパイル時エラー ( error: mismatched types [E0308] )
//println!("{}", tuple1 == tuple2);
// コンパイル時エラー ( error: mismatched types [E0308] )
//println!("{}", tuple3 == tuple4);
// ok
assert!(tuple1 != tuple3);
// ok
assert!(tuple2 == tuple4);
// ok
assert!(tuple6 == tuple2);
assert!(tuple6 == tuple4);
assert!(tuple6 == tuple7);
// tuple1 と tuple4 は型が違うため、比較することができない。
// そのため、何らかの方法で変換処理を行う必要がある。
//println!("{}", tuple1 == tuple4); // <-- error
// `(isize, PhantomData::<Centi>)` を `(isize, PhantomData::<Milli>)` に変換
let tuple5 = {
let (val, _) = tuple4;
(val * 10, PhantomData::<Milli>)
};
// ok
assert!(tuple1 == tuple5);
}
代入"型"の定義
Rust において、幽霊型変数に代入するということは、 PhantomData<T>
の T
に型を代入するということである。この代入値ならぬ、代入型は、この手法の名前と同じく幽霊型と呼ばれる。
幽霊型は、以下のコードに示すように、 struct
または enum
のどちらでも定義できる。
use std::marker::PhantomData;
struct PhantomTuple<A, B>(A,PhantomData<B>);
struct A; // Unit-like 構造体
enum B {} // ヴァリアントを持たない列挙型
fn main() {
let _tuple1: PhantomTuple<char, A> = PhantomTuple('Q', PhantomData);
let _tuple2: PhantomTuple<char, B> = PhantomTuple('Q', PhantomData);
}
状態によるメソッドのアクセス制御
『Swift で Phantom Type (幽霊型) - Qiita』で紹介されているように、アクセス修飾子によってオブジェクトの内外で使用可能なメソッドを制御するのと同様、幽霊型によってインスタンスの状態で使用可能なメソッドを制御することができる。( @sinkuu さん情報ありがとう!)
なお、以下のコードでは、 Preparing
と Ready
を列挙型( enum
)で定義しているが、構造体( struct
)で定義しても同じように動作する。
use std::marker::PhantomData;
enum Preparing {} // <--- struct で定義してもよい
enum Ready {} // <-/
struct Something<S = Preparing> {
_status: PhantomData<S>,
}
impl<S> Something<S> {
fn common_fn(&self) {
println!("called common_fn");
}
}
impl Something<Preparing> {
fn new() -> Self {
Something::<Preparing> { _status: PhantomData }
}
fn prepare(&self) -> Something<Ready> {
Something::<Ready> { _status: PhantomData }
}
}
impl Something<Ready> {
fn shout(&self) {
println!("shout!")
}
}
fn main() {
let s = Something::new();
s.common_fn();
//s.shout(); // <-- error
let s = s.prepare();
s.common_fn();
//s.prepare(); // <-- error
s.shout();
}
こぼれ話
本稿では、標準ライブラリの std::marker::PhantomData
をもとに幽霊型を説明してきたが、 コアライブラリにも core::marker::PhantomData
として幽霊型は定義されている。
というよりも、 std::marker::PhantomData
の実装を見ると、実態は(少なくとも 1.10.0
においては)コアライブラリの PhantomData
そのものである。
最後に
何か間違いがあれば、『コメント』ないしは『編集リクエスト』で教えていただけると嬉しい。
また、『Swift で Phantom Type (幽霊型) - Qiita』にあるような、幽霊型の値によって使用可能な関数を変更するのは、トレイトをうまく使うのだと思うが残念ながらやり方がわからなかった。具体的な方法がわかる方がいればこれもコメントで教えていただけると嬉しい。( @sinkuu さんのおかげで解決しました。ありがとう!)
参考文献
- Phantom type parameters | Rust by Example
- C# で Phantom Type もどき - present
- 幽霊型の紹介(サイバーエージェントA.J.A.社内勉強会 2016.6.23)
- 幽霊型を使ってウェブアプリで安全に文字列を使う:Rainy Day Codings:So-net blog
- Swift で Phantom Type (幽霊型) - Qiita
- OCaml-Nagoya(著)『入門 OCaml プログラミング基礎と実践理解』初版第1刷 ISBN:978-4-8399-2311-2