Rustでジェネリック関連型を理解する
Ethan Miller
Product Engineer · Leapcell

ジェネリック関連型をRustで理解する
ジェネリック関連型に飛び込もう:Rustにおける高階型の入り口です。
ジェネリック関連型(GATs)についての少しばかりの洞察
その名前はとても長い!一体これは何でしょう?
心配しないでください。最初から分解していきましょう。Rustの構文構造をいくつか復習することから始めましょう。Rustプログラムは何で構成されていますか?答えは:_アイテム_です。
すべてのRustプログラムは、個々のアイテムで構成されています。たとえば、main.rs
で構造体を定義し、2つのメソッドを持つimpl
ブロックを追加し、最後にmain
関数を記述すると、これらはクレートのモジュールアイテム内の3つのアイテムです。
_アイテム_を網羅したので、次は_関連アイテム_について話しましょう。関連アイテムはスタンドアロンのアイテムではありません!重要なのは「関連」という言葉です。何に「関連」しているのでしょうか?それは「特定の型に関連している」という意味であり、この関連付けによってSelf
という特殊なキーワードを使用できます。このキーワードは、関連付けている型を指します。
関連アイテムは、トレイト定義の中括弧内、または実装ブロック内で定義できます。
関連アイテムには、関連定数、関連関数、関連型(エイリアス)の3種類があります。これらは、一般的なRustの3つのアイテム型(定数、関数、型(エイリアス))に直接対応しています。
例を見てみましょう!
#![feature(generic_associated_types)] #![allow(incomplete_features)] const A: usize = 42; fn b<T>() {} type C<T> = Vec<T>; trait X { const D: usize; fn e<T>(); type F<T>; // ← これが新しい部分です!以前は、ここに<T>を記述できませんでした。 } struct S; impl X for S { const D: usize = 42; fn e<T>() {} type F<T> = Vec<T>; }
それの使い道は何ですか?
非常に便利ですが、特定の状況でのみです。Rustコミュニティでは、ジェネリック関連型の2つの古典的なユースケースがあります。それらを紹介してみましょう。
しかし、飛び込む前に、ジェネリクスを簡単に復習しましょう。「ジェネリック」という言葉は、英語で「汎用」を意味します。では、ジェネリック型とは何でしょうか?簡単に言うと、いくつかのパラメータが欠落している型です。パラメータはユーザーによって埋められます。
簡単な注意:以前の翻訳者は、「generic」を「泛型」(文字通り「汎用型」を意味する)としてレンダリングすることを選択しました。多くのシステムでは、型をパラメータ化できるためです。しかし、Rustでは、ジェネリクスは型に限定されません。実際には、型、ライフタイム、および定数の3種類のジェネリックパラメータがあります。
さて、ジェネリック型の具体的な例を次に示します:Rc<T>
。これは、1つのパラメータを持つジェネリック型です。Rc
自体は型ではないことに注意してください。型引数(Rc<bool>
のbool
など)を指定して初めて、実際の型が得られます。
内部でデータを共有する必要があるデータ構造を記述していると想像してください。ただし、ユーザーがRc
またはArc
のどちらを使用したいかを事前に知ることができません。どうすればいいでしょうか?最も簡単な方法は、コードを2回記述することです。少し不格好ですが、機能します。ちなみに、クレートim
とim-rc
は、一方がArc
を使用し、もう一方がRc
を使用することを除いて、ほぼ同一です。
実際、GATは、この問題を解決するのに最適です。ジェネリック関連型の最初の古典的なユースケースである型ファミリを見てみましょう。
タスク#1:GATを使用して型ファミリをサポートする
さて、コンパイラにRc<T>
またはArc<T>
のどちらを使用するかを決定させる「セレクター」を作成しましょう。コードは次のようになります。
trait PointerFamily { type PointerType<T>; } struct RcPointer; impl PointerFamily for RcPointer { type PointerType<T> = Rc<T>; } struct ArcPointer; impl PointerFamily for ArcPointer { type PointerType<T> = Arc<T>; }
とても簡単ですよね?これで、Rc
またはArc
を使用するかどうかを示すために使用できる2つの「セレクター」型を定義しました。これが実際にどのように機能するかを見てみましょう。
struct MyDataStructure<T, PointerSel: PointerFamily> { data: PointerSel::PointerType<T> }
この設定では、ジェネリックパラメータはRcPointer
またはArcPointer
のいずれかになり、それがデータの実際の表現を決定します。これにより、前述の2つのクレートを1つにマージできます。
タスク#2:GATを使用してストリーミングイテレーターを実装する
別の問題があります。これはRustに固有のものです。他の言語では、この問題が存在しないか、解決を諦めています(えへん)。
問題は次のとおりです。APIで依存関係を表したいのです。入力値間、または入力と出力の間。これらの依存関係は、必ずしも表現しやすいとは限りません。Rustの解決策は何でしょうか?
あの小さなライフタイムマーカー'_
です。誰もがそれを見たことがあるでしょう。これは、APIレベルでこれらの依存関係を表すために使用されます。
これを実際に見てみましょう。誰もが標準ライブラリのIterator
トレイトにおそらく精通しているでしょう。それは次のようになります。
pub trait Iterator { type Item; fn next(&'_ mut self) -> Option<Self::Item>; // ... }
素晴らしいように見えますが、小さな問題があります。Item
型は、Iterator
自体の型(Self
)にまったく依存していません。それはなぜですか?
next
を呼び出すと、一時的なライフタイムスコープ('_'
)が作成されます。これはnext
関数のジェネリックパラメータです。一方、Item
はスタンドアロンの関連型です。それをライフタイムに関連付ける方法はありません。
ほとんどの場合、これは問題ではありません。ただし、一部のライブラリでは、この表現力の欠如が真の制限になります。一時ファイルを提供するイテレーターを想像してください。ユーザーは好きなときにファイルを閉じることができます。この場合、Iterator
トレイトは正常に機能します。
ただし、イテレーターが一時ファイルを生成し、そこにデータをロードし、ユーザーが使い終わった_後_にファイルを削除する必要がある場合はどうでしょうか?または、次のファイルのストレージスペースを再利用することをお勧めします。この場合、イテレーターはユーザーがアイテムの使用をいつ終了したかを知る必要があります。
これはまさにGATが役立つ場所です。これらを使用して、次のようなAPIを設計できます。
pub trait StreamingIterator { type Item<'a>; fn next(&'_ mut self) -> Option<Self::Item<'_>>; // ... }
これで、実装はItem
を依存型(参照など)にすることができます。型システムは、next
を再度呼び出すか、イテレーターをドロップする前に、Item
値が使用されなくなったことを保証します。
あなたはとても地に足が着いていました-もう少し抽象的にできますか?
それでは、ここから、人間の言葉を話すのをやめましょう。(冗談ですが、これから抽象的になります。)注:この説明はまだ簡略化されています。たとえば、バインダーと述語は脇に置きます。
まず、ジェネリック型コンストラクターと具象型の関係を確立することから始めましょう。本質的に、それはマッピングです。
/// 疑似コード fn generic_type_mapping(_: GenericTypeCtor, _: Vec<GenericArg>) -> Type;
たとえば、Vec<bool>
では、Vec
はジェネリック型の名前であり、コンストラクターでもあります。<bool>
は型引数のリストです。この場合は1つだけです。両方をマッピングにフィードすると、特定の型Vec<bool>
が得られます。
次に:トレイト。トレイトとは一体何でしょうか?トレイトもマッピングです。
/// 疑似コード fn trait_mapping(_: Type, _: Trait) -> Option<Vec<AssociateItem>>;
ここで、Trait
は述語と考えることができます。型について判断を下すものです。結果は、None
(「このトレイトを実装していない」の意味)またはSome(items)
(「この型はトレイトを実装している」の意味)のいずれかで、関連アイテムのリストが付属しています。
/// 疑似コード enum AssociateItem { AssociateType(Name, Type), GenericAssociateType(Name, GenericTypeCtor), // ← これは新しい追加です AssociatedFunction(Name, Func), GenericFunction(Name, GenericFunc), AssociatedConst(Name, Const), }
これらのうち、AssociateItem::GenericAssociateType
は現在、Rustでgeneric_type_mapping
が_間接的に_呼び出される唯一の場所です。
異なるType
を最初のパラメーターとしてtrait_mapping
に渡すことにより、同じTrait
から異なるGenericTypeCtor
を取得できます。次に、generic_type_mapping
を適用すると、Rustの構文フレームワーク内で、異なるジェネリック型コンストラクターが特定のVec<GenericArg>
引数と組み合わされます。
ちょっと寄り道:GenericTypeCtor
のような構造は、一部の記事でHKTと呼ばれるものです-高階型。上記のアプローチのおかげで、Rustは初めてHKTのユーザー向けサポートを提供します。この1つの形式でしか表示されませんが、他の使用パターンをそこから構築できます。
要するに:奇妙な新しい力がアンロックされました!
歩くことを学ぶ:GATで高度な構造を模倣する
さて、まとめとして、GATを使用して他の言語のいくつかの構造を模倣してみましょう。
#![feature(generic_associated_types)] #![allow(incomplete_features)] trait FunctorFamily { type Type<T>; fn fmap<T, U, F>(value: Self::Type<T>, f: F) -> Self::Type<U> where F: FnMut(T) -> U; } trait ApplicativeFamily: FunctorFamily { fn pure<T>(inner: T) -> Self::Type<T>; fn apply<T, U, F>(value: Self::Type<T>, f: Self::Type<F>) -> Self::Type<U> where F: FnMut(T) -> U; } trait MonadFamily: ApplicativeFamily { fn bind<T, U, F>(value: Self::Type<T>, f: F) -> Self::Type<U> where F: FnMut(T) -> Self::Type<U>; }
次に、これらのトレイトを特定の「セレクター」に実装してみましょう。
struct OptionType; impl FunctorFamily for OptionType { type Type<T> = Option<T>; fn fmap<T, U, F>(value: Self::Type<T>, f: F) -> Self::Type<U> where F: FnMut(T) -> U, { value.map(f) } } impl ApplicativeFamily for OptionType { fn pure<T>(inner: T) -> Self::Type<T> { Some(inner) } fn apply<T, U, F>(value: Self::Type<T>, f: Self::Type<F>) -> Self::Type<U> where F: FnMut(T) -> U, { value.zip(f).map(|(v, mut f)| f(v)) } } impl MonadFamily for OptionType { fn bind<T, U, F>(value: Self::Type<T>, f: F) -> Self::Type<U> where F: FnMut(T) -> Self::Type<U>, { value.and_then(f) } }
さて、これでOptionType
を「セレクター」として使用して、Option
の動作をFunctor、Applicative、およびMonadとして表現および実装できます。
それで-どう感じますか?まったく新しい世界の可能性を解き放ったのでしょうか?
Leapcellは、Rustプロジェクトをホストするための最適な選択肢です。
Leapcellは、Webホスティング、非同期タスク、およびRedis向けの次世代サーバーレスプラットフォームです。
多言語サポート
- Node.js、Python、Go、または Rustで開発します。
無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ支払い — リクエストも請求もありません。
比類のないコスト効率
- アイドル料金なしの従量課金制。
- 例:25ドルで、平均応答時間60ミリ秒で694万リクエストをサポートします。
合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的なUI。
- 完全に自動化されたCI / CDパイプラインとGitOpsの統合。
- 実用的な洞察のためのリアルタイムのメトリックとロギング。
簡単なスケーラビリティと高性能
- 高い並行性を簡単に処理するための自動スケーリング。
- 運用上のオーバーヘッドはゼロ — 構築に集中するだけです。
ドキュメントで詳細をご覧ください!
Xでフォローしてください:@LeapcellHQ