Rustで use std; が必要なときとエラーになるときがあるのは何故か

Rustでは use std;use rand; のようなインポートが必要な場合と、逆に書くとエラーになる場合がある。

簡単に言うと

  • トップレベルモジュールでは、書くとエラーになる(既にある名前と衝突する)。それ以外の場所では、必要な場合がある。
  • これは名前をローカルで使うための仕組みと、名前の別名を公開するための仕組みが同じ use により実現されていることと関係している。

そもそも use std; は何故必要なのか

例えば、あるモジュール m1.rs 内で以下のようなコードを書くとエラーになる。

fn foo() {
    let stderr = std::io::stderr();
}
rustc 1.18.0 (03fc9d622 2017-06-06)
error[E0433]: failed to resolve. Use of undeclared type or module `std`
 --> m1.rs:2:18
  |
2 |     let stderr = std::io::stderr();
  |                  ^^^^^^^^^^^^^^^ Use of undeclared type or module `std`

error: aborting due to previous error

呼ぼうとしているのは確かに std::io::stderr なのにこれが発見されないのは何故か。これは、この手のパスが文脈によって異なる解釈をされることに由来する。そもそもパス(::で区切られているやつ)は次の3種類に分けられる。

  • :: または $crate:: で始まるもの (例: ::std::default::Default)
  • self または super で始まるもの (例: self::MyTrait) (※単独の self は例外)
  • それ以外
    • use path;pub(in path) では、絶対パスとして解釈される。
    • それ以外の文脈では、レキシカルスコープのパス (相対パスの亜種) として解釈される。

つまり、 std::io::stderr はここでは絶対パスではなく、相対パスのようなものとして扱われてしまっているのである。

絶対パスとして修正する場合

先ほどのコードで意図しているのは絶対パスだと考えられる。したがって次のようにすればエラーにはならない。

fn foo() {
    let stderr = ::std::io::stderr();
}

レキシカルスコープのパスとして修正する場合

もう一つの解釈として、レキシカルスコープのパスとしての使用を意図していた可能性も考えられる。例えば、以下のコードは正しい。

use std::io;
fn foo() {
    let stderr = io::stderr();
}

同じ要領で、 std をインポートすればよいという考えがありえる。これも正しい。

use std;
fn foo() {
    let stderr = std::io::stderr();
}

トップレベルモジュールでは不要なのはなぜか

トップレベルモジュールでは通常、 extern crate が多く行われている。例えば、 extern crate rand; と書いておくと、 rand のトップレベルモジュールが ::rand にリンクされる。

ここでもし m1 モジュール内で use rand; をすると、これがさらに ::m1::rand にリンクされることになる。これにより m1 の直下で rand を簡単に参照できるようになる。

同じ理屈で、トップレベルモジュールで use rand; をすると、 ::rand::rand にリンクしようとしていることになる。これはおかしいのでエラーになってしまうという寸法である。

こういった理屈のため、 use nalgebra as na; のような別名インポートはトップレベルモジュールでも必要である。例えば、

extern crate nalgebra;
use nalgebra as na;

..

mod m1 {
    use nalgebra as na;
}

のようになる。もちろん、以下のようにそもそも extern crate の時点で別名をつけてリンクすることもできる。(好ましい習慣かは別として)

extern crate nalgebra as na;
// use 不要

mod m1 {
    use na;
}

Rustの use の設計について

Rustの use は2つの目的を兼ねている。これは例えばこのNiko Matsakis氏のコメントでも確認できる。

  • ある定義(関数や型など)に別名をつける(別名をつけて公開する)こと。 (再エクスポート)
  • ある定義をローカルから使うためにスコープに入れること。 (インポート)

前者はモジュールグラフの切り貼り自体を目的にしているのに対して、後者は let などと同様にレキシカルスコープへの名前の導入ができればよく、グラフの切り貼りは意図していない。しかしRustでは両者を同じように、グラフの切り貼りで実現する仕組みになっている。このことは、 use の動作を理解するにあたって押さえておくとよいところかもしれない。

まとめ

  • トップレベルモジュールでは、 use std; を書くとエラーになる(既にある名前と衝突する)。それ以外の場所では、 use std; が必要な場合がある。
  • これは名前をローカルで使うための仕組みと、名前の別名を公開するための仕組みが同じ use により実現されていることと関係している。