高度な型

Rustの型システムには、この本で触れたけれども、まだ議論していない機能があります。ニュータイプが何故型として有用なのかを調査するため、 一般化してニュータイプを議論することから始めます。そして、型エイリアスに移ります。ニュータイプに類似しているけれども、 多少異なる意味を持つ機能です。また、!型と動的サイズ決定型も議論します。

注釈: 次の節は、前節「外部の型に外部のトレイトを実装するニュータイプパターン」を読了済みであることを前提にしています。

型安全性と抽象化を求めてニュータイプパターンを使用する

ここまでに議論した以上の作業についてもニュータイプパターンは有用で、静的に絶対に値を混同しないことを強制したり、 値の単位を示すことを含みます。ニュータイプを使用して単位を示す例をリスト19-23で見かけました: MillimetersMeters構造体は、u32値をニュータイプにラップしていたことを思い出してください。 型Millimetersを引数にする関数を書いたら、誤ってその関数を型Metersや普通のu32で呼び出そうとするプログラムはコンパイルできないでしょう。

型の実装の詳細を抽象化する際にニュータイプパターンを使用するでしょう: 例えば、新しい型を直接使用して、 利用可能な機能を制限したら、非公開の内部の型のAPIとは異なる公開APIを新しい型は露出できます。

ニュータイプはまた、内部の実装を隠匿(いんとく)することもできます。例を挙げれば、People型を提供して、 人のIDと名前を紐づけて格納するHashMap<i32, String>をラップすることができるでしょう。 Peopleを使用するコードは、名前の文字列をPeopleコレクションに追加するメソッドなど、 提供している公開APIとだけ相互作用するでしょう; そのコードは、内部でi32IDを名前に代入していることを知る必要はないでしょう。 ニュータイプパターンは、カプセル化を実現して実装の詳細を隠匿する軽い方法であり、 実装の詳細を隠匿することは、第17章の「カプセル化は実装詳細を隠蔽する」節で議論しましたね。

型エイリアスで型同義語を生成する

ニュータイプパターンに付随して、Rustでは、既存の型に別の名前を与える型エイリアス(type alias: 型別名)を宣言する能力が提供されています。 このために、typeキーワードを使用します。例えば、以下のようにi32に対してKilometersというエイリアスを作れます。


#![allow(unused)]
fn main() {
type Kilometers = i32;
}

これで、別名のKilometersi32同義語になりました; リスト19-23で生成したMillimetersMetersとは異なり、 Kilometersは個別の新しい型ではありません。型Kilometersの値は、型i32の値と同等に扱われます。


#![allow(unused)]
fn main() {
type Kilometers = i32;

let x: i32 = 5;
let y: Kilometers = 5;

println!("x + y = {}", x + y);
}

Kilometersi32が同じ型なので、両方の型の値を足し合わせたり、Kilometersの値をi32引数を取る関数に渡せたりします。 ですが、この方策を使用すると、先ほど議論したニュータイプパターンで得られる型チェックの利便性は得られません。

型同義語の主なユースケースは、繰り返しを減らすことです。例えば、こんな感じの長い型があるかもしれません:

Box<Fn() + Send + 'static>

この長ったらしい型を関数シグニチャや型注釈としてコードのあちこちで記述するのは、面倒で間違いも起きやすいです。 リスト19-32のそのようなコードで溢れかえったプロジェクトがあることを想像してください。


#![allow(unused)]
fn main() {
let f: Box<Fn() + Send + 'static> = Box::new(|| println!("hi"));

fn takes_long_type(f: Box<Fn() + Send + 'static>) {
    // --snip--
}

fn returns_long_type() -> Box<Fn() + Send + 'static> {
    // --snip--
    Box::new(|| ())
}
}

リスト19-32: 長い型を多くの場所で使用する

型エイリアスは、繰り返しを減らすことでこのコードをより管理しやすくしてくれます。リスト19-33で、 冗長な型にThunk(注釈: 塊)を導入し、その型の使用全部をより短い別名のThunkで置き換えることができます。


#![allow(unused)]
fn main() {
type Thunk = Box<Fn() + Send + 'static>;

let f: Thunk = Box::new(|| println!("hi"));

fn takes_long_type(f: Thunk) {
    // --snip--
}

fn returns_long_type() -> Thunk {
    // --snip--
    Box::new(|| ())
}
}

リスト19-33: 型エイリアスのThunkを導入して繰り返しを減らす

このコードの方が遥かに読み書きしやすいです!型エイリアスに意味のある名前を選択すると、 意図を伝えるのにも役に立つことがあります(thunkは後ほど評価されるコードのための単語なので、 格納されるクロージャーには適切な名前です)。

型エイリアスは、繰り返しを減らすためにResult<T, E>型ともよく使用されます。標準ライブラリのstd::ioモジュールを考えてください。 I/O処理はしばしば、Result<T, E>を返して処理がうまく動かなかった時を扱います。このライブラリには、 全ての可能性のあるI/Oエラーを表すstd::io::Error構造体があります。std::ioの関数の多くは、 Writeトレイトの以下の関数のようにEstd::io::ErrorResult<T, E>を返すでしょう:


#![allow(unused)]
fn main() {
use std::io::Error;
use std::fmt;

pub trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
    fn flush(&mut self) -> Result<(), Error>;

    fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
    fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>;
}
}

Result<..., Error>が何度も繰り返されてます。そんな状態なので、std::ioにはこんな類のエイリアス宣言があります:

type Result<T> = Result<T, std::io::Error>;

この宣言はstd::ioモジュール内にあるので、フルパスエイリアスのstd::io::Result<T>を使用できます。 つまり、Estd::io::Errorで埋められたResult<T, E>です。その結果、Writeトレイトの関数シグニチャは、 以下のような見た目になります:

pub trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize>;
    fn flush(&mut self) -> Result<()>;

    fn write_all(&mut self, buf: &[u8]) -> Result<()>;
    fn write_fmt(&mut self, fmt: Arguments) -> Result<()>;
}

型エイリアスは、2通りの方法で役に立っています: コードを書きやすくすることstd::ioを通して首尾一貫したインターフェイスを与えてくれることです。 別名なので、ただのResult<T, E>であり、要するにResult<T, E>に対して動くメソッドはなんでも使えるし、 ?演算子のような特殊な記法も使えます。

never型は絶対に返らない

Rustには、!という名前の特別な型があります。それは型理論の専門用語では Empty型 と呼ばれ値なしを表します。私たちは、 関数が値を返すことが決して (never) ない時に戻り値の型を記す場所に使われるので、never type(訳注: 日本語にはできないので、never型と呼ぶしかないか)と呼ぶのが好きです。 こちらが例です:

fn bar() -> ! {
    // --snip--
}

このコードは、「関数barはneverを返す」と解読します。neverを返す関数は、発散する関数(diverging function)と呼ばれます。 型!の値は生成できないので、barからリターンする(呼び出し元に制御を戻す)ことは決してできません。

ですが、値を絶対に生成できない型をどう使用するのでしょうか?リスト2-5のコードを思い出してください; リスト19-34に一部を再掲します。


#![allow(unused)]
fn main() {
let guess = "3";
loop {
let guess: u32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => continue,
};
break;
}
}

リスト19-34: continueになるアームがあるmatch

この時点では、このコードの詳細の一部を飛ばしました。第6章の「match制御フロー演算子」節で、 matchアームは全て同じ型を返さなければならないと議論しました。従って、例えば以下のコードは動きません:

let guess = match guess.trim().parse() {
    Ok(_) => 5,
    Err(_) => "hello",
}

このコードのguessは整数かつ文字列にならなければならないでしょうが、Rustでは、guessは1つの型にしかならないことを要求されます。 では、continueは何を返すのでしょうか?どうやってリスト19-34で1つのアームからはu32を返し、別のアームでは、 continueで終わっていたのでしょうか?

もうお気付きかもしれませんが、continue!値です。つまり、コンパイラがguessの型を計算する時、 両方のmatchアームを見て、前者はu32の値、後者は!値となります。!は絶対に値を持ち得ないので、 コンパイラは、guessの型はu32と決定するのです。

この振る舞いを解説する公式の方法は、型!の式は、他のどんな型にも型強制され得るということです。 このmatchアームをcontinueで終えることができます。何故なら、continueは値を返さないからです; その代わりに制御をループの冒頭に戻すので、Errの場合、guessには絶対に値を代入しないのです。

never型は、panic!マクロとも有用です。Option<T>値に対して呼び出して、値かパニックを生成したunwrap関数を覚えていますか? こちらがその定義です:

impl<T> Option<T> {
    pub fn unwrap(self) -> T {
        match self {
            Some(val) => val,
            None => panic!("called `Option::unwrap()` on a `None` value"),
        }
    }
}

このコードにおいて、リスト19-34のmatchと同じことが起きています: コンパイラは、valの型はTで、 panic!の型は!なので、match式全体の結果はTと確認します。panic!は値を生成しないので、 このコードは動きます。つまり、プログラムを終了するのです。Noneの場合、unwrapから値は返さないので、 このコードは合法なのです。

型が!の最後の式は、loopです:

// 永遠に
print!("forever ");

loop {
    // さらに永遠に
    print!("and ever ");
}

ここで、ループは終わりませんので、!が式の値です。ところが、breakを含んでいたら、これは真実にはならないでしょう。 breakに到達した際にループが終了してしまうからです。

動的サイズ決定型とSizedトレイト

コンパイラが特定の型の値1つにどれくらいのスペースのメモリを確保するのかなどの特定の詳細を知る必要があるために、 Rustの型システムには混乱を招きやすい細かな仕様があります: 動的サイズ決定型の概念です。時としてDSTサイズなし型とも称され、 これらの型により、実行時にしかサイズを知ることのできない値を使用するコードを書かせてくれます。

strと呼ばれる動的サイズ決定型の詳細を深掘りしましょう。本を通して使用してきましたね。 そうです。&strではなく、strは単独でDSTなのです。実行時までは文字列の長さを知ることができず、 これは、型strの変数を生成したり、型strを引数に取ることはできないことを意味します。 動かない以下のコードを考えてください:

// こんにちは
let s1: str = "Hello there!";
// 調子はどう?
let s2: str = "How's it going?";

コンパイラは、特定の型のどんな値に対しても確保するメモリ量を知る必要があり、ある型の値は全て同じ量のメモリを使用しなければなりません。 Rustでこのコードを書くことが許容されたら、これら2つのstr値は、同じ量のスペースを消費する必要があったでしょう。 ですが、長さが異なります: s1は、12バイトのストレージが必要で、s2は15バイトです。このため、 動的サイズ決定型を保持する変数を生成することはできないのです。

では、どうすればいいのでしょうか?この場合、もう答えはご存知です: s1s2の型をstrではなく、 &strにすればいいのです。第4章の「文字列スライス」節でスライスデータ構造は、 開始地点とスライスの長さを格納していると述べたことを思い出してください。

従って、&Tは、Tがどこにあるかのメモリアドレスを格納する単独の値だけれども、&str2つの値なのです: strのアドレスとその長さです。そのため、コンパイル時に&strのサイズを知ることができます: usizeの長さの2倍です。要するに、参照している文字列の長さによらず、常に&strのサイズがわかります。 通常、このようにしてRustでは動的サイズ決定型が使用されます: 動的情報のサイズを格納する追加のちょっとしたメタデータがあるのです。 動的サイズ決定型の黄金規則は、常に動的サイズ決定型の値をなんらかの種類のポインタの背後に配置しなければならないということです。

strを全ての種類のポインタと組み合わせられます: 例を挙げれば、Box<str>Rc<str>などです。 実際、これまでに見かけましたが、異なる動的サイズ決定型でした: トレイトです。全てのトレイトは、 トレイト名を使用して参照できる動的サイズ決定型です。第17章の「トレイトオブジェクトで異なる型の値を許容する」節で、 トレイトをトレイトオブジェクトとして使用するには、&TraitBox<Trait>(Rc<Trait>も動くでしょう)など、 ポインタの背後に配置しなければならないことに触れました。

DSTを扱うために、RustにはSizedトレイトと呼ばれる特定のトレイトがあり、型のサイズがコンパイル時にわかるかどうかを決定します。 このトレイトは、コンパイル時にサイズの判明する全てのものに自動的に実装されます。加えて、 コンパイラは暗黙的に全てのジェネリックな関数にSizedの境界を追加します。つまり、こんな感じのジェネリック関数定義は:

fn generic<T>(t: T) {
    // --snip--
}

実際にはこう書いたかのように扱われます:

fn generic<T: Sized>(t: T) {
    // --snip--
}

既定では、ジェネリック関数はコンパイル時に判明するサイズがある型に対してのみ動きます。 ですが、以下の特別な記法を用いてこの制限を緩めることができます:

fn generic<T: ?Sized>(t: &T) {
    // --snip--
}

?Sizedのトレイト境界は、Sizedのトレイト境界の逆になります: これを「TSizedかもしれないし、違うかもしれない」と解読するでしょう。 この記法は、Sizedにのみ利用可能で、他のトレイトにはありません。

また、t引数の型をTから&Tに切り替えたことにも注目してください。型はSizedでない可能性があるので、 なんらかのポインタの背後に使用する必要があるのです。今回は、参照を選択しました。

次は、関数とクロージャについて語ります!

関連キーワード:  コード, 関数, Result, サイズ, ニュータイプ, パターン, 決定, リスト, type, Sized