crate thin_delegate を書いた (2/4)

|

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

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:

  1. (2-1) の trait ShapeI に対する enum_dispatch::enum_dispatch 呼び出しにおいて, trait ShapeI の token stream を文字列化したものを HashMap TRAIT_DEFS に保存する [code] [TRAIT_DEFS]. このとき generics の情報も一部 HashMap のキーとして含める [code].
  2. (2-2) の enum Shape に対する enum_dispatch::enum_dispatch 呼び出しにおいて, enum Shape の token stream を文字列化したものを HashMap ENUM_DEFS に保存する [code] [ENUM_DEFS]. このとき generics の情報も一部 HashMap のキーとして含める [code].
  3. (2-1), (2-2) で引数として enum/trait 名が与えられていた場合 (上では (2-2)), その組が両方定義されていれば自動 delegation する. そうでなければ 1 defer する [code].
  4. (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 2022

Proc 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 という文脈から少し外れますが, 「もっと一般に情報伝達したい」「状態を持ちたい」 という要望はちらほらと上がっています. 目に入ったものを挙げていきます.

では自動 delegation は合法的には無理なのでしょうか? 実は合法的に実現する方法があります.

enum_delegate v0.2.0

enum_delegate は enum_dispatch を改善した crate (だったもの) です. しかし この PR でデザインが根本的に変わりました. なのでまず v0.2.0 までの話をします.

doc -- Comparison with enum_dispatchREADME には以下の点が差分として挙げられています.

  • 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 間で情報を伝達します.

上記の例では以下が起こります:

  1. (2-4) の trait ShapeI に対する #[enum_delegate::register] 呼び出しにおいて, trait ShapeI の他に decralative macro ShapeI を定義する. (後述)
  2. (2-5) の enum Shape に対する #[enum_delegate::implement(ShapeI)] 呼び出しにおいて, decl macro ShapeI を呼び出す [1]. ここで trait の定義を獲得する.
  3. その中で 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 macro ShapeI の展開はその定義の方が先に解決される (はず 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

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 する.

auto-delegate/src/lib.rs:

#[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>,
);
  1. (2-6) で #[auto_delegate::delegate] すると impl ShapeI for auto_delegate::Delegates<...> が定義される. 例えば receiver が &self の場合: [gen 1] [gen 2] [gen 3]. (どこかひとつのフィールドのみ Some であることが仮定されている.)
  2. (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

この 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 は真面目にやればできるかも.

最適化はされるらしい 17.

enum_delegate v0.3.0 まとめ

auto-delegate と同じ. 読めないし制約がきつすぎる.

ambassador

ambassador は enum_dispatch に継いで古くからある crate で, 機能的には最もカバー範囲が広いです. 一方で考えられる全てをカバーしようとしており構文的には複雑なものになっています.

ここでは全てを紹介することはせず, 簡潔にだけ纏めておきます. 詳細は README やドキュメントを参照してください.

  • generics サポート [REDAME]
  • trait bound サポート [README]
  • 外部 crate の trait サポート
    • 外部 crate 側で ambassador を使っている場合 [doc]
    • 外部 crate 側で ambassador を使っていない場合 [README]
  • 外部 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

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

「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 に縛られません. その代わり, メソッドの列挙は手動で行う必要があります.

また, 豊富な機能 があるようです 22.

仕組みと評価

N/A

delegate まとめ

たぶんおすすめです.

delegate-attr

この記事でターゲットとしている自動 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

「そうでなければ」というのは正確ではないが理解しやすさを優先してこの様な説明にした. (まぁ挙動として間違ってはいない (はず).) 正確な理解のためにはコードを追ってください.

2

例では 1 ファイルで説明していますが, trait と enum の定義を別ファイルに置くのは普通にやります. このとき proc macro の評価順序は proc macro 作者としては期待できないと思っておいた方が良いです. (僕のクソ雑理解では rustc のコンパイルは (特に proc macro 解決フェーズは) 決定的ではあるが, どのファイルがどの順序で読まれるかは普通意識しないため.) あくまで推測ですが「defer の仕組みがないとこの問題に対応できない」ということでこういう作りになっているのでしょう.

3

Rustonomicon にこのあたりのこと書いてないっぽいんですよねぇ... なのでここで言う「合法」というのは Rust 公式のワーディングではないし, 定義もない. この記事の中でのみ使い, 「なんか問題なく動きそう」くらいの意味である. ((ここに限らないけど) 誰か知ってたら教えてください.)

4

でもなんか期待した挙動にはならないんですよね... [3] [4 rs] [4 stdeerr].

5

rustc/cargo を読んで確定させろという話ですが...

6

記事を書くにあたって調べ直していて crates.io を見て驚きました. 流石に ambassader の方がダウンロード数多いと思っていたので...

7

enum_dispatch に限らずこの章でおすすめしないと書いている全ての crate に言えることですが, あなたが管理しているアプリケーションで使う分には問題ない壊れ方だと思います. とはいえ現代では ambassader や thin_delegate といった合法な crate があり, API も似た感じなので乗り換えるのをおすすめします.

8

僕は rustc の挙動に詳しくないのでぼかしておく. このへん から始めていけばわかる気がする.

9

enum_dispatch の最新版 (0.3.13) だとこれは扱えます [rs] [stderr].

10

trybuild を使って unsupported case を書いていてくれてマジでありがとう. 自分で書いて検証 せずに済んだ. 全人類見習うべきだよ.

11

発見されていない. まぁでも多分問題ないでしょう.

12

(ちゃんとチェックしてないけど) impl Trait for auto_delegate::Delegates<...> の if 分岐はコンパイラの最適化で消えることが期待されていると思う. (ちゃんとやらないといけないので消えなくてもおかしくはないけど.)

13

でも enum_delegate v0.3.0 ではサポートされていない super trait がサポートされていたり, (ちょっと弄った感じ) generics/trait bound まわりで反例が作れないので頑張ってはいると思う.

14

auto-delegate の initial commit より前である. v0.3.0 リリースされてないけど... (リリースされてなくて困って 野良リリース してる人がおる.)

15

真面目にやればできるのかもしれないがこの仕組みの上であんまり頑張ってもな...

16

この PR で not supported case のテストが削除されている. この仕組みは有望ではないと思っているので裏取りが面倒になってきた. enum_delegate v0.3.0 の実装は斜め読みでほぼほぼ cargo expand しか見ていない.

17

しかしそれ, 現時点で差がないことはわかるかもしれないけど継続的保証になってないのでは...?

18

docs.rs の方は 0.4.1 が最新だがリポジトリ側の tag は 0.3.5 が最新. ずれるけど許して. (最新を参照しないのはフェアじゃない感がある.)

19

説明の都合で順序が逆になって申し訳ないが, enum_delegate よりも ambassador の方が登場がだいぶ早い. (ambassodor, 機能も構文も複雑なんですよねぇ...)

20

実際のところは, 構文が好きじゃなくてあんまりまじめに秘孔を突く作業をしてないのでよくわからない... (先行研究はもっとちゃんと調べろというのははい...) まぁドキュメントやテストや issue を見ている限りでは信頼できると思います. (How it works は書いてほしかった.) 機能がこれで十分で構文が嫌いでなければ使うのは止めません.

21

syn::custom_keyword! はここで知った. ありがとう.

22

こういうゴテゴテしたのは僕の好みではない. 覚えられない. (本当に必要?) まぁ rfcs#3530 のコメント で author の Kobzol が言っている様に, 色々事情はあるんでしょう. 僕はこの問題を2ヶ月くらいしか考えてないので「好みじゃない」以上のことは言えない.

23

delegate 使えばよくない? ambassador よりダウンロード数が多いのもよくわからないし...

24

rfcs#2329 のコメント (2021/03) でも ambassador と delegate が引き合いに出されている.

25

僕個人としては thin_delegate の方が好きなので, 「trait method については thin_delegate, それ以外については delegate」 になると思います. delegate crate を使いたくなるケースは今のところ思い浮かびませんが.