Rustのトレイトの可視性規則が難しい

引き続きRustをいじっています。

安全でモダンなシステム記述言語という触れ込みですが、何と言いますか、可読性の悪い文法といくつもの慣習が学習を阻害する、という印象です。

今ぶつかっているというか理解を諦めたトレイトの可視性について書いておきます。トレイトはJavaでいうところのインターフェースに似たもので、特定のメソッドを束にして型として扱えるようにしたものです。Struct型にトレイトを付加することで、特定の操作インターフェースを持つ方として扱えます。C++では「特定のクラスとその子孫の型」を引数として持つことで多形的な関数を作ることができました。Rustでは「特定のトレイトを持つStruct」を引数として持つことで、多形的な関数を作ることができます。

さて、今回躓いた制限である

「構造体やenumその他の要素をuseで持ち込むときは、フルパスを書く」

をコンパクトに整理したのが以下のプログラムです。

このプログラムはコンパイル可能で、きちんと動作します。標準入力を読んで行数を標準出力に出力します。実際の計数をするのはline_count()関数です。main()関数ではstdinからミュータブルなストリーム・リーダー変数を作って、その参照をline_count()関数に渡します。そして、line_count()関数の返り値をプリントします。

// count the lines of the standard input

use std::io;
use std::io::BufRead; // Uncommenting this line will cause compile error.

fn main() {
    // Create a reader object from the standard input.
    let mut reader = io::BufReader::new(io::stdin());

    let counter = line_count(&mut reader);

    // Print the returned line count.
    println!("{}", counter);
}

// Count the line of the given stream by parameter.
// Then, return it.
pub fn line_count(reader: &mut impl io::BufRead) -> i32 {
    let mut counter = 0;

    // Take iterator for each line of the stream.
    for _line in reader.lines() {
        // Where compile error occur
        counter += 1;
    }
    counter
}

このプログラムは、関数や型がどこで定義されたかを明らかにするために、パスを一段上のモジュールから書いていることに注意してください。例えば、8行目のio::BufReaderなどがそれです。このioは3行目のuse std::io;によって取り込まれたモジュールです。use文で関数名を直接指定すれば以下のように、パスを指定しない書き方もできますが、慣習によりそうはしないとのことです。

// count the lines of the standard input

use std::io{BufReader, stdin};
use std::io::BufRead; // Uncommenting this line will cause compile error.

fn main() {
    // Create a reader object from the standard input.
    let mut reader = BufReader::new(stdin());

最初のプログラムにもどりますが、このプログラムでは関数line_count()のシグネチャを定義しています。このシグネチャはstd::io::BufRead traitを持つミュータブルな変数への参照を引数として受け取ります。話がややこしいので整理しますが、このプログラムではこういう状況になっています。

  1. 3行目のuse文により、std::io モジュールがグローバルに可視になっている。つまりstd::ioはioとしてアクセスできる。
  2. 4行目のuse文により、std::io::BufReadがグローバルに可視になっている。つまり、std::io::BufReadはBufReadとしてアクセスできる。
  3. 18行目で関数line_count()の引数を指定している。ここではstd::io::BufReadを、ioモジュールからの相対パス指定でio::BufReadとして表記している。

つまり、4行目によりstd::io::BufReadはBufReadとしてアクセスできるのですが、その表記は使わずにio::BufReadとしてアクセスしています。これ自身には問題はないように思えますし、実際このプログラムは意図通りに動きます。

ところが、4行目をコメントアウトするとコンパイル・エラーが起きます。コンパイルエラーが起きるのは22行目です。

// count the lines of the standard input

use std::io;
// use std::io::BufRead; // Uncommenting this line will cause compile error.

fn main() {
    // Create a reader object from the standard input.
    let mut reader = io::BufReader::new(io::stdin());

    let counter = line_count(&mut reader);

    // Print the returned line count.
    println!("{}", counter);
}

// Count the line of the given stream by parameter.
// Then, return it.
pub fn line_count(reader: &mut impl io::BufRead) -> i32 {
    let mut counter = 0;

    // Take iterator for each line of the stream.
    for _line in reader.lines() {
        // Where compile error occur
        counter += 1;
    }
    counter
}

これはとても不思議なことです。ちょっと整理してみましょう。

  1. 3行目のuse文により、std::io モジュールがグローバルに可視になっている。つまりstd::ioはioとしてアクセスできる。
  2. 4行目はコメントアウトされている。std::io::BufReadはBufReadとしてアクセスできない。
  3. 18行目で関数line_count()の引数を指定している。ここではstd::io::BufReadを、ioモジュールからの相対パス指定でio::BufReadとして表記している。

何も問題ないように思えます。実際、コンパイラは18行目を何の問題もなくコンパイルしています。readerはstd::io::BufReadトレイルを持つ型だときちんと認識されています。

ところが、渡された引数を使う段になって22行目でエラーができます。

具体的なエラーメッセージは以下の通りです。

   Compiling study-function v0.1.0 (/home/takemasa/rust/study-function)
error[E0507]: cannot move out of `*reader` which is behind a mutable reference
    --> src/main.rs:22:18
     |
22   |     for _line in reader.lines() {
     |                  ^^^^^^ ------- `*reader` moved due to this method call
     |                  |
     |                  move occurs because `*reader` has type `impl io::BufRead`, which does not implement the `Copy` trait
     |
note: `lines` takes ownership of the receiver `self`, which moves `*reader`
    --> /home/takemasa/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/io/mod.rs:2359:14
     |
2359 |     fn lines(self) -> Lines<Self>
     |              ^^^^

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

エラーメッセージには”move occurs because *reader has type impl io::BufRead, which does not implement the Copy trait”とあります。これも妙な話です。Copyトレイトが無いことがが問題ならば、4行目のコメントアウトを外すだけでコンパイルに成功する理由がありません。4行目ではstd::io::ReadBufしか指定していないのです。

// use std::io::BufRead; // Uncommenting this line will cause compile error.

おそらく、非常にわかりにくい理由で、「ミュータブルな引数にトレイトを持つ型への参照を指定する場合、パスをつけてはいけない」という制限があるのだと思います。

先にリンクを示した「The Rust Programming Language 日本語版 : 慣例に従ったパスを作る」には:

  • 関数を使う場合は一つ上のモジュール名から指定する
  • 構造体やenumその他の要素をuseで持ち込むときは、フルパスを書く

とあります。useで持ち込む時にはフルパスで書けとは、つまりは先の4行目はコメントアウトするな、ということです。

さて、くだんの文書には上の2つのルールの解説のすぐ下にこの一文があります。

こちらの慣例の背後には、はっきりとした理由はありません。自然に発生した慣習であり、みんなRustのコードをこのやり方で読み書きするのに慣れてしまったというだけです。

The Rust Programming Language 日本語版 : 慣例に従ったパスを作る

いやぁ、違いますよね。はっきりとした理由が無いなんてことはないです。だって、フルパスでuseしておかないとコンパイルエラーで止まるのです。これは慣習ではなくて規則ですよ。

憶測ですが、引用した文書は慎重にrustの問題に触れることを避けているのではないでしょうか。まだ文法の完全な定義を読んでいませんが、文法上許されることがコンパイラの制限(あるいはパッケージの問題)で許されておらず、その制限によるトラブルを避けるためには「構造体やenumその他の要素をuseで持ち込むときは、フルパスを書く」というワークアラウンドが必要なように思えます。上記の文書はそれを制限と書きたくなくて慣習としているのかもしれません。

いろいろと弄り回してたどり着いた結論は以下のようなものです。

  • use文は、use文を使うプログラムだけではなく、use文で引用されるパッケージの中の可視性も変えてしまう

ちょっと気持ち悪いです。

コメントする

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください