2章: proc macro 間での情報伝達と delegation crate 比較
1章では基本的な使い方を見ました. この章ではいくつかの技術的な課題のうち, 最も重要な proc macro 間での情報伝達方法 を扱います. またそれに伴い, 既存の delegation 系 crate の比較 も行います.
構文的な良し悪しはこの章では判断しません. 3章でまとめて扱います.
問題: proc macro 間での情報伝達
thin_delegate の基本的な使い方は以下の様なものでした.
#[thin_delegate::register] // (1-2)
trait ShapeI {
fn area(&self) -> f64;
}
struct Rect {
width: f64,
height: f64,
}
#[thin_delegate::register] // (1-3)
struct Window {
rect: Rect,
}
impl ShapeI for Rect {
fn area(&self) -> f64 {
self.width * self.height
}
}
#[thin_delegate::fill_delegate]
impl ShapeI for Window {} // (1-4)
(1-2) で trait ScoledShape
, (1-3) で struct Window
の情報を覚え, (1-4) でこれらの情報を利用してメソッドを自動実装しています.
つまり, 複数の proc macro 間で trait や型に紐付く情報を伝達しています.
また, この問題は 外部 crate の trait の扱い も要素として絡んできます.
さて, これはどうやるのでしょうか?
この問題へのアプローチ方法は crate 毎に異なります. なので既存の具体的な crate を挙げ, それの実装方針を見ることにしましょう.
enum_dispatch
- crates.io
- repository
- doc
- Initial commit: 2018/12
enum_dispatch は (僕が知る限り) trait method の自動 delegation を扱った最古の crate です. crates.io で daily (?) ダウンロード35k越えで, Twitter でも度々目にします. 自分もこれを最初に手に取りました.
enum_dispatch の基本的な使い方は以下です.
#[enum_dispatch::enum_dispatch] // (2-1)
trait ShapeI {
fn area(&self) -> f64;
}
struct Rect {
width: f64,
height: f64,
}
#[enum_dispatch::enum_dispatch(ShapeI)] // (2-2)
enum Shape {
Rect(Rect),
}
impl ShapeI for Rect {
fn area(&self) -> f64 {
self.width * self.height
}
}
fn main() {
let rect = Rect { width: 2.0, height: 3.0 };
assert_eq!(rect.area(), 6.0);
let shape = Shape::Rect(rect);
assert_eq!(shape.area(), 6.0);
}
(2-1) で trait 定義の情報を登録し, (2-2) で enum 定義と自動 delegation を同時に行っています.
thin_delegate の様に impl
は不要です. また, trait と enum の順番を変えることもできます.
この情報伝達は proc macro crate 中の global variables に文字列化した trait/enum の定義を保存する ことによって行われます. より詳細には 1:
- (2-1) の trait
ShapeI
に対するenum_dispatch::enum_dispatch
呼び出しにおいて, traitShapeI
の token stream を文字列化したものをHashMap
TRAIT_DEFS
に保存する [code] [TRAIT_DEFS
]. このとき generics の情報も一部HashMap
のキーとして含める [code]. - (2-2) の enum
Shape
に対するenum_dispatch::enum_dispatch
呼び出しにおいて, enumShape
の token stream を文字列化したものをHashMap
ENUM_DEFS
に保存する [code] [ENUM_DEFS
]. このとき generics の情報も一部HashMap
のキーとして含める [code]. - (2-1), (2-2) で引数として enum/trait 名が与えられていた場合 (上では (2-2)), その組が両方定義されていれば自動 delegation する. そうでなければ 1 defer する [code].
- (2-1), (2-2) で defer した組が解決されたとき, 自動 delegation する 2 [code].
最初にこの処理を見たときは感動しました. 「これって合法なんだ!!」と. でも実は合法ではありませんでした.
What do you suggest proc macros to be aware of each other as opposed to a static variable? から引用:
bjorn3
Jun 2022Proc macros should be expandable independently and without any side effects. We don't offer any guarantees around the order and amount of times a proc macro is expanded. For example rust-analyzer will only re-expand proc macros if you change the actual invocation of the proc macro. Rustc could also start caching proc macro outputs in the future I think.
意約:
proc macro は副作用を持たずに独立して展開されるべきです. proc macro の展開においてその順序も呼び出し回数も何も保証されません. 例えば rust-analyzer は proc macro の呼び出し箇所の変更毎に再展開されます. 将来的には rustc が proc macro 出力をキャッシュすることも考えられます 3.
これに照らして上記の方法を評価してみましょう.
- そもそも proc macro 呼び出し時 global variable がどういう状態か何も保証されていない. (例えば呼び出し毎に新しいプロセスを起動するのはコンパイラの挙動として合法.) 以下では状態が保存されていて一部 rustc と挙動が違うと仮定する.
- 順序不定: defer していることにより trait/enum の順序が逆になっても問題ない (はず).
- 複数回呼び出し:
cache_trait()
,cache_enum_dispatch()
はHashMap::insert()
を利用しており, key が重複する場合は更新されるので中身の変更は大丈夫 (なはず). trait/enum 名や型パラメータを弄った場合は古い定義が別エントリとして残るので駄目. また, (2-2) -> (2-1) -> (2-1) で評価された場合最初の (2-1) で defer が 消費 されているから駄目 (なはず) 4. - キャッシュ: キャッシュ単体で異常ケースはない気がする? (まぁでも副作用なしという重大な前提条件が満たされていないので...)
...と, 全体的によろしくないです.
ふたつめの enum_dispatch の良くない部分として, 外部 crate の trait をサポートしていないということが挙げられます.
ある crate で enum_dispatch::enum_dispatch
を使い
[1],
別の crate で同じ identifier に対して使っても
[2]
[3]
それ自体ではエラーにはなりません.
一方, 同一 crate 内で同じ identifier に対して使うと意図せぬ挙動になります
[4 rs]
[4 stdeerr].
このエラーは trait/enum の identifier (名前) を HashMap
のキーとして利用し, それを module 毎に区別していないのが原因です.
(実装がナイーヴすぎる.)
この差は compilation unit (crate) を跨いでいるときに global variable を引き継いでいないからだと思われます 5. 逆に言えば, 引き継げない ということでもあります.
というわけで, global variable を使う方法では異なる proc macro 間で情報を伝達できません.
みっつめ. generics や trait bound などのサポートが弱い.
struct Rect<T> {
width: T,
height: T,
}
#[enum_dispatch::enum_dispatch]
enum Shape<T> {
Rect(Rect<T>),
}
#[enum_dispatch::enum_dispatch(Shape<usize>)] // (2-3)
trait ShapeI {
fn area(&self) -> f64;
}
impl<T> ShapeI for Rect<T>
where
T: std::ops::Mul,
{
fn area(&self) -> f64 {
(self.width * self.height) as f64
}
}
これで (2-3) で以下の様なコードが生成され,
impl<T> ShapeI for Shape<T> {
#[inline]
fn area(&self) -> f64 {
match self {
Shape::Rect(inner) => ShapeI::area(inner),
}
}
}
T: std::ops::Mul
という制約が付いていないのでエラー
[rs]
[stderr]
になります.
これは単純に実装の問題です. 3章で議論します.
enum_dispatch
まとめ
- enum_dispatch は proc macro crate 中の global variables に文字列化した trait/enum の定義を保存する ことで proc macro 間で情報伝達しているが, この方法は根本的な問題がある.
- 外部 crate の trait/enum 定義を扱えない.
- generics や trait bound のサポートが弱い.
- エラーがわかりにくい [rs] [stderr]. 軽率に panic する [rs] [stderr].
というわけで enum_dispatch
は個人的にはおすすめしません. R.I.P. 6 7
閑話休題: proc macro で過激なことをやりたい
人間の欲求は底なしです. 「こうしたい」「ここでこれが使えればこれが作れるのに」「合法じゃないけど限定的に動作するものを作ってみた」 くらいならどんどん言ったりやったりしていくのが良いと思います.
今回のテーマである自動 delegation という文脈から少し外れますが, 「もっと一般に情報伝達したい」「状態を持ちたい」 という要望はちらほらと上がっています. 目に入ったものを挙げていきます.
- What do you suggest proc macros to be aware of each other as opposed to a static variable?
- Crate local state for procedural macros?
- Is it possible to store state within Rust's procedural macros?
- ちょっとオフトピだけど proc macro 中で $crate を使いたい ために Cargo.toml を読んでるやつ.
では自動 delegation は合法的には無理なのでしょうか? 実は合法的に実現する方法があります.
enum_delegate
v0.2.0
- crates.io
- repository
- doc
- Initial commit: 2022/10
enum_delegate は enum_dispatch を改善した crate (だったもの) です. しかし この PR でデザインが根本的に変わりました. なのでまず v0.2.0 までの話をします.
doc -- Comparison with enum_dispatch と README には以下の点が差分として挙げられています.
- enum_dispatch は enum のみサポートするが, enum_delegate は enum/struct with a field をサポートする.
- 外部 crate の trait をサポート
- より良いエラー (なんとちゃんと failure case のテスト がある!)
- Associated types サポート
使い方は enum_dispatch に近いです.
#[enum_delegate::register] // (2-4)
trait ShapeI {
fn area(&self) -> f64;
}
struct Rect {
width: f64,
height: f64,
}
#[enum_delegate::implement(ShapeI)] // (2-5)
enum Shape {
Rect(Rect),
}
impl ShapeI for Rect {
fn area(&self) -> f64 {
self.width * self.height
}
}
fn main() {
let rect = Rect { width: 2.0, height: 3.0 };
assert_eq!(rect.area(), 6.0);
let shape = Shape::Rect(rect);
assert_eq!(shape.area(), 6.0);
}
enum_delegate
の情報伝達方法
情報伝達の仕組みは How Does it Work.md で解説されています. 以下で解説します.
自動 delegation のためには
- trait
Trait
の path - trait
Trait
の定義 - struct/enum
StructEnum
の path - struct/enum
StructEnum
の定義
が必要です.
定義があっても path は得られないことに注意してください.
proc macro はコンパイルのかなり早いフェーズで展開されます. なので基本的に使えるのは字句情報 (TokenStream
) のみです.
あるトークンが型なのか trait なのか変数なのかや, 事前に use
されて別の module/crate を参照しているのかどうかなどはわかりません.
一方で impl path::to::Trait for path::of::StuctEnum { ... }
を生成するためには trait と struct/enum の path を
どこかしらから得る必要があります. enum_delegate::implement
の場合は trait の path は引数で与えられます. struct/enum の path は
この macro は struct/enum の定義に付けるという設計から ident StructEnum
をそのまま使えばよいです:
impl path::to::Trait for StuctEnum { ... }
.
残りは trait の定義のみです. enum_delegate は proc macro 中で decl macro を経由する ことで proc macro 間で情報を伝達します.
上記の例では以下が起こります:
- (2-4) の trait
ShapeI
に対する#[enum_delegate::register]
呼び出しにおいて, traitShapeI
の他に decralative macroShapeI
を定義する. (後述) - (2-5) の enum
Shape
に対する#[enum_delegate::implement(ShapeI)]
呼び出しにおいて, decl macroShapeI
を呼び出す [1]. ここで trait の定義を獲得する. - その中で proc macro
enum_delegate::implement_trait_for_enum!{}
を呼び出す. これがimpl ShapeI for Shape { ... }
を生成する [2] [3].
decl macro ShapeI
は
この様に定義 される:
quote! {
#[doc(hidden)]
#[macro_export]
macro_rules! #macro_name {
($trait_path: path, $enum_path: path, $enum_declaration: item) => {
enum_delegate::implement_trait_for_enum!{
$trait_path,
#parsed_trait,
$enum_path,
$enum_declaration
}
};
}
#[doc(hidden)]
pub use #macro_name as #trait_name;
#cleaned_trait
}
これは (trait の path, struct/enum の path, struct/enum の定義)
を引数に取り,
そこに trait の定義 parsed_trait
を合わせて enum_delegate::implement_trait_for_enum!{}
を呼び出しているだけである. (概ね)
何故動くのか?
これは global variable を使う方法とは違い, 合法である. 何故だろうか?
- 副作用がなく, 渡された
TokenStream
以外の情報を使っていない. - 順序不定:
#[enum_delegate::implement(ShapeI)]
が先に呼び出されたとしても内側の decl macroShapeI
の展開はその定義の方が先に解決される (はず 8). - 複数回呼び出し: 副作用がないので大丈夫.
- キャッシュ: decl macro の定義が変更されれば適切に invalidate されると期待できる.
では何故 enum_delegate
で不満なのか?
少なくとも generics [rs] [stderr], super trait [rs] [stderr] 9, trait item [rs] [stderr] がサポートされていない 10.
また, trait 定義が外部 crate にあり #[enum_delegate::register]
を付けられないということは普通にあります.
そのときは enum_delegate::implement
に直接 trait 定義を食わせます
[doc]
[example]:
mod external {
pub trait ShapeI {
fn area(&self) -> f64;
}
}
struct Rect {
width: f64,
height: f64,
}
#[enum_delegate::implement(
external::ShapeI,
trait ShapeI {
fn area(&self) -> f64;
}
)]
enum Shape {
Rect(Rect),
}
impl external::ShapeI for Rect {
fn area(&self) -> f64 {
self.width * self.height
}
}
これにはいくつか面倒なポイントがあります. 3章で再訪します.
enum_delegate
v0.2.0 まとめ
- enum_delegate は proc macro 中で decl macro を経由する ことで proc macro 間で情報を伝達する. これは問題がない 11.
- generics や trait bound のサポートが弱い.
- 外部 crate の trait の扱いが面倒.
ありがとう enum_delegate
, 勉強になったよ. でも別方向に行っちゃったんだよなぁ. R.I.P.
auto-delegate
- crates.io
- repository
- doc
- Initial commit: 2023/05
enum_delegate v0.3.0 の仕組みの解説の前に, より素朴な auto-delegate を取り上げます.
使い方:
#[auto_delegate::delegate] // (2-6)
trait ShapeI {
fn area(&self) -> f64;
}
struct Rect {
width: f64,
height: f64,
}
#[derive(auto_delegate::Delegate)] // (2-7)
#[to(ShapeI)]
enum Shape {
Rect(Rect),
}
impl ShapeI for Rect {
fn area(&self) -> f64 {
self.width * self.height
}
}
情報伝達の仕組み: impl Trait for auto_delegate::Delegates<...>
auto-delegate は auto_delegate::Delegates<...>
という struct を介して自動 delegation する.
#[doc(hidden)]
pub struct Delegates<A, B, C, D, E, F, G, H, I, J, K, L>(
pub Option<A>,
pub Option<B>,
pub Option<C>,
pub Option<D>,
pub Option<E>,
pub Option<F>,
pub Option<G>,
pub Option<H>,
pub Option<I>,
pub Option<J>,
pub Option<K>,
pub Option<L>,
);
- (2-6) で
#[auto_delegate::delegate]
するとimpl ShapeI for auto_delegate::Delegates<...>
が定義される. 例えば receiver が&self
の場合: [gen 1] [gen 2] [gen 3]. (どこかひとつのフィールドのみSome
であることが仮定されている.) - (2-7) で
#[derive(auto_delegate::Delegate)]
すると,impl ShapeI for Shape
の中で&self
などをauto_delegate::Delegates<variant, ...>
に変換し [enum] [struct], (2-6) の#[auto_delegate::delegate]
で定義されていたメソッドを呼びだす [gen 4]. 各 enum variant がauto_delegate::Delegates
のフィールドに対応する 12.
前のふたつとは異なり proc macro 間で TokenStream
を受け渡すのではなく,
rustc に型と関数呼び出しを解決させることにより trait と struct/enum を行き来している というのがキモである.
proc macro はその御膳立てをしているにすぎない.
この形式は rfcs#2329 のコメント (2018/05) で提案されています.
評価
この方法には致命的な欠点がみっつある.
ひとつめ. delegatee が crate local に定義されている型でなければ trait の実装がコンフリクトすること [rs] [stderr].
#[auto_delegate::delegate] pub trait Hello {...}
の時点で生成される中継となる定義の基本形は次です:
impl<DelegateImpl> Hello for DelegateImpl
where
DelegateImpl: auto_delegate::Delegatable<...>,
<DelegateImpl as auto_delegate::Delegatable<...>>::A: Hello,
...
<DelegateImpl as auto_delegate::Delegatable<...>>::L: Hello,
これと自分で定義した impl Hello for String
がコンフリクトします.
(将来的に auto_delegate が impl DelegateImpl for String
する可能性があるため rustc はこれを reject せざるを得ない.)
ふたつめ. 外部 crate の trait に対して後付けできない.
struct/enum が crate 内で定義されていたとしても, この仕組みでは impl external::Trait for auto_delegate::Delegates<...>
を経由せざるを得ません. trait external::Trait
に対して external
側では #[auto_delegate::delegate]
が付けられていない場合
(enum_delegate で trait 定義を直書きしているケース), この双方は non local であり特殊な場合を除き
orphan rule
に反します.
みっつめ. macro 展開結果がかなり読み難いということ [example].
これは単に読み難いというだけの問題ではない. 例えばエラーが出たときに (proc macro やこの仕組みを知っているとは限らない)
ユーザーが解決しやすいわかりやすいメッセージが出せないといった practical な問題を孕んでいる.
(例えば上のコンフリクトしているケース
[stderr]
でエラーメッセージからどうすれば解決できるのか, そもそも解決可能なのかがわかるだろうか?
もちろん cargo expand
して実装を読めばわかるが, それは最終手段である.)
あと致命的と言うべきかはわかりませんが, 全体的に柔軟性に欠けます. 例えば associated type は第一 variant のものを使っています [rs]. しかし associated type はともかく (enum variant 毎に違っているものを纏めたいケースを思いつかない) associated const は delegator 側で新しく定義するユースケースが出てくることを否定できません. 少なくとも外部 crate で trait が定義されていて associated const を持つケースにエスケープハッチを開けておく必要があります. (想定されているケースが素朴すぎる. 現場で使われるためには完璧なものかダクトテープで運用できるものが求められる.)
auto-delegate
まとめ
impl
がコンフリクトするケースがある.- 外部 crate の trait が扱えない.
- 生成されるコードが非自明.
- 柔軟性に欠ける.
仕組みの制約がきつすぎる. 黒魔術を使うならその原理をドキュメントに書いてほしい. R.I.P. 13
enum_delegate
v0.3.0
- crates.io
- repository
- doc: not yet released new version.
- commit: 2022/12 14
この issue でデザインが根本的に変更された.
- generics がサポートされている.
- super trait がサポートされていない [rs] [stderr] 15.
- associated type/const がサポートされていない [rs] [stderr] 16
仕組みと評価
auto-delegate
に似ているが, struct ではなく
enum_dispatch::Either<A, enum_dispatch::Either<B, ...enum_dispatch::Either<X, enum_dispatch::Void>>>
に変換する
[example].
auto-delegate とのこの違いにより, std types は
サポートされている.
impl Hello for enum_dispatch::Either<String, enum_dispatch::Void>
になるため.
(Hello
側は enum_dispatch
側に見えないので auto-delegate の様な問題は起こらない.)
外部 crate の trait が orphan rule にひっかかる点は同じ. (enum_dispatch::Either<A, ...>
は local ではないので.)
super trait は真面目にやればできるかも.
enum_delegate
v0.3.0 まとめ
auto-delegate と同じ. 読めないし制約がきつすぎる.
ambassador
- crates.io
- repository
- doc 18
- Initial commit: 2019/11 19
ambassador は enum_dispatch に継いで古くからある crate で, 機能的には最もカバー範囲が広いです. 一方で考えられる全てをカバーしようとしており構文的には複雑なものになっています.
ここでは全てを紹介することはせず, 簡潔にだけ纏めておきます. 詳細は README やドキュメントを参照してください.
- generics サポート [REDAME]
- trait bound サポート [README]
- 外部 crate の trait サポート
- 外部 crate の struct も扱える [README]
- trait のメソッドが receiver 以外の引数を取ってもよい
- 複数 field を持つ struct のサポート [README]
仕組み
基本的には enum_delegate と同じく, proc macro 中で decl macro を定義/利用しています
[in #[ambassador::derigatable_trait]
]
[in #[derive(ambassador::Delegate)
].
derive する際に enum_delegate は proc macro -> decl macro -> proc macro と処理をしていましたが,
ambassador は proc macro -> decl macro だけでコードを生成しています.
これを可能にするため, ambassador の decl macro は先にパーツ単位に加工しておき, 呼び出し側でそれらを別個に取り出す形になっています.
仕組みの評価
あんまり差がないので省略.
個人的には proc macro -> decl macro で処理している分煩雑になっていると思う. 4章でちらっと触れます.
ambassador
まとめ
- 仕組み: proc macro 中で decl macro を使う.
- 機能的にカバー範囲が広い.
たぶんおすすめです 20.
portrait
- crates.io
- repository
- doc
- Initial commit: 2023/01
portrait は enum をサポートしていませんが一応取り上げておきます.
#[portrait::make]
trait ShapeI {
fn area(&self) -> f64;
}
struct RectWrapper(Rect);
#[portrait::fill(portrait::delegate(Rect; self.0))]
impl ShapeI for RectWrapper {}
struct Rect {
width: f64,
height: f64,
}
impl ShapeI for Rect {
fn area(&self) -> f64 {
self.width * self.height
}
}
proc macro の中に proc macro を記述するという独特なスタイルです. portrait::delegate
の部分にはこれ以外も書くことができます
[doc].
また, attribute を struct/enum の定義ではなく impl Trait for Struct
に付ける というのも他にはない特徴です.
3章で議論します.
この形式は rfcs#2329 のコメント (2018/06) で提案されています.
仕組みと評価
情報伝達の仕組みは殆ど enum_delegate v0.2.0 と同じです. proc macro -> decl macro -> proc macro で処理をしています.
portrait
まとめ
- 仕組み: proc macro 中で decl macro を使う.
- delegation 以外もできる.
- enum をサポートしない.
- テストが少ない.
評価対象外 21.
delegate
- crates.io
- repository
- doc
- Initial commit: 2018/05
「Rust で delegation といったらこれ」くらいの認知度を誇る crate です. この記事でターゲットとしている自動 delegation ではないのですが, thin_delegate が大きく影響されているので取り上げます.
trait ShapeI {
fn area(&self) -> f64;
}
enum Shape {
Rect(Rect),
Circle { circle: Circle, center: (f64, f64) }
}
impl Shape { // or `impl ShapeI for Shape` for `area()`.
delegate::delegate! {
to match self {
Self::Rect(x) => x,
Self::Circle { circle, .. } => circle,
}
{
fn area(&self) -> f64;
}
}
fn center(&self) -> (f64, f64) {
match self {
Self::Rect(_) => unimplemented!(),
Self::Circle { center, .. } => center.clone(),
}
}
}
struct Rect {
width: f64,
height: f64,
}
impl ShapeI for Rect {
fn area(&self) -> f64 {
self.width * self.height
}
}
struct Circle {
radius: f64,
}
impl ShapeI for Circle {
fn area(&self) -> f64 {
3.14 * self.radius * self.radius
}
}
fn main() {
let rect = Rect { width: 2.0, height: 3.0 };
assert_eq!(rect.area(), 6.0);
let shape = Shape::Rect(rect);
assert_eq!(shape.area(), 6.0);
let circle = Circle { radius: 2.0 };
assert_eq!(circle.area(), 12.56);
let shape = Shape::Circle { circle, center: (0.0, 0.0) };
assert_eq!(shape.area(), 12.56);
assert_eq!(shape.center(), (0.0, 0.0));
}
見ての通りですが, delegate は柔軟性に特化しており, trait method の delegation に縛られません. その代わり, メソッドの列挙は手動で行う必要があります.
仕組みと評価
N/A
delegate
まとめ
たぶんおすすめです.
delegate-attr
- crates.io
- repository
- doc
- Initial commit: 2020/05
この記事でターゲットとしている自動 delegation ではないのですが目に入ったので一応取り上げておきます.
trait ShapeI {
fn area(&self) -> f64;
}
struct RectWrapper(Rect);
#[delegate_attr::delegate(self.0)]
impl RectWrapper {
fn area(&self) -> f64 {}
}
struct Rect {
width: f64,
height: f64,
}
impl ShapeI for Rect {
fn area(&self) -> f64 {
self.width * self.height
}
}
仕組みと評価
N/A
delegate-attr
まとめ
評価対象外 23.
2章まとめ
この章では proc macro 間での情報伝達と delegation crate 比較を行いました.
trait method の自動 delegation に使われている情報伝達方法は現状みっつです:
- proc macro 中の global variable を使う
- 合法ではない. 使うべきではない.
- proc macro 中で decl macro を使う
- 柔軟に対応可能. 他のケースにも応用が効きそう.
- 間に struct/enum/trait を挟む
- 制約が強い. テクニックとして知っておいて損はなさそう.
この観点からは, trait method に対しては ambassador, それ以外については delegate という使い分けになるでしょう 24 25.
3章 では thin_delegate の設計と実装の話をします.
「そうでなければ」というのは正確ではないが理解しやすさを優先してこの様な説明にした. (まぁ挙動として間違ってはいない (はず).) 正確な理解のためにはコードを追ってください.
例では 1 ファイルで説明していますが, trait と enum の定義を別ファイルに置くのは普通にやります. このとき proc macro の評価順序は proc macro 作者としては期待できないと思っておいた方が良いです. (僕のクソ雑理解では rustc のコンパイルは (特に proc macro 解決フェーズは) 決定的ではあるが, どのファイルがどの順序で読まれるかは普通意識しないため.) あくまで推測ですが「defer の仕組みがないとこの問題に対応できない」ということでこういう作りになっているのでしょう.
Rustonomicon にこのあたりのこと書いてないっぽいんですよねぇ... なのでここで言う「合法」というのは Rust 公式のワーディングではないし, 定義もない. この記事の中でのみ使い, 「なんか問題なく動きそう」くらいの意味である. ((ここに限らないけど) 誰か知ってたら教えてください.)
rustc/cargo を読んで確定させろという話ですが...
記事を書くにあたって調べ直していて crates.io を見て驚きました. 流石に ambassader の方がダウンロード数多いと思っていたので...
enum_dispatch に限らずこの章でおすすめしないと書いている全ての crate に言えることですが, あなたが管理しているアプリケーションで使う分には問題ない壊れ方だと思います. とはいえ現代では ambassader や thin_delegate といった合法な crate があり, API も似た感じなので乗り換えるのをおすすめします.
僕は rustc の挙動に詳しくないのでぼかしておく. このへん から始めていけばわかる気がする.
発見されていない. まぁでも多分問題ないでしょう.
(ちゃんとチェックしてないけど) impl Trait for auto_delegate::Delegates<...>
の if 分岐はコンパイラの最適化で消えることが期待されていると思う.
(ちゃんとやらないといけないので消えなくてもおかしくはないけど.)
でも enum_delegate v0.3.0 ではサポートされていない super trait がサポートされていたり, (ちょっと弄った感じ) generics/trait bound まわりで反例が作れないので頑張ってはいると思う.
auto-delegate の initial commit より前である. v0.3.0 リリースされてないけど... (リリースされてなくて困って 野良リリース してる人がおる.)
真面目にやればできるのかもしれないがこの仕組みの上であんまり頑張ってもな...
この PR
で not supported case のテストが削除されている. この仕組みは有望ではないと思っているので裏取りが面倒になってきた.
enum_delegate v0.3.0 の実装は斜め読みでほぼほぼ cargo expand
しか見ていない.
しかしそれ, 現時点で差がないことはわかるかもしれないけど継続的保証になってないのでは...?
docs.rs の方は 0.4.1 が最新だがリポジトリ側の tag は 0.3.5 が最新. ずれるけど許して. (最新を参照しないのはフェアじゃない感がある.)
説明の都合で順序が逆になって申し訳ないが, enum_delegate よりも ambassador の方が登場がだいぶ早い. (ambassodor, 機能も構文も複雑なんですよねぇ...)
実際のところは, 構文が好きじゃなくてあんまりまじめに秘孔を突く作業をしてないのでよくわからない... (先行研究はもっとちゃんと調べろというのははい...) まぁドキュメントやテストや issue を見ている限りでは信頼できると思います. (How it works は書いてほしかった.) 機能がこれで十分で構文が嫌いでなければ使うのは止めません.
syn::custom_keyword!
はここで知った. ありがとう.
こういうゴテゴテしたのは僕の好みではない. 覚えられない. (本当に必要?) まぁ rfcs#3530 のコメント で author の Kobzol が言っている様に, 色々事情はあるんでしょう. 僕はこの問題を2ヶ月くらいしか考えてないので「好みじゃない」以上のことは言えない.
delegate 使えばよくない? ambassador よりダウンロード数が多いのもよくわからないし...
rfcs#2329 のコメント (2021/03) でも ambassador と delegate が引き合いに出されている.
僕個人としては thin_delegate の方が好きなので, 「trait method については thin_delegate, それ以外については delegate」 になると思います. delegate crate を使いたくなるケースは今のところ思い浮かびませんが.