Rust は良い言語ですが、それでも書いてるうちに不満を感じることもあります。この記事では、Rust の言語仕様がこんな風になってたら良かったのにといふ個人的な願望を垂れ流してみます。
(記載の内容は Rust 1.84.0, Edition 2021 の仕様に基づいてゐます。)
must_use はデフォルトであって欲しかった
Rust では、函数を呼び出した後にその戻り値を使はなかった時などに「値を使ふのを忘れてるよ」といふコンパイル時警告を発動させるために #[must_use]
といふ属性が使はれてゐます。この警告自体は有用ですが、函数 (もしくは構造体などのデータ型) を定義するたびにいちいち #[must_use]
属性を付けて回らないといけません。自分の感覚だと、この属性を付けないことの方が少ないので、「警告を出させるために属性を付ける」のではなく「警告を出させないために属性を付ける」方が楽だったのではないかと思います。
ちなみに「警告を出させないために属性を付ける」タイプの最近の言語としては Swift があります。
// 戻り値を無視しても警告されない函数
fn f() -> i32 {
0
}
// 戻り値を無視すると警告される函数
#[must_use]
fn g() -> i32 {
1
}
// 戻り値を無視しても警告されない函数
#[maybe_unused]
fn f() -> i32 {
0
}
// 戻り値を無視すると警告される函数
fn g() -> i32 {
1
}
パニックし得る箇所をもっと明示的にして欲しかった
Option
から値を取り出すときには (パターンマッチングをしない場合は) unwrap
とか unwrap_or
とか unwrap_or_default
とかのメソッドを使ひますが、このうち unwrap
は値が無い時に直ちにパニックするもので、他の unwrap_or...
系メソッドは値が無い時に代はりの値を返します。パニックを起こし得る操作は他の操作よりもより注意深く扱ひたいものですが、メソッド内でパニックが起き得るかどうかはメソッドの命名パターンからは読み取れず区別できません。パニックを起こし得る unwrap
が他の unwrap_or...
系メソッドよりも短い名前になってゐるのは、潜在的危険性のある方の処理をより呼び出しやすくしてゐる様で、プログラマーにフレンドリーではない気がします。
Rust には、同じ操作に対して失敗時にパニックする API と Option::None
や Result::Err
を返す API とが両方用意されてゐる場合がちらほらあります。例へば、配列スライスに対する &a[i]
と a.get(i)
とか、Vec
に対する reserve
と try_reserve
とかです。かういふのは、API を使ふ側からすると適切な方を吟味して選ぶ手間がかかるし、API を実装する側からすると複数の似た様な API を実装するといふ面倒があります。
また、 Vec::remove
の様にパニックするバージョンしか API が用意されてゐないものもあり、使ふ側の設計の自由度や簡潔さを下げます。
この様ないまいちさの元を糺すと、操作がパニックを起こし得るかどうかが型では分からないために、Rust の持つ強力な型システムを意図しないパニックの防止に活かすことができてゐないといふことが分かります。私としては、失敗し得る操作は全て Option
または Result
を返すように統一し、パニックは必要に応じて呼び出し元で発生させる様な設計思想が良かったのではないかと思ひます。少なくともライブラリが自分でパニックしない様にすれば、それを呼び出す側のプログラマーが明示的にパニックを起こさせない限り、パニックは起きなくなるはずです。 (「起きなくなる」は言ひすぎかもしれませんが、少なくともどこでパニックが起き得るのかはコード上でもっと明確になるのではないでせうか。)
Rust では !
は前置演算子としてもう使はれてしまってゐますが、それをいったん無視すると、 ?
と同様に !
を後置演算子として活用する言語仕様もあり得たと思ひます。 ?
は「Option
または Result
の中身が失敗側だったら直ちに return
する」といふ効果を持ちますが、それと似た様なノリで !
に「Option
または Result
の中身が失敗側だったら直ちにパニックする」といふ動作を与へれば、必要に応じてわざとパニックを引き起こさせるのも簡単にできます。
let a = [1, 2, 3];
let b: i32 = a[1];
let c: Option<&i32> = a.get(3);
let mut v = Vec::<i32>::new();
v.reserve(3);
let r: Result<(), TryReserveError> = v.try_reserve(99999999999999999);
let a = [1, 2, 3];
let b: i32 = a[1]!;
let c: Option<&i32> = &a[3];
let mut v = Vec::<i32>::new();
v.reserve(3)!;
let r: Result<(), TryReserveError> = v.reserve(99999999999999999);
unsafe はもっと細分化されてて欲しかった
Rust でコードを書くにあたって unsafe なものに関はらずに済むに越したことはありませんが、実現したいことによっては unsafe な何かに触れざるを得ないこともあります。ただ、今の Rust だと unused といふキーワードを一つ付けるだけであらゆるヤバいことが無制限に許可されてしまひます。個人的には、許可される危険な操作の種類を細分化して、個別に許可を得る風になっててくれたら嬉しいです。その方が、自分が何をしようとしてゐる (あるいはしてゐない) のかについてプログラマーがより自覚的になり、コンパイラーに意図をより正確に伝へ、より厳密な型検査ができる様になると思ひます。
use std::mem::MaybeUninit;
fn main() {
let mut a = MaybeUninit::<i32>::uninit();
unsafe { a.as_mut_ptr().write(42); }
let b = unsafe { a.assume_init() };
println!("{b}");
}
use std::mem::MaybeUninit;
fn main() {
let mut a = MaybeUninit::<i32>::uninit();
#[allow(unsafe_pointer_dereference)]
{ a.as_mut_ptr().write(42); }
#[allow(unsafe_assume_init)]
let b = a.assume_init();
println!("{b}");
}
上の例だととりあへず unsafe_pointer_dereference
(ポインターの参照外し) と unsafe_assume_init
(初期化されてないかもしれない値を初期化済みだと言ひ張る) の二種類を挙げてみましたが、unsafe な演算の分類としては他にも unsafe_pointer_arithmetic
(ポインター演算) とか unsafe_static_access
(静的変数へのアクセス) とかが考へられさうです。
上記の説明だけだといまいち有用性がお分かりいただけないかもしれませんが、例へば上の例を下の様に書き換へるとコンパイル時エラーになって欲しいといふのが私の想定です。
use std::mem::MaybeUninit;
fn main() {
let mut a = MaybeUninit::<i32>::uninit();
#[allow(unsafe_pointer_dereference)]
{ *a.as_mut_ptr() = 42; } // ← この行を書き換へた
#[allow(unsafe_assume_init)]
let b = a.assume_init();
println!("{b}");
}
なぜこれがエラーなのか? それは write
函数と代入演算子の意味論の違ひによります。代入演算子は、「そこにある値を drop した後、新しい値をそこに移す」といふ動作をします。つまり、 *a.as_mut_ptr() = 42
は以下のコードと等価です。1
a.assume_init_drop();
a.as_mut_ptr().write(42);
しかし、先ほどのコードでは *a.as_mut_ptr() = 42
の部分には allow(unsafe_pointer_dereference)
だけが注釈されてゐて allow(unsafe_assume_init)
は付いてゐませんから、 assume_init_drop
函数の呼び出しに相当する部分が許可されずエラーになるといふ訣です。これで、ポインターに write
函数を使ふべきところでうっかり代入演算子を使ってしまふといふありがちな間違ひを捕へることができました。23
実際に上の様なことをしようとすると、unsafe な操作を正確に分類することが実際には相当難しさうだと想像されます。しかしそれこそが unsafe な世界が持つ複雑さの一端であり、現状その複雑さの相手をすることはすべてプログラマーに任されてゐます。そこをもう少し言語やその処理系の側から手助けしてくれる様になってゐたらなぁと思ひます。
値.into::<変換先型>()
と書きたかった
Into
トレイトの型パラメーターはメソッドではなくトレイト自体に付いてゐます。
pub trait Into<T>: Sized {
fn into(self) -> T;
}
impl<T, U> Into<U> for T
where
U: From<T>,
{
fn into(self) -> U {
U::from(self)
}
}
これの微妙に不便なところは、into といふ英単語の自然な用法に従って into
の後に変換先の型名を書かうとしても書けないといふ点です。例えば &str
を String
に変換しようとするときに let x = "foo".into::<String>();
みたいには書けません。
Into
トレイトが以下の様に定義されていたら、その様な書き方もできたのですが……。
pub trait Into: Sized {
fn into<T>(self) -> T
where
T: From<Self>;
}
impl<T> Into for T {
fn into<U>(self) -> U
where
U: From<Self>,
{
U::from(self)
}
}
他にも何か思ひ付いたら後で書き足します。