Rust業界に詳しくないので既に広く知られているものかもしれないが、昨晩突如として以下のようなトリックを思いついた。
struct X<T> {/* fields omitted */} impl X<f32> { pub fn f32(self) -> Self {self} } impl X<f64> { pub fn f64(self) -> Self {self} } let x = X::new(/* ... */).f64() // ^^^^^^ // 他の関数呼び出しなどからは無理でも、これのおかげでT = f64と推論できる
いや、明示的に型指定しろよ、それかデフォルト型パラメータを使えよ、と思う方が大半であろうが、以下のようなケースで便利になるのではないかと思う。というか困っていたから思いついた。
まず、何らかの物体の三次元座標が入ったファイルがあり、それを読み込みたいと思っているとする。
A 1.00 1.00 1.00 B 2.00 3.00 1.00 ...
これを読み込む際、例えばデータ量が超絶多いとか、可視化できればそれでいいから精度は低くていいとか、あるいはその両方といった理由でf64
ではなくf32
を使いたいケースというのはあるだろう。そういう場合、まあcfg
でビルド時に決めてしまってもいいのだが、ジェネリクスを使って解決することもできる。
struct Particle<T> { name: std::string::String, pos : [T; 3], }
で、これを読み込みたいとき、例えばちょっとLazyにしたかったりするだろう。そういうときはIterator
をimpl
するのがよさそうだ。
struct Reader<R> { /* fields omitted */ } impl<R: std::io::Read> std::iter::Iterator for Reader<R> { type Item = Particle<T>; // ^^^ // この型パラメータTはどうするのか? fn next() -> Option<Particle<T>> { // ... } }
すると、Iterator
のItem
に型パラメータが必要になる。だが、Reader
は今Particle
の型パラメータT
を知らない。なのでT
を決めようがない。
仕方がないので、Reader
にダミーのパラメータを持たせることになるだろう。
struct Reader<T, R> { _marker: std::marker::PhantomData<fn() -> T>, // ... } impl<T, R: std::io::Read> std::iter::Iterator for Reader<T, R> { type Item = Particle<T>; fn next() -> Option<Particle<T>> { // ... } }
さて、このReader
をnew
するとき、どうすることになるだろうか。
理想は、R: std::io::Read
の方は型推論が何とかしてくれて、T: f32 | f64
の方だけユーザーが明示的に指定できるというものだろう。
let reader = Reader::<f32>::new( std::fs::File::open("example.dat").unwrap() );
だがこれは通らない。C++のtemplate
関数は決まっていない型パラメータだけを明示的に指定することを許すが、Rustは許さない。二つパラメータがあるなら二つ書く必要がある。
まあそれでも、「これは型推論しろ」という意思をアンダースコアで表現できるのでまだマシだが。
let reader = Reader::<f32, _>::new( std::fs::File::open("example.dat").unwrap() );
これでいいのでは? いや、考える必要があるのはユーザーのことだ。新規ユーザーがこのサンプルコードを見たら、「おや、このライブラリは柔軟性のために書きやすさと学習コストを犠牲にしているようだぞ? こんな単純なサンプルですら型推論が助けにならないなら、多分この後ずっと、どの型が何個ジェネリクスパラメータを取るか毎回調べないといけなくなるんだろうな、はーめんどくせ。なんのために型推論があると思ってるんだ。Rustやめちまえ」と思うだろう。
このユーザーは私の心の声でもある。昔書いたライブラリの構造体のジェネリクスパラメータの数なんか覚えてねえよ。コンパイラの仕事だろ。コンパイラが数えろ。
で、最初は「仕方ない、たいてい必要になるのはファイルを開いて読み込むことだから、それをするユーティリティ関数でも書いて、そこでf32
とf64
を選べるようにするか」と考えた。
pub fn open<T, P>(path: P) -> Result<Reader<T>> where P: std::convert::AsRef<std::path::Path> { // ... }
ああーっ、また型パラメータ増やしてやがる! お前はいつもそうだ。誰もお前を愛さない。
さて、そうなると、やはりこの関数を呼ぶときにも型パラメータの数を意識しないといけない。
let reader = crate::reader::open::<f32, _>( "example.dat" ).unwrap();
何も簡単になってないじゃないか!
というわけで、冒頭のトリックが有用になったりするわけだ。
impl<R> Reader<f32, R> { pub fn f32(self) -> Self {self} } impl<R> Reader<f64, R> { pub fn f64(self) -> Self {self} }
これを使うとrustc
の型推論を補助することができる。
let reader = crate::reader::open("example.dat").unwrap().f32();
f32()
を呼べるのはReader<f32, R>
だけだから、T = f32
に決定する。そしてopen
ができるのはReader<T, std::fs::File>
だけだから、R = std::fs::File
に決まる。というわけで、明示的に型パラメータを一切指定せずに、全ての型パラメータを決めることができるようになった。
別の行に書いても多分大丈夫だと思う。
if let Ok(reader) = crate::reader::open("example.dat") { let reader = reader.f32(); }
これちょっと格好良くないか? ちょっとビルダーパターンっぽい気もする。動的に型変えられるようになってる感があって面白い。
実際は、動的に変わっているのではなくて、rustc
が型不定のまま1行めを解釈したあとreader
に対してf32()
が呼ばれているのを見てそこで1行めのreader
がReader<f32, File>
だったことを知るのだ。なので実際には遡って型パラメータが決定されていっている。
これで型パラメータに怯えることはなくなった。型パラメータの数をいちいち数える日々から開放された。
安心して今夜も眠れるだろう。