4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

かうなってて欲しかった Rust

Last updated at Posted at 2025-01-13

Rust は良い言語ですが、それでも書いてるうちに不満を感じることもあります。この記事では、Rust の言語仕様がこんな風になってたら良かったのにといふ個人的な願望を垂れ流してみます。
(記載の内容は Rust 1.84.0, Edition 2021 の仕様に基づいてゐます。)

must_use はデフォルトであって欲しかった

Rust では、函数を呼び出した後にその戻り値を使はなかった時などに「値を使ふのを忘れてるよ」といふコンパイル時警告を発動させるために #[must_use] といふ属性が使はれてゐます。この警告自体は有用ですが、函数 (もしくは構造体などのデータ型) を定義するたびにいちいち #[must_use] 属性を付けて回らないといけません。自分の感覚だと、この属性を付けないことの方が少ないので、「警告を出させるために属性を付ける」のではなく「警告を出させないために属性を付ける」方が楽だったのではないかと思います。

ちなみに「警告を出させないために属性を付ける」タイプの最近の言語としては Swift があります。

今の Rust だと
// 戻り値を無視しても警告されない函数
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::NoneResult::Err を返す API とが両方用意されてゐる場合がちらほらあります。例へば、配列スライスに対する &a[i]a.get(i) とか、Vec に対する reservetry_reserve とかです。かういふのは、API を使ふ側からすると適切な方を吟味して選ぶ手間がかかるし、API を実装する側からすると複数の似た様な API を実装するといふ面倒があります。

また、 Vec::remove の様にパニックするバージョンしか API が用意されてゐないものもあり、使ふ側の設計の自由度や簡潔さを下げます。

この様ないまいちさの元を糺すと、操作がパニックを起こし得るかどうかが型では分からないために、Rust の持つ強力な型システムを意図しないパニックの防止に活かすことができてゐないといふことが分かります。私としては、失敗し得る操作は全て Option または Result を返すように統一し、パニックは必要に応じて呼び出し元で発生させる様な設計思想が良かったのではないかと思ひます。少なくともライブラリが自分でパニックしない様にすれば、それを呼び出す側のプログラマーが明示的にパニックを起こさせない限り、パニックは起きなくなるはずです。 (「起きなくなる」は言ひすぎかもしれませんが、少なくともどこでパニックが起き得るのかはコード上でもっと明確になるのではないでせうか。)

Rust では ! は前置演算子としてもう使はれてしまってゐますが、それをいったん無視すると、 ? と同様に ! を後置演算子として活用する言語仕様もあり得たと思ひます。 ? は「Option または Result の中身が失敗側だったら直ちに return する」といふ効果を持ちますが、それと似た様なノリで ! に「Option または Result の中身が失敗側だったら直ちにパニックする」といふ動作を与へれば、必要に応じてわざとパニックを引き起こさせるのも簡単にできます。

今の Rust だと
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 といふキーワードを一つ付けるだけであらゆるヤバいことが無制限に許可されてしまひます。個人的には、許可される危険な操作の種類を細分化して、個別に許可を得る風になっててくれたら嬉しいです。その方が、自分が何をしようとしてゐる (あるいはしてゐない) のかについてプログラマーがより自覚的になり、コンパイラーに意図をより正確に伝へ、より厳密な型検査ができる様になると思ひます。

今の Rust だと
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 トレイトの型パラメーターはメソッドではなくトレイト自体に付いてゐます。

今の Rust だと
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 の後に変換先の型名を書かうとしても書けないといふ点です。例えば &strString に変換しようとするときに 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)
    }
}

他にも何か思ひ付いたら後で書き足します。

  1. 「そこにある値を drop」の部分は a.assume_init_drop() ではなく直接 std::ptr::drop_in_place(a.as_mut_ptr()) を実行する方が正確な翻訳ですが、unsafe_assume_init との関連を分かりやすくするためにごまかしました。

  2. この間違ひが「ありがち」かどうかは人によるかもしれませんが。

  3. この例だと i32Drop を実装してゐないのでこの間違ひは特に問題にはならないのですが、MaybeUninit の中身が Drop を実装してゐるもの (Vec とか) だと本当に未定義動作になります。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?