レッスン8 — Rust:async・トレイト・ジェネリクス
問い
Alloy も Reth も Revm も、コードを読むと必ず async / trait / <T: Bound> が出てくる。この 3 つを最低限読めるようにする — async は Future を返す関数、trait は Java の interface 相当、ジェネリクスは型パラメータでコード再利用。
原理(最小モデル)
async fn. 戻り値は Future(未完了の計算)、.awaitで完了まで進める、Tokio 等のランタイム上で実行。trait. 共有インターフェース、Java/Kotlin の interface に類似、impl Trait for Typeで実装、コンパイル時解決。Box<dyn Trait>/&dyn Trait. 実行時ディスパッチ、複数の具象型を統一で扱う、vtable 経由で遅い代わりに柔軟。- ジェネリクス
<T>+ 境界T: Bound. コンパイル時に型パラメータを具象化、モノモーフ化で実行時オーバーヘッドなし。 impl Trait構文. 引数&impl Provider= 「Provider実装の何か」、戻り値impl Future<Output=T>= 具体的な型は隠蔽。async + trait= async-trait crate or Rust 1.75+ 標準. トレイトメソッドが async を返す場合、過去は#[async_trait]属性が必要、現在は標準対応。
具体例 + ステップで組み立てる
Rust:async・トレイト・ジェネリクス
Revmや Reth のコードを読む前に、3つの言語機能 を押さえる。これがないとAlloy/Reth本体のコードはほぼ読めない。
1. async / await — 「待つ」を書ける
ネットワーク通信のような 時間がかかる処理 を効率よく扱うための機能である。
async fn fetch_block_number() -> u64 {
// ここに重い処理(HTTP通信など)
42
}
#[tokio::main]
async fn main() {
let n = fetch_block_number().await; // .await で実際に待つ
println!("{}", n);
}
async が返すのは「未来の値」
async fn は実行時に Future(あとで値になるもの)を返す。.await を呼ぶまで実際の実行は進まない。
#[tokio::main] の正体
Rust標準には非同期ランタイムが入っていない。tokio でランタイムを起動するのが #[tokio::main] の役割であり、Alloyは tokio 上で動く。
2. トレイト(trait) — 「○○できる」という契約
Java/TypeScriptの interface に近いが、より強力である。「この型は××できる」を宣言する。
trait HasArea {
fn area(&self) -> f64;
}
struct Square { side: f64 }
impl HasArea for Square {
fn area(&self) -> f64 {
self.side * self.side
}
}
let s = Square { side: 3.0 };
println!("{}", s.area()); // 9.0
Alloyではどう使われているか
provider.get_block_number().await?;
provider は Provider トレイトを実装した何らかの型 である。HTTP・WebSocket・IPCで中身が違っても、同じインターフェースで呼べる。これがトレイトの威力である。
トレイトの最重要ポイント — Reth/Revmで頻出
| パターン | 意味 |
|---|---|
impl Trait for Type | 型にトレイトを実装 |
fn f<T: Trait>(x: T) | 「Traitを実装した何か」を受け取る関数 |
Box<dyn Trait> | 動的ディスパッチ(実行時にメソッド解決) |
async fn ... -> Result<T, E> | async関数(Futureを返すトレイト実装の糖衣構文) |
3. ジェネリクス — 「型を後から決める」
Vec<i32> の <i32> がジェネリクスである。Vec は 何でも入れられる が、コンパイル時に要素型を確定する。
fn first<T: Clone>(v: &Vec<T>) -> T {
v[0].clone()
}
let v = vec![10, 20, 30];
let f = first(&v); // T = i32 と推論される
Alloyの Provider<N: Network = Ethereum>
Alloy の Provider は 「どのネットワーク向けか」 を型パラメータで持つ。
let p = ProviderBuilder::new() // デフォルトは Ethereum
.connect_http(rpc_url);
let p = ProviderBuilder::new()
.network::<Optimism>() // 別のネットワークに切り替え
.connect_http(rpc_url);
これは「コンパイル時に型でチェーンを固定し、ランタイムバグを減らす」というRustらしい設計である。
4. ライフタイム(チラ見せ)
&str のような借用には、実は ライフタイム <'a> 注釈が暗黙にある。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() >= y.len() { x } else { y }
}
「返す参照は、引数の参照と同じ寿命を持つ」というアノテーション。最初は読めればOK、書けるのは中級ティアで。
5. これからRevm/Rethで見る形
async fn my_exex<Node: FullNodeComponents>(
mut ctx: ExExContext<Node>,
) -> eyre::Result<()> {
while let Some(notification) = ctx.notifications.recv().await {
// ...
}
Ok(())
}
async fn:時間がかかる処理<Node: FullNodeComponents>:ジェネリクス+トレイト境界while let Some(x) = ...:Optionの中身を取り出すパターン.await?:非同期+エラー伝播
すべて、ここまでで紹介した文法の組み合わせである。読めれば、書ける。
次のレッスンで、実際のRevmの世界に入っていきましょう。
まとめ(3行)
async fn= Future を返す、.awaitで完了待ち、Tokio ランタイム上、Alloy の全 RPC メソッドが async。trait= 共有インターフェース、ジェネリクス<T: Bound>でコンパイル時解決、impl Traitは引数 / 戻り値で短縮記法。Box<dyn Trait>は実行時ディスパッチ(vtable)、ジェネリクスは静的ディスパッチ(モノモーフ化)、次は Revm 実行エンジン紹介。