Don't Repeat Yourself

Don't Repeat Yourself (DRY) is a principle of software development aimed at reducing repetition of all kinds. -- wikipedia

Rust の `!` (ビックリマーク、エクスクラメーションマーク、感嘆符、never) 型

タイトルは記号のググラビリティ確保のためにわざとつけています(笑)。動作はすべて、Rust 1.47.0 (stable) です。nightly を使用した場合は nightly と文中に記載しました。

TL; DR

  • Rust には ! という「何も返さないことを示すモノ」が存在する。
  • never 型と呼ぶ。
  • 値を何も返さない計算の表現として使用される。
  • 普段は型強制される(ので、使っていることに気づかないかも)。
  • never 型は安定化されていない。

今回も記事が長くなってしまいました。ちゃんと調べると、結構いろいろ書くべきことがありボリューミーになってしまいました。好きな場所をかいつまんでお読みください。

基本

! = never 型

! は Rust では never 型と呼ばれます。

これは他のプログラミング言語型理論などではボトム型あるいは発散型と呼ぶことがあります*1。他のプログラミング言語でも同様の概念が存在しています。Scala ではボトム型は Nothing として表現されます。TypeScript では Rust と同様に never として型をつけます

たとえば、

fn diverges() -> ! {
    panic!("This function never returns!");
}

この関数は値を何も返しません。それでは unit 型と同じなのでは?と思うかもしれませんが、unit 型は Rust においては中身のない Tuple という値なので、実質的に値を返していることと同義になります。そうではなく、never 型は文字通り値を何も返さないことを示します。

このように何も返さない関数を diverging functions と呼びます。この型は、関数や計算が発散していることを示しています。関数や計算が発散しているとは、つまり値を呼び出し側に返さないことを意味します。

never 型はどこに出てくるのか

たとえば loop キーワードは never 型を返します。下記はコンパイルがしっかり通ります。

fn print_forever() -> ! {
    loop {
        println!("永遠に。。。");
    }
}

その他、Rust では breakcontinuereturnpanicunimplemented マクロ*2unreachable マクロ*3などが never 型を返します。

ここまでの例では、いくぶん暗黙的な型変換の例であったり、あるいは関数の戻り値としてしか ! を登場させてきませんでした。これは意図的で、現在の stable Rust では never 型は関数の戻り値でしか指定できません。基本編では説明しませんが、のちの発展編ではそのあたりの経緯を少し説明していますので、興味がある方は発展編まで読み進めてください。

Coercing

never 型はいろいろな型に型強制(type coercing; coercing の読み方はこの動画)できます*4。型強制は要するに、ある型と、必要とする型とが異なる場合に、自動的に型の変換を挟むことです。たとえば下記の例では、!i32 に型強制(自動的な型変換)が行われています。

fn main() {
    // 最終的な型は i32 として判定される。これは、型強制が起きているためにこうなる。
    let result: i32 = match answer() {
        Ok(v) => v, // こちらの腕は `i32` を返す
        Err(err) => panic!("unexpected error") // こちらの腕は `!` を返す
    };
    println!("{}", result);
}

enum Error {}

fn answer() -> Result<i32, Error> {
    Ok(1)
}

通常、パターンマッチの各腕の型は最終的には一致している必要があります。つまり、たとえば Err(err) => panic!(...)と書いている箇所に、Err(err) => "unexpected error"と書いて &str 型にしてしまうと、この時点でコンパイルエラーになります。

fn main() {
    // 最終的な型は i32 として判定される。これは、型強制が起きているためにこうなる。
    let result: i32 = match answer() {
        Ok(v) => v, // こちらの腕は `i32` を返す
        Err(err) => "unexpected error" // こちらの腕は `&str` を返す
    };
    println!("{}", result);
}

enum Error {}

fn answer() -> Result<i32, Error> {
    Ok(1)
}
error[E0308]: `match` arms have incompatible types
 --> src/main.rs:5:21
  |
3 |       let result: i32 = match answer() {
  |  _______________________-
4 | |         Ok(v) => v, // こちらの腕は `i32` を返す
  | |                  - this is found to be of type `i32`
5 | |         Err(err) => "unexpected error" // こちらの腕は `&str` を返す
  | |                     ^^^^^^^^^^^^^^^^^^ expected `i32`, found `&str`
6 | |     };
  | |_____- `match` arms have incompatible types

型強制は、型同士のミスマッチが起きたタイミングで一度チェックされます。先ほどもおそらく一度チェックが走っているのでしょう。結果、 &stri32 には部分型等の関係性が何もなかったために変換は起きなかったというわけです。

一方で never 型は例外的で、すべての型のボトムに存在する型のため、!i32 の部分型になっています。これによって i32 への型強制が起き、結果 i32 として型が判定されます。

たまに出てくる、すべての型の基底にあってどんな型にも強制できる型という理解で、さしあたっては大丈夫です。

発展

以下は少し発展的な内容になります。

never 型はまだ安定化されていない

実は never 型はまだ型としては「本格的には」安定化されていません*5。たとえば、次のような明示的な never 型の変数における型宣言は、まだ stable コンパイラではできません。nightly を使用し、feature を有効化する必要があります。

stable コンパイラ 1.47.0 で下記コードをコンパイルしてみます。

fn main() {
    let p: ! = panic!("never type is experimental!");
}
error[E0658]: the `!` type is experimental
 --> src/main.rs:4:12
  |
4 |     let p: ! = panic!("never type is experimental!");
  |            ^
  |
  = note: see issue #35121 <https://github.com/rust-lang/rust/issues/35121> for more information

これを回避するためには、#![feature(never_type)] を使用する必要があります。下記は nightly 1.49.0 を使用してコンパイル可能でした。

#![feature(never_type)]
fn main() {
    let p: ! = panic!("never type is experimental!");
}

std::convert::Infallible

Rust 1.34.0 から導入された std::convert::Infallible という型を使用すると、「決してそこでエラーが起こらないことを示す」ことができます(できることになっています)。決してXXXが起こらない――これは never 型が本来担うべき役割なように見えますね。

use std::convert::Infallible;

fn main() {
    // このコードはコンパイルが通る。
    // loop は `!` 型であることに注意。
    let p: Infallible = loop {
        println!("never fail but print forever")
    };
}

この std::convert::Infallible の定義は実は空の enum になっています

pub enum Infallible {}

将来的にはこの Infallible は下記のように ! のタイプエイリアスとして定義される予定のようです。

pub type Infallible = !;

ただこれは、あくまでエラーがないことを示すがための型のように思います。すべての never 型を代表するような一般性はないかもしれません。

まったく異なる Infallible という型が、なぜ先ほどのように ! の代用できるかというと、「基本」で示したように裏で型強制が起きているからなのでしょう。! はボトム型である以上実質どの型にも型強制されうるので、結果表面上は Infallible = ! なように見えるというわけです。そして、Infallible は値をもたないので、実質「値を決して返さない」never 型の機能も満たせているというわけです。

例としては Result 型に対する使用でしょう。本来エラーが返ることはない操作に対して、なにか他の実装との都合でどうしても Result 型を使用する必要がある場合に、「決してエラー側は返すことがない」を表現するために Result<A, Infallible>(A は任意の型)といった宣言をして回避するという手法が取れます。将来的には Result<A, !> としたいのです。

たとえば std::convert::Infallible のドキュメントには TryFrom の例が記載されており、TryFromOk しか返さないことを Result 型を使用しつつも表現できています。

impl<T, U> TryFrom<U> for T where U: Into<T> {
    type Error = Infallible;

    fn try_from(value: U) -> Result<Self, Infallible> {
        Ok(U::into(value))  // Never returns `Err`
    }
}

普段のアプリケーションで意図的に never 型(Infallible)を使用したいというのは稀かもしれませんが、頭の片隅に置いておきたい話ですね。

で、なんで Rust には ! がいるの?

基本編でも書いたように、最終的な挙動や意味合いは unit 型(unit 型は Rust では要素0個の Tuple (())でしたね)と似ています。ではなぜ、わざわざ never 型を区別して用意しているのでしょうか?

詳しい人が StackOverflow にて回答してくれています。簡単に要約していきます。

stackoverflow.com

まずひとつめの理由は、意味のないコード片をコンパイルタイムで拾い上げられるからです。

たとえば、Rust では下記のようなコードを書くと警告が出ます。このコード片では exit が never 型を返します。println!exit より後ろにあるため、決して呼び出されないからです。これらを解析することで、下記のような警告を出せるわけです。

fn main() {
    std::process::exit(0);
    println!("unreachable");
}
warning: unreachable statement
 --> src/main.rs:3:5
  |
2 |     std::process::exit(0);
  |     --------------------- any code following this expression is unreachable
3 |     println!("unreachable");
  |     ^^^^^^^^^^^^^^^^^^^^^^^^ unreachable statement
  |
  = note: `#[warn(unreachable_code)]` on by default
  = note: this warning originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

Rust ではやはり他のモダンなコンパイラと同様にデータフロー解析が行われるので、そこで利用されるようです。

次は、Rust のコンパイラバックエンドとして使用されている LLVM の最適化に使用できるようです。以下は私が LLVM にまったく詳しくないので間違ったことを書くかもしれません。

never 型は上記のコンパイルエラーからもわかるとおり、到達しないコード片の解析に利用できます。同時に、 LLVM にもそのことを伝えられるようです。

先ほどのコードを LLVM IR に直してみてみると、unreachable という文字列が見られます。ドキュメントを参照してみると、unreachable という命令があります。これは、ドキュメントによるとオプティマイザにそのコード部分が使用されないことを伝える役割をもっているそうです。

; Function Attrs: nonlazybind uwtable
define internal void @_ZN10playground4main17h9866004d1365e02aE() unnamed_addr #1 !dbg !163 {
start:
; call std::process::exit
  call void @_ZN3std7process4exit17h58c2c865f4c85e96E(i32 0), !dbg !166
  unreachable, !dbg !166
}

あるいは、先ほどの例にあるように Ok しか返さない Result 型で Err 側を呼び出してしまった場合などについては、コンパイラOk のケースだけ考えればよくなるというメリットも示されています。

他の言語での never 型導入のメリットは、Rust とは少し事情が異なるかもしれません。調べてみてもおもしろいかもしれません。

Rust ではなぜ、関数の戻り値だけにしか ! を使用できなくしてあるの?

まず結論ですが、これは調べてみましたがわかりませんでした。コンパイラをもっとしっかり読んでみるべきかもしれません。読んでみてなにかわかったら追記をしようと思います。以下は調べた話を目的もなくつらつらと書きます。

なんとなくなんですが、型推論の問題があるのかなと思いました。変数の束縛時やトレイトの型引数には ! を直接宣言することは今のところ無理で、関数の戻り値のように明示的な場合はOKということは、型推論や型解決のフェーズでなにか問題があったりするということなのでしょうか。

never type は一度 revert されています。経緯に関してはこの記事がとても詳しいです。このコミットによると、まず戻されたのは stable にするためのスイッチオンの部分だけのようです。

never 型の実装の追加に関する PR はないかと少し探してみました。すると、この PR がそれに該当したようで出てきました。読んでみると、どうやら型チェック周りの実装を修正している形跡があります。そしてこの実装は、(現在のコンパイラとは大きくディレクトリ構成が違う気がするので推測になりますが)現在もコンパイラに残っているようです。つまり、スイッチさえ切り替えればオンにできる状態なのかなと思います。

えー、なんの答えにもなっていないので、私の疑問をまとめておくと:

  • 関数の戻り値と、変数の型宣言やトレイト等の型引数では、指定可能な型が異なる実装をされている?
  • なぜ、関数の戻り値には指定して OK としてあって(これは私の予想では ! は型強制を走らせられるため、最悪使う側で型を解決し、! をなくした状態で型推論に乗せられるから)、その他の箇所で自主的にユーザーが設定するのは NG としたのか(私の予想では、実装当時 !型推論アルゴリズム上なにか解決できないものが存在していたから)?
  • 実装場所どこー

です。Rust の型周りをもう少し勉強しないとわからなそうです。

*1:これは型理論の標準的な入門書とされる TaPL にも載っています。TaPL には、「Bot は空ということである。つまり、型 Bot の閉じた値は存在しない。(...)Bot が空であるからといって、役に立たないということにはならない。むしろ Bot は、ある種の操作(特に、例外の送出や継続の呼び出し)が戻ってこないはずであるという事実を表現するのに大変都合がよい方法である」(p.150)と書かれています。Bot というのはボトム型のことです。

*2:まだ実装していないことを示す際に便利なマクロです。

*3:その分岐・ブロックには決して処理が到達しないはずであることを示すマクロです。

*4:頭痛が痛いみたいな感じがしますね。

*5:んん?さっき型って言ったじゃん!と思うかもしれません(私も思いました)。いくつかのPRを見ていると、full-fledged (羽が生えきった、という意味になるんですが、日本語の文脈に直すなら、要するに完全体とかフル装備とか本格的とかそういう意味です)という単語が出てきます。現状の Rust の never 型は full-fledged ではない状態ということになります。という意味です。たしかに、関数の戻り値としては使用可能だけど、変数宣言や型エイリアス、型引数には使用できないというのは片手落ち感がありますよね。