LoginSignup
30
10

More than 1 year has passed since last update.

【Rust】個人的に注目中のUnstable Features(2022年1月)

Posted at

最近、RustでUnstableな機能を使っているのですが、なんとなく雰囲気で使っているので、詳細をもう少し掘り下げて知ろうと思っていました。
そこで、ついでに最近こういうアウトプットを行っていなかったので、この機会にまとめを書こうと思います。

rustバージョンは、1.60.0-nightlyです。

Unstable Featuresを使う

Unstable Featuresとは、まだ安定化されていない、Nightlyバージョンでのみ試験的に使用できるFeatureのことです。
Unstable Bookにその一覧が載っています。また、RFCの一覧も参考になるかと思います。

Unstable Featureを使うためには、まずnightlyバージョンのrustcをインストールします。
nightlyのインストールはrustupのドキュメントに載っています(英語ですが)。

rustup toolchain install nightly

通常使うtoolchainをnightlyにするには

rustup default nightly

とします。

もし、通常はstableを使うけど、対象のプロジェクトやワークスペースのみnightlyにしたい場合は、プロジェクトのトップレベル(Cargo.tomlがある場所)に"rust-toolchain"というファイル名でファイルを作成し、その中に"nightly"という文字を書いておくと、cargo build等がnightlyで実行されるようになります。(この方法をお勧めします。)

そして、ソースのトップ(main.rsやlib.rs)に#![feature(featureの名前)]属性を付与すれば、そのプロジェクト全体で指定したfeatureが使えるようになります。

個人的に注目中のUnstable Features

言語仕様に関するFeatureをメインで挙げます。

generic_associated_types

関連型にその関連型専用の型引数やライフタイム注釈を設定できるようになります。通称GATs。

#![feature(generic_associated_types)]

trait GatsTrait: Sized {
    type Gats<'a, T>
    where
        Self: 'a,
        T: 'a;

    fn gats_fn<'a: 'b, 'b, T>(
        &'a mut self,
        val: &'b mut T,
    ) -> (Self::Gats<'a, Self>, Self::Gats<'b, T>);
}

impl<U> GatsTrait for U {
    type Gats<'a, T>
    where
        Self: 'a,
        T: 'a,
    = Option<&'a mut T>;

    fn gats_fn<'a: 'b, 'b, T>(
        &'a mut self,
        val: &'b mut T,
    ) -> (Self::Gats<'a, Self>, Self::Gats<'b, T>) {
        (Some(self), Some(val))
    }
}

どういう意味があるかというと、これまでは関連型は、トレイトが実装されている型の型引数やライフタイムの関係を定義することは出来ましたが、トレイトのメソッドで定義されている型引数やライフタイムの関係を定義することは出来なかったのです。

上記の例はあまり実用性はありませんが、GATsの例としてよく、StreamingIterator(LendingIterator)やMonadが紹介されています。
ここではStreamingIteratorについて触れてみます。

例えば、VecとそのVec内の要素を指すindexで構成されたIteratorを以下のように定義したいとします。

struct VecStruct<T> {
    vec: Vec<T>,
    index: usize,
}

impl<'a, T> Iterator for VecStruct<T> {
    type Item = &mut T;
    fn next(&mut self) -> Option<Self::Item> {
        if let Some(item) = self.vec.get_mut(self.index) {
            self.index += 1;
            Some(item)
        } else {
            None
        }
    }
}

このItem = &mut Tのライフタイムはどのように定義できるでしょう。ここのライフタイムはnextメソッドの&mut selfのライフタイムと同じです。しかし、メソッドで使用されているライフタイムを関連型で定義することは出来ません。そこで以下のような定義のイテレータを作成します。

#![feature(generic_associated_types)]

trait StreamingIterator {
    type Item<'a>
    where
        Self: 'a;
    fn next(&mut self) -> Option<Self::Item<'_>>;
}

この定義では、Itemにライフタイム注釈がついているため、Itemを使用する場所ごとに異なるライフタイムを使うことが出来ます。

impl<T> StreamingIterator for VecStruct<T> {
    type Item<'a>
    where
        Self: 'a,
    = &'a mut T;
    fn next(&mut self) -> Option<Self::Item<'_>> {
        if let Some(item) = self.vec.get_mut(self.index) {
            self.index += 1;
            Some(item)
        } else {
            None
        }
    }
}

generic_associated_typesは公式Blogでも推されているようなので、おそらく近いうちに安定化すると思われます。また、同じ関連型のfeatureとして、associated_type_defaultsassociated_type_boundsも注目しています。

unboxed_closures, fn_traits

pub trait FnOnce<Args> {
    type Output;
    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}
pub trait FnMut<Args>: FnOnce<Args> {
    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}
pub trait Fn<Args>: FnMut<Args> {
    extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}

クロージャの定義はこのようになっています。unboxed_closuresは、extern "rust-call"で関数定義を可能にし、fn_traitsは、クロージャを任意の型に実装できるようにします。

#![feature(unboxed_closures)]
#![feature(fn_traits)]

struct Adder {
    a: i32
}

impl FnOnce<(i32, )> for Adder {
    type Output = i32;
    extern "rust-call" fn call_once(self, b: (i32, )) -> Self::Output {
        self.call(b)
    }
}
impl FnMut<(i32, )> for Adder {
    extern "rust-call" fn call_mut(&mut self, b: (i32, )) -> Self::Output {
        self.call(b)
    }
}
impl Fn<(i32, )> for Adder {
    extern "rust-call" fn call(&self, b: (i32, )) -> Self::Output {
        self.a + b.0
    }
}

上記のAdder型は、クロージャとして振る舞うことが出来ます。

let adder = Adder { a: 2 };
assert_eq!(adder(2), 4);
assert_eq!([1, 2, 3, 4, 5].map(adder), [3, 4, 5, 6, 7]);

具体的な見える型に対して、クロージャを実装することができるので、トレイトにクロージャを返すメソッドを定義したり出来るようになるのが大きいかと思います。

unsized_fn_params

関数の引数にサイズ不定の変数を渡せるようになります。主にdyn Traitのためのfeatureだと思っています。

本来、Selfをパラメータに持つトレイトは、トレイトオブジェクトに出来ないのですが(Sizedではないため、パラメータに出来ない)、FnOnceは普通にBox<dyn FnOnce>として扱えます。これにはトリックがあって、

impl<Args, F: FnOnce<Args> + ?Sized, A: Allocator> FnOnce<Args> for Box<F, A> {
    type Output = <F as FnOnce<Args>>::Output;

    extern "rust-call" fn call_once(self, args: Args) -> Self::Output {
        <F as FnOnce<Args>>::call_once(*self, args)
    }
}

という定義があり、これにより、Boxが直接FnOnceを実装する、Self(Box)がSizedになるので、Boxで包んでいる限り、dyn FnOnceが扱えます。

ただし、本来は*selfがSizedではないため、上記のような定義はコンパイルエラーとなります。なので、この定義を可能にするため、coreライブラリには#![feature(unsized_fn_params)]が設定されており、サイズ不定の引数を扱えるようになっています。

もう少し一般的なトレイトでやってみると、以下のようになります。

#![feature(unsized_fn_params)]

trait MyTrait {
    fn my_proc(self);
}

impl<F> MyTrait for Box<F>
where
    F: MyTrait + ?Sized,
{
    fn my_proc(self) {
        (*self).my_proc();
    }
}

このような感じで、Selfをパラメータに持つトレイトをトレイトオブジェクトとして扱えるという点で注目しています。

また、似たようなfeatureでunsized_localsがありますが、こちらはまだincompleteとなっています。

generators, generator_trait

途中で中断、再開できる関数を作成できるようになります。他の言語でもよくある、yieldを使えるようになります。

#![feature(generators, generator_trait)]

fn gen<F>(iter: F) -> impl Generator<i32, Yield = i32, Return = &'static str>
where
    F: Iterator<Item = i32>,
{
    move |mut i: i32| {
        for v in iter {
            i = yield v + i;
        }
        "done"
    }
}

let mut generator = gen([1, 2, 3, 4, 5].into_iter());

let mut buf = 0;
loop {
    match Pin::new(&mut generator).resume(buf) {
        GeneratorState::Yielded(i) => {
            print!("{} ", i);
            buf += i;
        }
        GeneratorState::Complete(s) => {
            print!("{}", s);
            break;
        }
    }
}
// 1 3 7 15 31 done

Generatorトレイトは、すでにライブラリドキュメントが用意されているので、そちらを参照してください。
現状ではこれだけの機能ですが、今後、streamなどと組み合わせて非同期で楽に使えるような仕様になれば、非常に便利になると思います。

また、似たような機能を実現するライブラリで、tokioのasync-streamもあります。

specialization, min_specialization

あるトレイトの実装を、汎用の実装と特化した実装に分けて実装できるようになります。

specializationは、結構前からrfcに上がっているのですが、仕様がまだ固まっていないようで、incompleteとなっています。しかし、その一部だけでも標準ライブラリで使用するため、min_specializationというサブセットが作られました。昨年に安定化したmin_const_genericsと似たような流れなのでしょうか。

#![feature(min_specialization)]

trait DebugString {
    fn debug_string(&self) -> String;
}

// 1. 一般的な型に実装する
impl<T> DebugString for T
where
    T: Debug,
{
    // default
    default fn debug_string(&self) -> String {
        format!("{:?}", self)
    }
}

// 2. 1について特化した型に実装する
impl DebugString for i32 {
    // specialized
    fn debug_string(&self) -> String {
        format!("{:#?}", self)
    }
}

上記のように、まず、一般的な型についてトレイトを実装します。このとき、fnの前にdefaultというキーワードをつけます。そして、それを特化した実装をdefaultキーワード無しで実装します。

残念ながら、min_specializationでspecializationのどこまでの範囲が使えるようになるのかよくわからなかったので、これ以上説明が出来ません。

min_specializationに関しては、すでに標準ライブラリで使用されているので、気になる方はチェックしてみてください。現在、ToStringの実装等で使われています。

try_block

?演算子のスコープとなるtryブロックを作成できるようになります。

?演算子は、エラーを受け取ると早期returnで関数を抜けるため、関数内で?演算子を使いつつResultOptionの処理を行いたい場合は

let result: std::io::Result<String> = (|| {
    let mut file = File::open("foo.txt")?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
})();

のように、クロージャでスコープを作る必要がありました。これが、tryブロックが使えるようになると

#![feature(try_block)]

let result: std::io::Result<String> = try {
    let mut file = File::open("foo.txt")?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    contents
};

のように、クロージャを作らずに書くことが出来ます。

tryブロック内のreturntryブロックを通り越して関数を抜けるので、クロージャを使うよりも扱いやすくなります。
もともと?演算子とセットで?のスコープブロックも提案されていたようですが、?演算子が先に安定化されて、tryブロックは諸々仕様を議論して今に至っているようです。

また合わせて、?演算子が使える型を定義できるtry_trait_v2 featureも便利そうです。

最後に

Unstable Featureを知っていると、もちろんそれを使うという意味でも役立つのですが、Stableで今できないことがわかるので、余計な試行錯誤せずに済んだりします。ここに上げたFeatureは、そこそこメジャーなものだと思いますので、頭の片隅においておくといつか役立つかもしれません。

また、ここに載せた情報は、あまり正確な情報が見つからないものが多かったので、私が検証して想像したものも含まれています。誤った情報があれば、ご報告いただければ修正します。

30
10
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
30
10