לדלג לתוכן

2.10 - יצירת קוד ומאקרואים הניתנים לבדיקה - הרצאה

יוצרים קוד כשפונקציה אינה מספיקה

לפני שכותבים מאקרו כדאי לשאול מה חסר בפונקציה רגילה. אם אנחנו רק מחשבים ערך מתוך ערכים אחרים, פונקציה גנרית בדרך כלל מספיקה וקלה יותר לדיבוג. מאקרו מועיל כאשר צריך לעבוד עם מבנה תחבירי, ליצור פריטים או לתמוך בצורה שאינה רק רשימת פרמטרים רגילה.

נתחיל במאקרו הצהרתי שמקבל ביטויים ובונה Vec. הדוגמה לימודית, משום שהספרייה כבר מספקת את vec:

macro_rules! collect_values {
    ($($value:expr),* $(,)?) => {{
        let mut result = ::std::vec::Vec::new();
        $(result.push($value);)*
        result
    }};
}

fn main() {
    let mut calls = 0;
    let result = collect_values![
        { calls += 1; 10 },
        { calls += 1; 20 },
    ];
    assert_eq!(result, [10, 20]);
    assert_eq!(calls, 2);
}

החלק expr מתאר איזה סוג תחביר מתקבל. החזרה מסומנת באמצעות $() יחד עם הכוכבית, והחלק האחרון מאפשר פסיק מסיים. בגוף ההרחבה כל ביטוי מופיע פעם אחת, ולכן תופעת הלוואי של כל ארגומנט מתבצעת פעם אחת. מאקרו שמשתמש פעמיים באותו ביטוי יכול לשנות התנהגות גם אם ההרחבה נראית תמימה.

תחום פנימי ומניעת התנגשויות שמות

הסוגריים המסולסלים הכפולים יוצרים בלוק שמחזיר ערך. המשתנה result שייך להרחבה, ולא אמור לשכתב משתנה בשם זהה אצל הקורא. מנגנון ההיגיינה של macro_rules מסייע לשמור את ההפרדה בין שמות שנוצרו בתוך ההרחבה לשמות שמגיעים מאתר הקריאה.

עם זאת, היגיינה אינה מבטיחה שכל נתיב ייפתר כפי שהתכוונו. במאקרו שמיוצא מספרייה צריך להשתמש בנתיבים שמתאימים למקום שבו ההרחבה תיבדק. כאשר רוצים להפנות לפריט ציבורי של ה־crate המגדיר, $crate הוא כלי חשוב. העתקת שם crate קשיח לתוך ההרחבה עלולה להישבר כאשר הלקוח משנה את שם התלות.

מאקרו פרוצדורלי מקבל tokens

מאקרו derive אינו מקבל מופע של האובייקט בזמן ריצה. הוא מקבל תיאור תחבירי בזמן הבנייה ומחזיר קוד נוסף. כדי לראות את המנגנון בלי ספריית ניתוח חיצונית, נכתוב derive מוגבל שמקבל רק unit struct פרטי ללא פרמטרים ומוסיף מתודה עם שם הטיפוס.

צרו שתי חבילות שכנות באמצעות הפקודות הבאות:

cargo new --lib type_name_derive
cargo new derive_demo

הוסיפו לקובץ type_name_derive/Cargo.toml את הסעיף הבא:

[lib]
proc-macro = true

החליפו את type_name_derive/src/lib.rs:

use proc_macro::{TokenStream, TokenTree};

#[proc_macro_derive(TypeName)]
pub fn type_name(input: TokenStream) -> TokenStream {
    let tokens: Vec<_> = input.into_iter().collect();
    let name = match tokens.as_slice() {
        [TokenTree::Ident(keyword), TokenTree::Ident(name), TokenTree::Punct(end)]
            if keyword.to_string() == "struct" && end.as_char() == ';' => name.to_string(),
        _ => return "compile_error!(\"TypeName expects a private unit struct without generics\");"
            .parse().expect("fixed diagnostic is valid"),
    };
    format!("impl {name} {{ pub fn type_name() -> &'static str {{ \"{name}\" }} }}")
        .parse().expect("generated unit struct implementation is valid")
}

הקוד מצומצם בכוונה. הוא אינו מנתח כל תחביר חוקי של מבנה, אינו מטפל בשדות ואינו מנסה להבין פרמטרים גנריים. במקרה שאינו נתמך הוא מפיק compile_error, במקום לייצר מימוש שנראה תקין אבל אינו מתאים לקלט.

הוסיפו תחת dependencies בקובץ derive_demo/Cargo.toml:

type_name_derive = { path = "../type_name_derive" }

החליפו את derive_demo/src/main.rs:

use type_name_derive::TypeName;

#[derive(TypeName)]
struct Worker;

fn main() {
    assert_eq!(Worker::type_name(), "Worker");
    println!("{}", Worker::type_name());
}

הריצו cargo run מתוך derive_demo. אחרי הניסוי הוסיפו שדה ל־Worker וראו שהבנייה נדחית לפי המדיניות המוגדרת שלנו. אין להציג את המאקרו הזה כפתרון כללי ל־derive; הוא מדגים את הגבול בין קלט תחבירי, ניתוח, יצירת קוד ואבחון.

שגיאה טובה היא חלק מן המוצר

במאקרו אמיתי חשוב לשייך שגיאה למקום המתאים בקלט. מידע ה־span של tokens מאפשר להצביע על שדה או תכונה בעייתיים. הודעה שמצביעה תמיד על כל שורת ה־derive מקשה על המשתמש להבין מה לתקן, במיוחד כאשר הקוד שנוצר גדול.

גם בדיקות של מאקרו צריכות לכלול קלט חוקי וקלט שאמור להידחות. בדיקה שההרחבה מתקמפלת אינה מוכיחה שהקוד המופק שומר על חוזי בעלות, סדר הערכה או ביצועים. במקרה של מאקרו שמייצר unsafe, חובות ההוכחה קיימות בדיוק כמו בקוד ידני.

אפשר לבדוק את הממשק באמצעות חבילת לקוח קטנה עם שמות משתנים מתנגשים, נתיבים שונים וארגומנטים בעלי תופעות לוואי. כלי הרחבה יכולים להראות את הקוד שנוצר, אך אין לבסס את ההסבר רק על צילום פלט: צריך לחבר כל חלק בהרחבה להבטחה של המאקרו.

בוחרים את ההפשטה הקטנה שמספיקה

ה־derive נוח משום שהוא חוסך קוד שחוזר על עצמו לפי צורת טיפוס. הוא אינו תחליף לתכנון trait נכון או להגדרת חוזה ציבורי. אם כל לקוח מקבל מאות שורות שלא ברור כיצד הן פועלות, החיסכון בהקלדה עלול להפוך למחיר אבחון ותחזוקה.

בפרויקט הפרק נשתמש במאקרו רק במקום שבו יש תבנית תחבירית קטנה ומוגדרת. שאר המערכת תישען על פונקציות ותכונות רגילות, כדי שאפשר יהיה לבדוק את האלגוריתם בלי להבין תחילה מנגנון ליצירת קוד.