Rustのturbofishを理解する

このエントリはRustその3 Advent Calendar 20193日目に飛び入り参加しているエントリです。 Rustの型パラメータ指定の構文、通称ターボフィッシュ(turbofish)について。

Rustでジェネリクス関数は以下のように関数名に続いて <パラメータ名> で指定しますよね?

fn generics<T>(t: T) {
    // ...
}

これを呼び出すときはどうやって指定しましょう?直感的にはこうですよね?

generics<u8>(0)

しかしこれは構文エラーです。

error: chained comparison operators require parentheses
 --> turbofish.rs:6:13
  |
6 |     generics<u8>(0);
  |             ^^^^^
  |
  = help: use `::<...>` instead of `<...>` if you meant to specify type arguments
  = help: or use `(...)` if you meant to specify fn arguments

error: aborting due to previous error

エラーメッセージにも書いてある通り、型を指定するときは名前と型パラメータの間に :: を置く必要があります。

generics::<u8>(0)

この ::<> という構文は魚がロケットエンジンで右に進んでいるように見えるからか、turbofishと呼ばれています。

::<> ::<> ::<> ::<> ::<>
  ::<> ::<> ::<> ::<> ::<>
::<> ::<> ::<> ::<> ::<>

よくターボフィッシュのお世話になる関数の1つは str::parse でしょうか。以下のように使います。

use std::net::Ipv4Addr;
// 型推論が効かない環境でもターボフィシュを使えば `Ipv4Addr` を指定できる
let addr = "127.0.0.1".parse::<Ipv4Addr>();

ところでなんで :: が必要なの?というと、どうも :: がないと他の構文と区別がつかないケースがあるからです。

fn generics2<T1, T2>(_: (T1, T2)) {
  // ...
}

let tuple = ('a', 0);
// 関数呼び出し + 型パラメータの指定のつもりのコード
(generics2<char, i32>(tuple))
let generics2 = 'b';
let char = 'a';
let i32 = 0;
let tuple = 1;
// 2つの比較演算のタプルのつもりのコード
(generics2<char, i32>(tuple))

現行のRustでは後者の方が既に合法なコードとして存在するので簡単には入れられません。

さて、この文法さえ覚えれば基本はオッケーなんですが、どこに書くのか意外と迷いやすいです。

例えば以下のように str の値を String に変換したいとします。

let s: String = "str".into();

今回のケースは単純なので変数の方に型アノテーションを書くこともできますが、メソッドチェーンの途中だと型アノテーションを書ける場所がなく、ターボフィッシュを使いたいこともあるでしょう。そういうケースでのお話しです。

parse の類推から、 into にターボフィッシュをあてがえばコンパイルできそうに思えます。

let s = "str".into::<String>();

しかし残念ながら、このように unexpected type argument が出てしまいます。

error[E0107]: wrong number of type arguments: expected 0, found 1
 --> turbofish.rs:6:26
  |
6 |     let s = "str".into::<String>();
  |                          ^^^^^^ unexpected type argument

error: aborting due to previous error

For more information about this error, try `rustc --explain E0107`.

これは Into の定義を見ると納得がいきます。

pub trait Into<T> {
    fn into(self) -> T;
}

よくみると into メソッドではなくて Into のトレイトの方に型パラメータがついていますね。 なので Into の方にターボフィッシュが必要です。

let s = Into::<String>::into("str");

まあ、このケースでは From があるので From を使った方が手っ取り早いでしょう。

let s = String::from("str");

今回のは From があるという細かい話はありますが、基本は型パラメータのあるところにターボフィッシュが必要と覚えて下さい。

例えば Vecnew はこう。

let v = Vec::<u32>::new();

これでターボフィッシュをマスターできましたか?

実はまだよく分からないケースがあります。 enum の型パラメータはどうやらバリアントにも付けられるようです。

Option は以下のように定義されています。

pub enum Option<T> {
    None,
    Some(T),
}

順当にいけば None の型パラメータは以下のように指定するのが正しいですね。

let o = Option::<String>::None;

実際、これは正しくコンパイできます。

しかし、以下のような書き方も許容されています。

let o = None::<String>;

どうも、昔のRustは Option::None という書き方ができず、ターボフィッシュを置く場所がなかったのでバリアントに型パラメータが書けるようになったようです。 複雑ですね。

ということで飛び入りの小ネタでした。

Written by κeen
Later article
itertoolsの紹介
Older article
Rust 2020