目次
はじめに
Rust で高い汎用性と再利用性を追求する API を設計する際、呼び出し時に余分な型注釈を要求しない抽象メソッドを提供できると非常に便利です。本記事では次の点を解説します。
- ジェネリック引数を 1つだけ に絞り、カーソルベースの引数バンドルを扱う
Method
トレイトの定義方法 - メソッドを通常の関数呼び出しのように扱うための
CallOwn
と"rust-call"
ABI を使ったFnOnce
実装 - さらに型注釈を一切省略可能にする、PhantomData ベースの ヒント 技法による型引数の自動推論
これらを組み合わせることで、最小限の記述で強力な抽象化を実現します。
Method
トレイトの定義
まずは本題となる Method
トレイトを見てみましょう。ジェネリック引数は 1つだけ、A
(アティック/引数バンドル型)です。ですが呼び出し時に明示する必要はありません。
trait Method {
/// 引数を読み取るためのカーソル列
type CurArgs: Curs;
/// 戻り値を書き込むためのカーソル
type OutputCur: Cursor;
/// `A` と実際の引数型を紐づけるためのヒント
type Hint<A: Atts>;
/// アティック `A` から得られる、実際の引数タプル型
type Args<A: Atts>: Tp;
/// アティック `A` から得られる、戻り値の型
type Output<A: Atts>;
/// 唯一の抽象メソッド
fn method<A: Atts>(self, args: Self::Args<A>) -> Self::Output<A>;
}
* `CurArgs`/`OutputCur` でカーソルと Rust 型の変換方法を定義
* `Hint<A>` がゼロサイズのマーカーとして `A` と具象引数を結びつける
* `Args<A>`/`Output<A>` はアティック `A` に依存した関連型
ジェネリックを 1 つに限定することで、実装側は型パラメータを最小限に抑えつつ、呼び出し側はヒントにより推論だけで済みます。
## `CallOwn`での `FnOnce` 実装
トレイトのメソッドを `call(a, b)` のように呼び出せるよう、`CallOwn` を定義し、Rust の `"rust-call"` ABI を用いて `FnOnce` を実装します。
```rust
struct CallOwn<M: Method, A: Atts>(M, M::Hint<A>);
impl<M: Method, A: Atts> FnOnce<M::Args<A>> for CallOwn<M, A> {
type Output = M::Output<A>;
extern "rust-call" fn call_once(self, args: M::Args<A>) -> Self::Output {
// 抽象メソッドに委譲
self.0.method(args)
}
}
CallOwn
は実装者M
とヒントM::Hint<A>
を保持FnOnce
実装で(args…)
の形でそのまま呼び出し可能に
汎用的なメソッド実装の記述
例として、"test01"
というメソッドを実装してみます。u8
と u64
を受け取り、合計を u64
で返すケースです。
const _: () = {
// アティックから最初と2番目の要素を取り出すヘルパー
type Arg1<A> = UHead<A>;
type Arg2<A> = UHead<UTail<A>>;
impl Method for Name<"test01"> {
type CurArgs = Cons<IntoU8Cur, Cons<IntoU64Cur, Nil>>;
type OutputCur = IntoU64Cur;
// アティック型 A を 2 要素タプルに固定するヒント
type Hint<A: Atts> = Ph<(A, Cons<Arg1<A>, Cons<Arg2<A>, Nil>>)>;
// 実際の引数は Cratic<カーソル, 型>
type Args<A: Atts> = (Cratic<IntoU8Cur, Arg1<A>>, Cratic<IntoU64Cur, Arg2<A>>);
// 戻り値も Cratic<カーソル, Attic>
type Output<A: Atts> = Cratic<IntoU64Cur, impl Attic>;
fn method<A: Atts>(self, args: Self::Args<A>) -> Self::Output<A> {
let a = args.0 .0.into();
let b = args.1 .0.into();
Cratic::<_, IntoU64Att<u64>>((a as u64) + b)
}
}
};
CurArgs
/OutputCur
でカーソル型を指定Hint
によってアティックA
が必ず 2 要素タプルと等価になるよう強制method
内で値を取り出し、計算後に再びCratic
に包んで返却
hint
技法による型引数の推論
通常はメソッド呼び出し時に型注釈が必要ですが、以下の小さな const fn
を使うことで、型 A
を自動推論させます。
#[allow(dead_code)]
const fn hint<A>() -> Ph<(A, A)> {
Ph
}
hint()
は実行時コストゼロのゼロサイズ値であり、内部で A
が等価であることをコンパイラに示します。
まとめ:完全な例とテスト
以下のテストでは、型注釈ゼロでメソッドを連鎖呼び出ししています。
#[test]
fn testprog() {
let a = Cratic::<_, IntoU8Att<u8>>(1_u8);
let b = Cratic::<_, IntoU64Att<u64>>(2_u64);
// "test01": u8 + u64 → u64
let c = CallOwn(Name::<"test01">, hint())(a, b);
// 仮に "test02" が u64 を受け取る別メソッドだとすると
let d = CallOwn(Name::<"test02">, hint())(c);
assert_eq!(103_u64, d.0.into());
}
結論
- ジェネリックを 1つだけ に絞った
Method
トレイトで抽象化を単純化 - PhantomData ベースの
Hint
で呼び出し時の型注釈を完全に排除 CallOwn
によるFnOnce
実装で通常の関数のように呼び出し可能
これにより、Rust の型システムとエルゴノミクスを両立した、カーソルベースの高度に抽象化された API 設計が可能になります。Happy coding!
この投稿が気に入った方は、Rust における継続渡しスタイル (CPS) の簡略化 もぜひご覧ください。
このブログ記事の議論に参加してください:
この記事は AI 搭載の翻訳ツールによって翻訳されました。翻訳に誤りがある場合はご容赦ください。すぐに校正し、考えられる誤りを修正します。誤りを見つけた場合は、GitHub で問題を作成してください。