strong typedef
スライス型を自前定義する意義を考えるうえで、まず strong typedef というテクニックについて説明せねばなるまい。 strong typedef について十分知っている読者は、このセクションを丸々読み飛ばして問題ない。
本来なら C++ あたりのコード例で説明するところだが、本書は Rust での手法について説明するものなので例も Rust で書くことにする。
C や C++ での typedef
に相当するものは、 Rust では type
である1。
typedef
前提として、 typedef
とはC言語や C++ で型に別名を与える構文のための予約語である。
typedef
は本当に別名を用意するだけで、その性質や扱いに変化を与えることはない。
純粋に可読性のための存在といえる。
// Rust での `type` (C や C++ での `typedef`) は型に別名を与えるだけで、その性質には // 変化を与えないため、 `usize` の変数と `ArrayIndex` の変数は同じ型を持つものとして // 扱われる。 /// A type for an array index. type ArrayIndex = usize; fn main() { let i0: usize = 42; // `ArrayIndex` and `usize` are identical. let i1: ArrayIndex = i0; let _i2: usize = i1; }
型の性質に変化を与えないため挙動は理解しやすいが、反面 typedef
は新しい型を作る用途で使うことはできない。
あくまで別名を割り当てるだけである。
つまり、本来禁止したかった用法や無意味な計算が許されかねないということである。
// 別名を与えただけで型としては同一であるため、 `ArrayIndex` 型に対して // `usize` から引き継いだ演算が全て使えてしまう。 /// A type for an array index. type ArrayIndex = usize; fn main() { let i0: ArrayIndex = 1; let i1: ArrayIndex = 2; // What does this mean? let _sum: ArrayIndex = i0 + i1; // Nonsense! let _nonsense: ArrayIndex = i0 * !i1; }
// 同一の型である以上、意味や用途が明確に違っても別名同士は区別されない。 /// A type for an array index. type ArrayIndex = usize; /// A type for distance between array elements. type ArrayDistance = usize; fn main() { let i0: ArrayIndex = 42; let i1: ArrayIndex = 314; // Meaningful. let _distance: ArrayDistance = i1 - i0; // Nonsense. Array index is offset from the beginning, // but not distance between arbitrary elements. let _nonsense: ArrayIndex = i1 - i0; }
こうして「既存の型をもとにして (つまり内部表現を同一にして) 用途特化型を楽に定義したい」という夢は潰えた。 夢破れた人々がそれでも諦められない場合に使うのが strong typedef である。
strong typedef の違いと例
strong typedef とは、内部的には既存の型を使って、それでも元の型と互いに区別されるような用途特化型を定義しようという手法である。 言語や文化圏によって opaque typedef とか opaque alias とか newtype pattern とか様々な名前で呼ばれるが、いずれも同じ手法を指している。 本書では strong typedef と呼ぶことにする。
具体例を見た方が理解が早かろう。 strong typedef は Rust では以下のように (あるいは他言語でも似たような方法で) 実現される。
// strong typedef で添字と距離を互いに区別される別々の型として定義し、 // 特定の意味ある演算だけ明示的に実装した。 use std::{cmp, ops}; /// A type for an array index. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] struct ArrayIndex(usize); impl ArrayIndex { pub fn new(i: usize) -> Self { Self(i) } pub fn to_usize(self) -> usize { self.0 } } /// A type for distance between array elements. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] struct ArrayDistance(usize); impl ArrayDistance { pub fn new(i: usize) -> Self { Self(i) } pub fn to_usize(self) -> usize { self.0 } } // index + distance => index. impl ops::Add<ArrayDistance> for ArrayIndex { type Output = Self; fn add(self, distance: ArrayDistance) -> Self::Output { Self(self.to_usize() + distance.to_usize()) } } // index - distance => index. impl ops::Sub<ArrayDistance> for ArrayIndex { type Output = Self; fn sub(self, distance: ArrayDistance) -> Self::Output { Self(self.to_usize() - distance.to_usize()) } } // index - index => distance. impl ops::Sub<ArrayIndex> for ArrayIndex { type Output = ArrayDistance; fn sub(self, other: ArrayIndex) -> Self::Output { let min = cmp::min(self, other); let max = cmp::max(self, other); ArrayDistance::new(max.to_usize() - min.to_usize()) } } fn main() { let i0: ArrayIndex = ArrayIndex::new(42); let i1: ArrayIndex = ArrayIndex::new(314); let d0: ArrayDistance = i1 - i0; let d1: ArrayDistance = i0 - i1; assert_eq!(d0, d1); // ERROR. //let _sum_meaningless = i0 + i1; // ERROR. //let _product_meaningless = i0 * i1; // If you really really want the product, you can do this. let _product = i0.to_usize() * i1.to_usize(); }
このコードは正直微妙なところがあるが2、例としては十分だろう。
typedef
では混同できていた index と distance が strong typedef では区別されており、混同するとコンパイルが通らない。
typedef
では添字同士の乗算などの一般に意味のない演算ができてしまったが、 strong typedef ではそのような演算はできない。
このように、 strong typedef は次のような方法によって型を定義する手法のことである。
- デフォルトでは他の型から暗黙に変換できないような型を作る。
C, C++, Rust では struct を作るのが一般的。
- 通常この構造体はメンバ変数 (あるいはフィールド) をひとつだけ持ち、その型がベースとなる既存の型である。
- 新たな型の値を作る方法を用意する。
新たな型とベースとした型との間で相互に変換できるよう関数を用意するのが一般的。
- ただし、暗黙の型変換などを迂闊に実装しないよう注意すること。
- 新たな型について、意味のある演算子や関数などを実装する。
内部実装としては、ベースとした型での演算をそのまま再利用するのが一般的。
- たとえば f64 をベースにした時刻型であれば、時刻同士の減算には意味があるが、加算や乗算、除算には意味がない。 このような場合には時刻同士の加算演算子だけを定義し、内部的には f64 の減算を使う。 ただし、戻り値の型は時刻 (time point) ではなく時間 (duration) となるだろう。
strong typedef は極めて応用範囲の広い手法で、たとえば以下のような利用例が考えられる。
- 物理量の区別
- 質量と距離はともに実数だが、これらを足したり混同するのは無意味なので禁じたい。
- バイト列と UTF-8 文字列の区別
- 両者はともにバイト列で表現可能だが、任意のバイト列が常に正しい UTF-8 文字列とは限らない。 別の言い方をすると、 UTF-8 文字列はバイト列の部分集合である。
- 大文字・小文字を区別しない文字列型の定義
- ほとんどの場合通常の文字列として振る舞うが、比較時だけ大文字と小文字を同一視するような文字列型が欲しい場合がある。
- ASCII 文字しか持てない文字列型の定義
- ほとんどの場合通常の文字列として振る舞うが、 ASCII 文字しか持てないよう制約を加えた文字列型が欲しい場合がある。
Rust での type
は型パラメータを持ったり部分特殊化などもできるため、厳密には C++ における typedef
よりは using
の方がより近い
たとえばインデックス同士の距離を表現するのに offset として isize
を使うのでなく distance として usize
を使っているところとか…… たぶんこんなものを定義しても実用上はあまり用途がない