[Rust 筆記] 淺聊泛型常量參數 Const Generic

引題

最近有網友私信我討論:若使用規則宏編譯時統計token tree序列的長度,如何繞開由宏遞歸自身侷限性造成的:

的問題。然後,就貼出瞭如下的一段例程代碼 1:

fn main() {
    macro_rules! count_tts {
        ($_a:tt $($tail: tt)*) => { 1_usize + count_tts!($($tail)*) };
        () => { 0_usize };
    }
    assert_eq!(10, count_tts!(,,,,,,,,,,));
}

嚯!這段短小精悍的代碼餒餒地演示了Incremental TT Muncher設計模式的精髓。贊!

首先,宏遞歸深度是有極限的(默認是128層)。所以,若每次遞歸僅新統計一個token,那麼被統計序列的最大長度自然不能超過128。否則,突破上限,編譯失敗!

其次,尾遞歸優化運行時壓縮函數調用棧的技術手段,卻做不到編譯時抑制調用棧的膨脹。所以,巧用#![recursion_limit="…"]元屬性強制調高宏遞歸深度上限很可能會導致編譯器棧溢出。

由此,如果僅追求快速繞過問題,那最經濟實惠的作法是:在每次宏遞歸期間,統計幾個token 例程 2(而不是一次一個)。從算數上,將總遞歸次數降下來,和使計數更長的token tree序列成爲可能。

fn main() { // 這代碼看着就“傻乎乎的”。
    macro_rules! count_tts {
        ($_a: tt $_b: tt $_c: tt $_d: tt $_e: tt $_f: tt // 一次遞歸統計 6 個。
         $($tail: tt)*) => { 6_usize + count_tts!($($tail)*) };
        ($_a: tt $_b: tt $_c: // 一次遞歸統計 3 個。
         tt $($tail: tt)*) => { 3_usize + count_tts!($($tail)*) };
        ($_a: tt // 一次遞歸統計 1 個。
         $($tail: tt)*) => { 1_usize + count_tts!($($tail)*) };
        () => { 0_usize };  // 結束了,統計完成
    }
    println!("token tree 個數是 {}", count_tts!(,,,,,,,,,,));
}

倘若要標本兼治地解決問題,將遞歸調用變形成循環結構纔是正途,因爲循環本身不會增加調用棧的深度。這涵蓋了:

  1. 宏循環結構token tree序列變形成數組字面量

  2. 常量函數調用觸發編譯器對數組字面量的類型推導

  3. 因爲rust數組在編譯時明確大小,所以數組長度被編入了數據類型定義內。

  4. 泛型常量參數從數據類型定義中提取出數組長度值,並作爲序列長度返回。

全套操作被統稱爲Array length設計模式。它帶入了兩個技術難點:

  1. 如何觸發rustc對數組字面量的類型推導,和從推導結果中提取出數組長度信息。

  2. 如何撇開遞歸的 “吐吞模式”(即,吐Incremental TT Muncher和吞Push-down Accumulation),僅憑宏循環結構,將token tree序列變形成爲數組字面量。

第一個難點源於自rustc 1.51才穩定的新語言特性 “泛型常量參數Const Generic”。而第二個難點的解決就多樣化了

接下來,它們會被逐一地講解分析。

泛型常量參數

rustc 1.51+起,【泛型常量參數 】允許泛型項(類或函數)接受常量值或常量表達式爲泛型參數。根據泛型常量參數出現的位置不同(請見下圖例程 3),它又細分爲

下文分別將它們簡稱爲 “泛型常量形參” 與 “泛型常量實參”。

泛型參數的分類

於是,已知的泛型參數就包含有三種類型:

泛型常量參數的數據類型

可用作【泛型常量參數】的數據類型包括兩類:

泛型常量參數的 “怪癖”

首先,就 “同名衝突” 而言,若【泛型常量形參】與【類型】同名並作爲另一個泛型項的泛型參數實參,那麼rustc會優先將該泛型參數當作類型帶入程序上下文。多數情況下,這會造成程序編譯失敗。解決方案是使用表達式{...}包裝泛型常量參數,以向rustc標註此同名參數是泛型常量參數而不是類型名 例程 4。

其次,就 “聲明和使用” 而言,泛型常量參允許僅被聲明,而不被使用。對另兩種泛型參數而言,這卻會導致編譯失敗例程 5。

最後,泛型常量參的trait實現不會因爲窮舉了全部備選形參值而自動過渡給泛型常量參。如下例程 6(左),即便泛型項struct Foo顯示地給泛型常量B每個可能的(參)值true / false都實現的同一個trait Bar,編譯器也不會 “聰明地” 歸納出該trait Bar已經被此泛型項的泛型常量參充分實現了,因爲編譯器可不會 “歸納法” 方法論(不確定chatGPT是否能做到?)。相反,每個參上的trait實現都被視作不相關的個例。正確地作法是:泛型項必須明確地給泛型常量參實現trait例程 7(右)。

泛型常量參數的適用位置

泛型常量參數原則上可出現於常量項適用的全部位置,包括但不限於:

上述列表內的#1 ~ #5,可在下面例程 8 源碼內找到對應的代碼行。

use rand::{thread_rng, Rng};
fn main() {
    fn foo1<const N1: usize>(input: usize) { // 在泛型函數內,泛型常量參數的形參可用於
        let sum = 1 + N1 * input;   // #1 運行時求值的表達式
        let foo = Foo([input; N1]); // #5 結構體字段的值
        let arr: [usize; N1] = [input; N1]; // #4 綁定變量的數據類型 —— 編譯時參數化數組長度
                                            // #5 綁定變量的值
        println!("運行時表達式:{sum},\n\
                  元組結構體:  {foo:?},\n\
                  數組:       {arr:?}");
    }
    trait Trait<const N2: usize> {
        const CONST: usize = N2 + 4; // #2 關聯常量 + 常量表達式
        type Output;
    }
    #[derive(Debug)]
    struct Foo<const N3: usize>(
        [usize; N3] // #4 結構體字段的數據類型 —— 編譯時參數化數組長度
    );
    impl<const N4: usize> Trait<N4> for Foo<N4> {
        type Output = [usize; N4]; // #3 關聯類型 —— 編譯時參數化數組長度
    }
    let mut rng = thread_rng();
    foo1::<2>(rng.gen_range::<usize, _>(1..10));
    foo1::<{1 + 2}>(rng.gen_range::<usize, _>(1..10));
    const K: usize = 3;
    foo1::<K>(rng.gen_range::<usize, _>(1..10));
    foo1::<{K * 2}>(rng.gen_range::<usize, _>(1..10));
}

泛型常量參數的不適用位置

首先,泛型常量參不能:

上述列表內的#1 ~ #4,可在下面例程 9 源碼內找到對應的代碼行。

fn main() {
    fn outer<const N: usize>(input: usize) {
        // 泛型常量參數【不】可用於函數體內的
        // #1 常量定義
        //     - 既不能定義類型
        const BAD_CONST: [usize; N] = [1; N];
        //     - 既不能定義值
        const BAD_CONST: usize = 1 + N;
        // #1 靜態變量定義
        //     - 既不能定義類型
        static BAD_STATIC: [usize; N] = [N + 1; N];
        //     - 既不能定義值
        static BAD_STATIC: usize = 1 + N;
        fn inner(bad_arg: [usize; N]) {
            // #2 在子函數內不能引用外層函數聲明的
            //    泛型常量形參,無論是將其作爲
            //    變量類型,還是常量值。
            let bad_value = N * 2;
        }
        // #3 結構體內也不能引用外層函數聲明的
        //    泛型常量形參。
        struct BadStruct([usize; N]);
        //    相反,需要給結構體重新聲明泛型常量參數
        struct BadStruct<const N: usize>([usize; N]);
        // #4 類型別名內不能引用外層函數聲明的
        //    泛型常量形參。
        type BadAlias = [usize; N];
        //    相反,需要給類型別名重新聲明泛型常量參數
        type BadAlias<const N: usize> = [usize; N];
    }
}

其次,泛型常量接受包含了泛型常量參的常量表達式例程 10。

但是,泛型常量參並不拒絕接受

數組重複表達式與泛型常量參數

數組重複表達式[repeat_operand; length_operand]是數組字面量的一種形式。在數組重複表達式中,泛型常量形參

回到序列計數問題

類似於解析幾何中的 “投影” 方法,通過將高維物體(token tree序列)投影於低維平面(數組),以主動捨棄若干信息項(每個token的具體值與數據類型)爲代價,突出該物體更有價值的信息內容(序列長度),便可降低從複雜結構中摘取特定關注信息項的合計複雜度。這套 “降維算法” 帶來的啓發就是:

  1. 既然讀取數組長度是簡單的,那爲什麼不先將token tree序列變形爲數組呢?

  2. 答:投影token tree序列爲數組

  3. 既然token tree序列的內容細節不被關注,那爲什麼還要糾結於數組的數據類型與填充值呢?全部充滿unit type豈不快哉!

  4. 再答:投影token tree序列爲單位數組[(); N]。僅數組長度對我們有價值。

於是,循環替換設計模式Repetition Replacement(RR)與元變量表達式${ignore(識別符名)}都是被用來改善【宏循環結構】的使用體驗,以允許Rustacean對循環結構中的循環重複項 “宣而不用” —— 既遍歷token tree序列,同時又棄掉每個具體的token元素,最後還生成一個等長的單位數組[(); N]。否則,未被使用的 “循環重複項” 會導致error: attempted to repeat an expression containing no syntax variables matched as repeating at this depth的編譯錯誤。

然後,常量函數調用和函數參觸發編譯器對單位數組字面量的類型推導

接着,泛型常量參從被推導出的數據類型定義內提取出數組長度信息。

最後,將泛型常量參作爲常量函數的返回值輸出。

上圖,一圖抵千詞。

結束語

除了前文提及的【宏遞歸法】與Array Length設計模式,統計token tree序列長度還有

【規則宏】與【泛型參數】皆是rust編程語言提供的業務功能開發利器。宏循環結構與泛型常量參數僅只是它們的冰山一角。此文既彙總分享與網友的討論成果,也對此話題拋磚引玉。希望有機會與路過的神仙哥哥和仙女妹妹們更深入地交流相關技術知識點與實踐經驗。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ezr2C0naV4tMAAJrvXGr0g