
Raph Levien は、非常に巧妙な Rust ベースの SIMD 設計を使用して、最速の グリフ レンダリング エンジン を超効率的に実装したことで有名です。彼の作品は、2023 年に Bestechnics で SIMD GPU を設計していたときに、私に大きなインスピレーションを与えてくれました。その後、Ralf Levien はこのグリフ レンダリングの GPU バージョンにも取り組みました。
そこで、彼の最新の講演「Rustはいかにして勝利したか:高性能で信頼性の高いソフトウェアを求めて」(https://www.youtube.com/watch?v=k_-6KI3m31M)を偶然見つけたとき、私は1時間かけてその講演を見て、学ぶ価値のある洞察を何か得ようと決心しました。

激戦区(溺れるゾーンとも呼ばれる)は、Rustの秘伝のソース:より正確なタイプというタイトルのスライドから始まり、およそ55:00頃です。

56:50で、Raphは「ここでアフィン型が登場します。クローン可能性を『このインスタンスが1つ存在する』としてモデル化したいのです。そして、この型を使うことで、関数シグネチャで所有権の処理方法を細かく指定できます。最後の例は省略しますが、面白いですよね…」と述べています。
私は決してRustのエキスパートを気取っているわけではありませんし、Goの方が少し好きだという自覚もあります。そこで質問なのですが、アフィン型 が実際に何を意味するのか、理解できるでしょうか?また、膨大な知識の海に溺れることなく、平易な言葉で説明してもらえるでしょうか?
YouTubeのコメントで指摘されているもう一つの疑問点ですが、最後の関数の戻り値の型は、Cow<&'a [T]>ではなくCow<'a, [T]>であるべきのようです。正直なところ、これは量子アルゴリズムよりも複雑に思えます。しかし、もしかしたらそうである必要はないのかもしれません。
それでは、これらの「より正確なタイプの秘訣」の例を詳しく見ていきましょう。
fn α( l: &[T] ) → Vec<T> : 常に割り当てを行い、入力には影響しません
fn β( l: Vec<T> ) → Vec<T> : 入力を消費し、割り当てを再利用します。
fn δ( l: &mut Vec<T> ): 割り当てなし、入力ベクトルを変更するだけ
fn ω( l: &'a [T]) → Cow<'a,[T]>: 割り当てが発生する可能性がある。重複が末尾のみにある場合は割り当てを回避する。
汎用テンプレート T で最初に確認すべき問題は、本当に正しく動作しているか?これらの関数はそのままコンパイルできるのか?最もシンプルなバージョンの関数で試してみましょう。
// fails with: error cannot find type T in this scope
fn α( l: &[T] ) -> Vec<T> { vec![] }
これは明らかに機能せず、「このスコープで型 T が見つかりません」というエラーで失敗します。C++ に精通していれば、型 T は関数名とともに宣言する必要があると予想できます。したがって、α を宣言すると、 の代わりに を使用します。
// all right with the T declaration
fn α<T>( l: &[T] ) -> Vec<T> { vec![] }
この方法では問題なく動作します!しかし、基本的な実装(重複を削除せずにvecに変換するだけ)を追加すると、コンパイラは「トレイト境界T: Cloneが満たされていません」というエラーで失敗します。
// fails with: the trait bound `T: Clone` is not satisfied
fn α<T>( l: &[T] ) -> Vec<T> { l.to_vec() }
幸いなことに、Rustにはジェネリックとトレイトに関する非常に優れたドキュメントがあり(https://doc.rust-lang.org/book/ch10-02-traits.html)、10分ほど読めば、α を変更する必要があることが明らかになります。<T> からα<T: Clone> タイプ T の 特性境界 を表現する手段として。
// all right with the trait bound
fn α<T: Clone>( l: &[T] ) -> Vec<T> { l.to_vec() }
次のステップは、重複を削除するソリューションを実装することです。このためのアプローチの1つは、HashSetを使用することです。まず、配列をイテレータに変換します(let iterator: std::slice::Iter<T> = l.iter();) をイテレータとして、このイテレータからハッシュセットを構築し (HashSet::from_iter(iterator))、最後にハッシュセットをベクトル set.into_iter().collect()` に変換します。
//Fails with: the trait bound `T: Eq` is not satisfied
use std::collections::HashSet;
fn α<T: Clone>( l: &[T] ) -> Vec<T> {
let array_iterator: std::slice::Iter<T> = l.iter();
let hash_set: HashSet<&T> = HashSet::from_iter(array_iterator);
hash_set.into_iter().collect()
}
当然のことながら、これは失敗します。これは当然のことです。なぜなら、Rust の trait ドキュメントを 10 分読んでいれば、HashSet は型 T の要素を「比較」できる何かを必要としていることがわかったはずだからです。これは Eq トレイトが行うことなので、それをトレイト境界 (α から) に追加しましょう。<T: Clone> からα<T: Clone + Eq> ):
//Fails with: the trait bound `T: Hash` is not satisfied
use std::collections::HashSet;
fn α<T: Clone + Eq>( l: &[T] ) -> Vec<T> {
let array_iterator: std::slice::Iter<T> = l.iter();
let hash_set: HashSet<&T> = HashSet::from_iter(array_iterator);
hash_set.into_iter().collect()
}
それでも失敗します。その理由は、型 T をハッシュ値に変換できる必要があることを言及し忘れたからです。これは Hash トレイトが行うことです。では、α から変更してみましょう。<T: Clone + Eq> からα<T: Clone + Eq + Hash> `
//Fails with: a value of type `Vec<T>` cannot be built from an iterator over elements of type `&T`
use std::collections::HashSet;
fn α<T: Clone + Eq + std::hash::Hash>( l: &[T] ) -> Vec<T> {
let array_iterator: std::slice::Iter<T> = l.iter();
let hash_set: HashSet<&T> = HashSet::from_iter(array_iterator);
hash_set.into_iter().collect()
}
ここでクローン作成の面白さが発揮されます。ハッシュセットは、最初にリストに含まれていた要素へのポインタを含む構造体であり、それらの要素は値渡しで渡されました(正確には、配列はポインタで渡され、配列には値で参照される要素が含まれていました)。そのため、ハッシュセットにこれらの要素のコピーを作成するように指示する必要があります。これは、最後の collect の前に cloned() を追加することで実行できます(collect はイテレータをベクターコレクションに変換するために使用されます)。
// all right!
use std::collections::HashSet;
fn α<T: Clone + Eq + std::hash::Hash>( l: &[T] ) -> Vec<T> {
let array_iterator: std::slice::Iter<T> = l.iter();
let hash_set: HashSet<&T> = HashSet::from_iter(array_iterator);
hash_set.into_iter().cloned().collect()
}
さあ、最初の関数例の準備ができました。
fn main() {
println!("{:?}", α(&["A","A","B","D"]));
println!("{:?}", α(&[1,1,2,4]));
}
正直言って、これは複雑でもあり、そうでもない。複雑なのは、コンパイラが厳格なルールを強制し、それらは暗黙的に導出するのではなく明示的に宣言する必要があるため、学習曲線が急峻になるからだ。複雑ではないのは、ドキュメントを「10分」読んでしまえば、あとは簡単になるはずだからだ。おそらく問題は、ドキュメントを読むのに10分かかることではなく、書かれている内容を理解するのに10時間かかることだろう。これはまさにデジャヴュだ…。
クローン可能性についてまだ質問があります。配列に渡された要素への参照のみを含むベクトルを返すことは可能でしょうか?一見すると、Vec を変更するだけで済むように思えます。 をVec<&T>` に変換し、アフィン型の制約を適切に処理するにはどうすればよいでしょうか?試してみましょう。
// All right - works fine
use std::collections::HashSet;
fn α<T: Clone + Eq + std::hash::Hash>( l: &[T] ) -> Vec<&T> {
let array_iterator: std::slice::Iter<T> = l.iter();
let hash_set: HashSet<&T> = HashSet::from_iter(array_iterator);
hash_set.into_iter().collect()
}
よし、うまくいった!ライフタイムに関する問題はないのか?つまり、要素を借用している状況なので、ライフタイムをチェックする必要があるのではないだろうか?問題は、main関数が単純すぎることなのか?では、もっと複雑なことを試してみよう。
// fails with expected named lifetime parameter
fn limited_lifetime() -> vec<&str> {
let l = ["A","A","B","D"];
α(&l)
}
fn main() {
let v = limited_lifetime();
println!("{:?}",v);
}
今回は失敗します。コンパイラはライフタイムパラメータが不足していると警告するので、その10分を費やしてRustのドキュメントのライフタイム付き参照の検証を読んでみましょう。一見すると、‘a ? を追加するだけでよいように見えます。
// fais with: mismatched types; expected `&[str]`, found `&[&str; 4]`
fn limited_lifetime<'a>() -> Vec<&'a str> { ... }
しかし、それではうまくいきません。「不変の文字列」を動的な「文字列」に変換してみましょう。
// fails with: cannot return value referencing local variable `l`
fn limited_lifetime<'a>() -> Vec<&'a String> {
let l = ["A".into(),"A".into(),"B".into(),"D".into()];
α(&l)
}
さて、これはまさに想定通りのエラーです。借用チェッカーは正しく動作しています。では、ローカルに割り当てられた変数の所有権を移転するようにコンパイラに伝えるにはどうすればよいでしょうか?そのためには、配列 l をヒープに割り当てるようにコンパイラに指示する必要があります。そしてそのためには、配列の代わりに vec を使用する必要があります。
// this works
fn β<T: Clone + Eq + std::hash::Hash>( l: Vec<T> ) -> Vec<T> {
let hash_set: HashSet<_> = l.into_iter().collect();
hash_set.into_iter().collect()
}
fn limited_lifetime<'a>() -> Vec<&'a str> {
let l2 = vec!["A","B","B","D"];
β(l2)
}
fn main() {
println!("{:?}",limited_lifetime());
}
このバージョンは、新しいβ関数が要素のベクトルを受け取り、要素へのポインタではなく同じ要素を返すため、実際に機能します。つまり、要素が複製されているのです。では、β関数が入力要素へのポインタを返すようにしたい場合はどうすればよいでしょうか?
3つ目の課題:書き込み時にクローンを作成する
見出しへのリンク

最後のオメガ関数に出てきた「牛」を覚えていますか?
fn ω( l: &’a [T]) → Cow<&’a [T]>
Cowは実際には列挙型です。不変(借用)参照または同じオブジェクトの可変クローンを保持できます。例:
use std::borrow::Cow;
fn trim_input(input: &str) -> Cow<str> {
if input.ends_with(' ') {
Cow::Owned(input.trim_end().to_string())
}
else Cow::Borrowed(input)
}
では、Cow<'a [T]> と書くべきでしょうか、それとも Cow<&'a [T]> と書くべきでしょうか?一見すると、入力はライフタイム 'a を持つ型 [T] のオブジェクトへのポインタ & なので、出力も同じ型のオブジェクトへのポインタになります。違いは、Cow<> もポインタを表すということです。したがって、正解は Cow<'a, [T]> となります。
fn ω<'a, T>( l: &'a [T]) -> Cow<'a, [T]> where [T]: ToOwned { Cow::Borrowed(l) }
ここで興味深いのは、Rustでは配列の長さがコンパイル時に固定されるということです。では、実行時に動的なサイズの新しい配列を割り当てることは可能でしょうか?どうやら、このstack overflowのコメントによると、それは不可能のようです。もしかしたら、Ralfは代わりにvecを使うつもりだったのかもしれません。
fn ωx<'a, T: Clone + Eq + std::hash::Hash>( l: &'a Vec<T>) -> Cow<'a, Vec<T>> {
let hash_set: HashSet<_> = l.into_iter().collect();
if hash_set.len()==l.len() {
return Cow::Borrowed(l);
}
let own_copy = hash_set.into_iter().cloned().collect();
return Cow::Owned(own_copy);
}
正直に言うと、私は「fn ω<‘a, T>( l: &‘a [T]) -> Cow<‘a, [T]> where [T]: ToOwned { Cow::Borrowed(l) }」という構文で自滅してしまったことを認めざるを得ません。これは単に「同じもののクローン」を意味するはずです。
&'a T を Ptr<'a,T> と書き換えることができると考えると、関数も次のように書き換えることができます。
fn ω<'a, T>( l: Ptr<'a, [T]>) -> Cow<'a, [T]> where [T]: ToOwned { Cow::Borrowed(l) }
Tが配列であるという事実を省略すると、上記のコードは次のように書き換えることができます。
fn ω<'a, T: ToOwned>( l: Ptr<'a, T>) -> Cow<'a, T> { Cow::Borrowed(l) }
では、型を構文ではなくマクロベースの式として書き直すのはどうでしょうか?
fn ω<'a, T>( l: Ptr<'a, T> ) -> !Cow(l) { Cow::Borrowed(l) }
!Cow(l)マクロは書き換えを許可することで、型TがトレイトToOwnedにバインドされている必要があることを暗黙的に示します。このように考えると、'aとTは、Ptrトレイトを使用する際に明示的に指定する必要のない汎用型であると予測できます。
fn ω( l: Ptr ) -> !Cow(l) { Cow::Borrowed(l) }
では、コンパイル時の型と実行時の実装を区別せずに指定するのは紛らわしい。しかし、もしかしたらそれこそがRustの本質なのかもしれない。
# 結論
ラルフの「秘訣」は、1時間の講演でたった3分で説明できるものではなく、1時間かけて説明し、専用のスライドセットを用意するべきものだと思います。また、批判的な視点を持つことも重要です。すべてが楽しいわけではないかもしれません。過剰な設計によって、多くの開発者が興味を失っている可能性もあります。そして、もしかしたら、それが「楽しさ」の本質なのかもしれません。いずれにせよ、今後数週間かけて、1時間の講演用のスライドをいくつか作成してみようと思います。
量子コンピューティングの課題に戻りましょう。問題は、量子コンピューティングシステムを機能させるために、同様の複雑さが必要なのかどうかということです。もしかしたら、これが現代の課題なのかもしれません。一見複雑に見えますが、量子アルゴリズムは、この野蛮なRust構文に比べれば非常に単純です。量子コンピューティングの「有用性」という課題を解決するには、一歩先を見据える必要があるのかもしれません。そして、この複雑さを加えなければ、基本的な計算しかできないということでしょうか?
これがすべて操作したいオブジェクトのセマンティクスに関することだと仮定すると、量子ビットの上に論理的な抽象化レベルが存在することを認識する必要があるかもしれない。しかし、業界がまだ量子ビットの動作を制御するのに苦労しているため、それはまだ明らかになっていない。