本記事はRust Advent Calendar 2022の13日目の記事です。

本記事では、標準的なRustの教材ではあまり触れられない、RustのUnstable(experimental)機能について解説していきます。
まず、unstable機能というものの存在意義から説明します。なんとなく知っている人は飛ばして構いません。

Rustは”Empowering everyone to build reliable and efficient software.”というキャッチコピーから分かる通り、堅牢さを売りの一つとしており、強い後方互換性と安定性を保証します。しかしこのスタンスは開発の遅滞化を招きかねないというデメリットもあります。
そこで、Rustはコンパイラについてnightly/beta/stableの3バージョンを公開しています。nightlyバージョンはほぼ最も不安定で、最も早く最新の機能を試せます。それがunstable機能です。一般のユーザーにも不安定な機能を公開し、バグがないか検証してもらおうという訳です。

unstable機能はbeta/stableコンパイラでは使えず、nightly版コンパイラでのみ使えます。しかもデフォルトでは使えず、#![feature(…)]アトリビュートで宣言しなければなりません。nightlyであっても不用意にコードが壊れないように細心の注意を払うRustの思想が垣間見えます。

Rustのunstable機能の一覧はThe Unstable Bookから確認できます。ざっと400個ほどもあるようです。以下ではその中の一部をピックアップして紹介します。

注目のunstable機能

box_patterns

#![feature(box_patterns)]
// x: Option<Box<X>>
match x {
    Some(inner) => {
        let inner = *inner;
        ...
    }
    ...
}
// ↓
match x {
    Some(box inner) => {
        ...
    }
    ...
}

パターンマッチングでBox型の中身を取り出すことができるようになります。refパターンと似たような感じですね。

box_syntax

#![feature(box_syntax)]
let boxed: Box<Str> = Box::new("aaa");
// ↓
let boxed: Box<str> = box "aaa";

boxというキーワードが追加され、Box::newの代わりに使うことが出来ます。
……というだけの機能に見えますが、なんとこの機能、7年以上もunstableのままという泥沼状態となっています。
調べてみたところ、box syntaxは単なるBox::newの構文糖ではないようです。Box型はオブジェクトをヒープに配置するポインタ型ですが、Box::newを使う場合、引数を一度スタックに置く必要があるようです。これはスタックサイズが小さい環境で大きな構造体を生成する際に問題となります。boxはそれを最適化し、スタックを経由せず直接ヒープに配置するための構文というわけです。

(しかし、それならこの機能は単に内部実装の変更であり、Box::newを差し置いてboxという構文をわざわざ入れる必要もないような気もします。Box::newは関数なので、使えてしまう場所が多い=最適化しにくいのが問題なのでしょうか。)

追記: やはりあまり意味がないということでstableに入らないまま削除という運びになったようです。
https://github.com/rust-lang/rust/pull/108471

generators

#![feature(generators, generator_trait)]

use std::ops::{Generator, GeneratorState};
use std::pin::Pin;

fn main() {
    let mut generator = || {
        yield 1;
        return "foo"
    };

    match Pin::new(&mut generator).resume(()) {
        GeneratorState::Yielded(1) => {}
        _ => panic!("unexpected value from resume"),
    }
    match Pin::new(&mut generator).resume(()) {
        GeneratorState::Complete("foo") => {}
        _ => panic!("unexpected value from resume"),
    }
}

Pythonのジェネレータのようなことが出来るようになります。すなわち、yieldで関数を一時中断して、再度呼び出した際にそこから再開できるということです。
クロージャっぽく見えますが、Generetorトレイトを実装する別のリテラルとして扱われるようです。

once_cell

現在のRustは、const式以外で安全にグローバルデータを保管する標準的な方法をほぼ提供しません。外部crateに頼る必要があります。once_cellはその一つです。遅延初期化によって実行時まで確定しない値でもconstにバインドできるようになります。本機能はonce_cellをstdに入れるものです。

use std::sync::Lazy;

// `BACKTRACE` implements `Deref<Target = Option<String>>` 
// and is initialized on the first access
static BACKTRACE: Lazy<Option<String>> = Lazy::new(|| {
    std::env::var("RUST_BACKTRACE").ok()
});

result_option_*

Option/Result型には便利なunstableメソッドが幾つか生えています。その一部を紹介します。

inspect

Optionの中身を覗くことができます。.map(|x| {…; x})のshort-handです。

#![feature(result_option_inspect)]

let x: Option<&usize> = Some(1).inspect(|x| println!("inner: {x}")); // inner: 1

unzip

Option<(T, U)>型に対して定義されるメソッドで、中身を2つのOptionに分解できます。

#![feature(unzip_option)]

let x = Some((1, "hi"));
assert_eq!(x.unzip(), (Some(1), Some("hi")));

追記: Rust 1.66でstabilizeされました。

closure_lifetime_binder

fn func(_: impl Fn(&i32) -> &i32) {}

fn main() {
    func(for<'a> |arg: &'a i32| -> &'a i32 { arg });
}

クロージャでライフタイム指定が出来るようになります。forを使うのはちょっと気持ち悪いですね。まあimpl ~ forでも使われていますが。

concat_idents

let foo_1 = 1;
let foo_2 = 2;
assert!(concat_idents!(foo, _1) == 1);
assert!(concat_idents!(foo, _2) == 2);

識別子を結合することが出来ます。マクロを作るときには役に立つかもしれません。が、個人的にはこのようなことをするならば配列かconst functionを代わりに使ったほうが良いと感じます。

最近安定化された機能

最近(ver 1.62~)安定化された機能の中で個人的に便利だと感じたものをピックアップします。

const_panic

const文脈でpanicが出来ます。ただしまだformattingは出来ません。

#[repr(u8)]
enum E {
    A = 1,
    B = 2,
    ...
}

impl E {
    const fn from_byte(b: u8) -> E {
        match b {
            1 => E::A,
            ...
            _ => panic!("invalid input"),
        }
    }
}

let_else

x: Result<Option<X>>
if let Ok(Some(inner)) = x { inner } else { todo!() };
// ↓
let Ok(Some(inner)) = x else { todo!() }; 

else内は発散する項(!型の項。panic!や無限ループなど通常の値を返さない)でなくてはなりません。

derive_default_enum

#[derive(Default)]
enum Type {
   Int,
   Bool,
   ...
   #[default]
   Illegal,
}

fn main() {
   let mut v = vec![Type::Int];
   v.resize_with(3, Default::default);
assert_eq!(v, [Type::Int, Type::Illegal, Type::Illegal]);
}

enumの選択肢の一つに#[default]属性をつけると、Default::default()でそれを返すようになります。

广告
将在 10 秒后关闭
bannerAds