最近、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_defaults、associated_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で関数を抜けるため、関数内で?
演算子を使いつつResult
やOption
の処理を行いたい場合は
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
ブロック内のreturn
はtry
ブロックを通り越して関数を抜けるので、クロージャを使うよりも扱いやすくなります。
もともと?
演算子とセットで?
のスコープブロックも提案されていたようですが、?
演算子が先に安定化されて、try
ブロックは諸々仕様を議論して今に至っているようです。
また合わせて、?
演算子が使える型を定義できるtry_trait_v2 featureも便利そうです。
最後に
Unstable Featureを知っていると、もちろんそれを使うという意味でも役立つのですが、Stableで今できないことがわかるので、余計な試行錯誤せずに済んだりします。ここに上げたFeatureは、そこそこメジャーなものだと思いますので、頭の片隅においておくといつか役立つかもしれません。
また、ここに載せた情報は、あまり正確な情報が見つからないものが多かったので、私が検証して想像したものも含まれています。誤った情報があれば、ご報告いただければ修正します。