📚

[Rust] なぜ Iterator<char> の .collect() で String に変換できるのか?

2023/09/03に公開

疑問

  1. なぜ Iterator.collect() 利用時に型を明記する必要があるのか?
  2. なぜ Iterator<char>.collect()String に変換できるのか?

playground

// 1. なぜ Iterator の .collect() 利用時に型を明記する必要があるのか?
fn main() {
    let v = vec![1, 2, 3];
    // これはコンパイルエラー
    // let s = v.into_iter().collect();
    let s: Vec<i32> = v.into_iter().collect();
    println!("{:?}", s);
}

playground

// 2. なぜ Iterator<char> の .collect() で String に変換できるのか?
fn main() {
    let v = vec!['a', 'b', 'c'];
    // .collect() で Iterator<char> を String に変換できる
    let s: String = v.into_iter().collect();
    println!("{:?}", s);
}

まとめ

  • .collect() は、FromIterator を実装する型を返すが、 FromIterator を実装するものが複数あるので、型推論できません。そのため、型を明示的に指定する必要があります。
  • StringFromIterator<char> を実装しています。そのため、 .collect()String 変換できます。

型推論できないのがコンパイルエラーの原因

コンパイルエラーの内容を確認します。Rust はこの手のエラーメッセージが非常に親切なので助かります。 error[E0282]: type annotations needed というエラーでした。

error[E0282]: type annotations needed
 --> src/main.rs:5:9
  |
5 |     let s = v.into_iter().collect();
  |         ^
  |
help: consider giving `s` an explicit type
  |
5 |     let s: Vec<_> = v.into_iter().collect();
  |          ++++++++

For more information about this error, try `rustc --explain E0282`.
error: could not compile `playground` (bin "playground") due to previous error

rustc --explain E0282 を実行するとより詳細な説明も得られます。

The compiler could not infer a type and asked for a type annotation.
...

通常、Rust は型を省略して書いても型推論により型を自動的に推論して決めてくれます。しかし、型推論時に一つに型を特定できない場合やそもそも特定できない場合、このエラーが出ます。

型推論の(複数の)候補は一体何なのか?

.collect() で返される型が複数ありうるため型推論が失敗しているとすると、一体それは何があるのだろうか。.collect() はどんな型を返すのかを確認してみることでわかるようになります。

.collect() の定義は、以下のようになっている。ジェネリクス関数であって、FromIterator<Self::Item> を実装している型 B を返すことになっています。

    fn collect<B: FromIterator<Self::Item>>(self) -> B
    where
        Self: Sized,
    {
        FromIterator::from_iter(self)
    }

つまり、FromIterator を実装している型が複数あるということになります。実際、標準ライブラリのコードを見てみると、以下のようになっています。

Vec<T> の例

impl<T> FromIterator<T> for Vec<T> {
    #[inline]
    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Vec<T> {
        <Self as SpecFromIter<T, I::IntoIter>>::from_iter(iter.into_iter())
    }
}

HashSet<T> の例

impl<T, S> FromIterator<T> for HashSet<T, S>
where
    T: Eq + Hash,
    S: BuildHasher + Default,
{
    #[inline]
    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> HashSet<T, S> {
        let mut set = HashSet::with_hasher(Default::default());
        set.extend(iter);
        set
    }
}

これ以外にもたくさんのコレクションの型が FromIterator を実装していました。これでは型推論で型は一つに定まることはまずあり得ないです。二つめの疑問も String の実装を見ることで納得できました。

String の例

impl FromIterator<char> for String {
    fn from_iter<I: IntoIterator<Item = char>>(iter: I) -> String {
        let mut buf = String::new();
        buf.extend(iter);
        buf
    }
}

つまり、Iterator<char> を使って .collect() する際には Vec<char> をはじめとする各種コレクションの型もありえるし、String にもなり得るということです。

あとがき

まとめはこちらです。以下は感想です。

  • この FromIterator を適切に実装するカスタムクラスを作ることもできそうです。
  • 思い返せば Java だと Stream.collect は引数で .collect(HashSet::new) のような形で HashSet に変換していましたが、Rust では trait を実装して型推論させることで、型の方を指定すれば良いようになっています。これは単に作りの問題なのか型推論の精度の差などに起因するのか興味が出てきました。この点はまた調べてみたいです。

Discussion