あえて言おう。Rustが大好きであると。

この記事はRust 2 Advent Calendar 2020 の8日目の記事です。
近年、Rustの人気が上がってきているようで、インターネット上でも、「Rust 好き」等で検索するとさまざまな記事が見つかる反面、逆に「Rust 嫌い」というワードで出る記事が少ないのが不公平な気もするので、あえてRustの良くないなと思う点を挙げてみました。

非同期処理について

Rustにはビルトインの非同期処理があります。これは大きく分けて2つの要素から成り立っています。

Futureトレイト
ランタイム

詳しくは他の記事を参照していただきたいのですが、あまり正確でない説明をすると、1. のFutureクレイトは非同期処理自体を定義するためのトレイトです。例えば、Rustの組み込みキーワードasyncで定義された関数

async fn func() -> Result<(), Error> { /* snip */ }

は、

fn func() -> impl Future<Output = Result<(), Error>> { /* snip */ }

ような感じに変換されます。この時Futureトレイトが使われています。しかしながら、ここでfunc()を

let _ = func();

としても、func()の中身は実行されません。Futureトレイトを実装したオブジェクトが生成されるだけです。
実際にFutureの処理を実行するためには、2. のランタイムが必要であり、これはFutureトレイトと違ってRustの標準ライブラリには含まれません。
よって、ランタイムを実装した外部トレイトに依存する必要があります。

現在、Rustで広く使われているランタイムには(私が知っている限り)以下のものがあります1。

tokio 0.2系

tokio 0.3系

tokio 1.0系 (←追加)
futures

例えば、tokio 0.2系で作られたFutureをtokio 0.3系やfuturesのランタイムで実行することは基本的にできません。
これが特に問題となるのはasync fnを含む外部クレートを利用する場合で、その外部クレートがサポートするランタイムがどれかを利用者が注意深く調べておく必要があります。tokio 0.3系のランタイムを使用したくても、依存するクレートがtokio 0.2系に依存している場合は、0.3系を使うことができません。

現在は移行期であり、今後tokio 0.3系に対応するクレートが増えてくると思いますが、完全に移行し切るまでは、クレートのインストール時に動でfeaturesを指定する等する必要があるでしょう。

使えるトレイトが多くて混乱する

トレイトは、Rustのすばらしい機能の一つです。逆に、Rustの機能の多くはトレイトに依存しているので、トレイトを使いこなせなければRustを便利に活用することは難しいでしょう。
しかし、トレイトには多くの種類があるため、トレイトの全貌を知るのは簡単ではありません。特に、似たような機能が複数のトレイトで提供されている場合があり、ドキュメントを読んでもどう使い分ければいいか理解できないことが多いです。例えば、以下のトレイトはすべて「型の変換」に係るトレイトです。

    • From

 

    • Borrow

 

    • AsRef

 

    • ToOwned

 

    Deref

この中で、Derefはスマートポインタに対して演算子*を適用したときの動作をオーバーライドするトレイトであり、使用には注意が必要です。
BorrowとAsRefについては以下を参照。

Borrow and AsRef

関数の引数と所有権

情報提供: @Iwancof_ptrさん

例えば以下のコードはエラーになります。なぜでしょうか?

struct S;

impl S {
	pub fn outer_ref(&self, _: i32) {
	}
	pub fn outer_mut(&mut self, _: i32) {
	}
	pub fn inner_ref(&self) -> i32 {
		10
	}
	pub fn inner_mut(&mut self) -> i32 {
		10
	}
	
}
 
fn main() {
	let mut s = S;
	s.outer_ref(s.inner_mut());
}

ここで、outer_ref()呼び出しを以下のように展開してみるをわかりやすいと思います。

S::outer_ref(&s, S::inner_mut(&mut s));

これを更に分解するとこのようになります。

    1. 変数sの参照&sを取得する。

 

    1. 変数sの参照&mut sを取得する。

&mut sを引数としてS::inner_mut()を呼び出し値10を得る。

&s及び10を引数としてS::outer_ref()を呼び出す。

ここで、2. から3. までの間、変数sに対する参照&s及び&mut sが共存しています。これがエラーの原因だと思われます。
よって、以下のように書けばエラーは無くなります。

fn main() {
	let mut s = S;
	let i = s.inner_mut();
	s.outer_ref(i);
}

また、試したところs.outer_mut(s.inner_ref())はエラーになりませんでした。原因がわかる方教えてください。(追記: Two-phase borrowによるものとの事です。)

https://t.co/rvZq7Mj6T0こんな感じになりました。うーん。どちらかを mut にすると借用できないっぽいですね。— いわんこ (@Iwancof_ptr) November 21, 2020

(追加) メソッド呼び出し構文のaudo-dereferenceについて

参考

Rustでは、型Xのオブジェクトxに対してメソッドmを呼び出す際に、X::m(x)もしくはx.m()という構文を用いることができます。しかし、後者の場合、自動Deref機能が働き、以下の例のように本来(&x).m()や(*x).m()と書くべきところで*や&を省略することができます。

// ---- & の例 ----
struct X;
impl X {
    fn m(&self) {}
}
let x = X;
X::m(&x);
(&x).m(); // 上に同じ
// ここで&は下のように省略できる
X.m();
// ---- * の例 ----
let y = Box::new(1.5f64);
println!("{}", f64::ceil(*y));
println!("{}", (*y).ceil()); // 上に同じ
// ここで*は以下のように省略できる
println!("{}", y.ceil());

これは便利な機能ですが、時折互換性の問題を起こすという問題があります。

例えば、IntoIteratorというトレイトがあります。これは、vec![1,2,3].into_iter()のようにinto_iter()という関数を提供してイテレータを生成するためのトレイトです。これを配列[T; N]に実装しようという議論があります。

Add IntoIterator impl for arrays by value (for [T; N])

しかし、ここで問題なのは、すでに&[T; N]に対してIntoIterator()が実装されている点です。すなわち、以下のようなコードは現時点でも合法2で、<&[i32; 3] as IntoIterator>::into_iter(&arr)がよばれます。そのため、ループ内で使える変数iは&i32型です。

let arr = [1, 2, 3i32];
for i in arr.into_iter() {
    println!("{}", i);  // i は &i32
}

一方、[T; N]に対してIntoIteratorを実装すると、上のコードでは代わりに<[i32; 3] as IntoIterator>::into_iter(arr)が呼ばれることになり、変数iの型はi32となります。これは明らかに互換性を侵害しています。

実際、上記コードをコンパイルすると以下のような警告が表示されます。

  |
3 |     for i in arr.into_iter() {
  |                  ^^^^^^^^^ help: use `.iter()` instead of `.into_iter()` to avoid ambiguity: `iter`
  |
  = note: `#[warn(array_into_iter)]` on by default
  = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
  = note: for more information, see issue #66145 <https://github.com/rust-lang/rust/issues/66145>

現状の最新のエディションはedition=2018ですが、近い将来のエディションでは解消されるのではないでしょうか。

#rustlang [T; N] に IntoIterator<Item=T>を実装してほしい。&[T; N]に対するIntoIter<Item=&T>はすでに実装されてるけど。— yasuo_ozu@FizzBuzzむずい (@yasuo_ozu) December 28, 2020

async-stdは現在futuresを部的に使っているのと思われます。またactix-rtはtokioを使っているものと思われます。 ↩

&[T]にもIntoIteratorが実装されているため、これに合わせて&[T; N]にも実装しているのだと思います ↩

bannerAds