Menu

Choice ו-Match ב-Zero: איחודים מתויגים והתאמת תבניות

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

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

איחודים מתויגים, בסגנון Zero

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

choice Result {
    ok:  i32,
    err: String,
}

ערך Result הוא או ok שנושא i32 או err שנושא String. אף פעם לא שניהם, ואף פעם לא אף אחד מהם. מערכת הטיפוסים מבטיחה את זה, ו-match הופך את הפעולה על הערך לנוחה.

זה אותו רעיון ששפות אחרות קוראות לו "tagged union", "sum type", "discriminated union" או "algebraic data type". ב-Zero כותבים אותו choice ושומרים על דקדוק קטן.

הצהרה על Choice

choice Name {
    variantA: PayloadTypeA,
    variantB: PayloadTypeB,
}

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

choice Token {
    word:   String,
    number: i32,
    eof:    Void,
}

Token.eof הוא וריאנט בלי מטען שימושי (טיפוס המטען שלו הוא Void), וזה שימושי למקרים שמסמנים סוף.

בניית ערך Choice

בונים ערך על ידי ציון הטיפוס, אחריו הוריאנט, ואז העברת המטען:

let success = Result.ok(42)
let failure = Result.err("validation failed")

טיפוס המטען חייב להתאים למטען שהוצהר לוריאנט. Result.ok("hello") יהיה שגיאת קומפילציה, כי ok מצפה ל-i32.

גם כאן הסקת טיפוסים עובדת. אם הביטוי שאחרי סימן השוויון קובע את הטיפוס במלואו, אפשר לכתוב let success = Result.ok(42) והטיפוס של הקישור יהיה Result. אפשר גם לציין את הטיפוס במפורש כשרוצים שהוא יהיה מתועד במקום הקישור:

let success: Result = Result.ok(42)

התאמה על Choice

match הוא הדרך לקרוא ערך choice. המבנה:

match value {
    .variantA => binding { /* הגוף כש-value הוא variantA, עם המטען ב-`binding` */ }
    .variantB => binding { /* הגוף כש-value הוא variantB */ }
}

דוגמה מלאה מהמאגר הרשמי של Zero. לחצו על Run כדי לראות את הזרוע .ok מופעלת:

קראו את ה-match מילולית: "בהתאם לוריאנט ש-result מחזיק, הרץ את הזרוע המתאימה וקשור את המטען לשם שנבחר." בזרוע .ok, value הוא המטען מטיפוס i32. בזרוע .err, message הוא המטען מטיפוס String. כל זרוע היא תחום נפרד; הקישור נראה רק בתוך הגוף שלו.

בדיקה ממצה

זה הרווח הגדול של match על פני שרשראות if/else if: הקומפיילר מוודא שלכל וריאנט יש זרוע. אם שכחתם את המקרה .err, לא תקבלו נפילה בזמן ריצה לענף ברירת מחדל, אלא שגיאת קומפילציה:

{
    "code": "MAT001",
    "message": "match is not exhaustive: missing variant 'err'",
    "line": 9
}

(קוד השגיאה הוא להמחשה; העיקרון הוא החוזה.)

הוסיפו וריאנט חדש ל-choice, למשל Result.timeout: Void, וכל match על Result בבסיס הקוד יהפוך לשגיאת קומפילציה עד שתטפלו במקרה החדש. זה פיצ'ר, לא מטלה: הקומפיילר אומר לכם בדיוק איפה המקרה החדש דורש תשומת לב.

כשלא צריך את המטען

אם המטען של וריאנט הוא Void או שפשוט לא אכפת לכם ממנו בזרוע הזו, אפשר להתעלם מהקישור, אבל עדיין צריך לכתוב את הזרוע כדי לעמוד בדרישת הבדיקה הממצה:

match token {
    .word   => w { /* השתמשו ב-w */ }
    .number => n { /* השתמשו ב-n */ }
    .eof    => _ { /* אין מה לקשור */ }
}

הכתיב המדויק ל"התעלמות מהמטען" עשוי להשתנות ב-Zero שלפני 1.0 (ייתכן שתראו _ או פשוט השמטה של הקישור). הנקודה הרעיונית, שלכל וריאנט יש זרוע עם מטען או בלעדיו, היא החלק היציב.

דפוסים נפוצים

טיפוס שגיאה בסגנון Result

זו בדיוק הדוגמה שהמאגר הרשמי משתמש בה:

choice Result {
    ok:  i32,
    err: String,
}

פונקציות שיכולות להצליח עם ערך או להיכשל עם הודעה מחזירות Result. הקוראים מבצעים התאמת תבניות כדי לחלץ את הערך או את ההודעה. מערכת raises/check של Zero מטפלת בהעברה (propagation) של פעולות שעלולות להיכשל; Result שימושי כשרוצים להחזיק ערך של הצלחה או כישלון כנתון.

טוקן של מנתח

choice Token {
    word:   String,
    number: i32,
    eof:    Void,
}

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

מכונת מצבים

choice State {
    waiting:    Void,
    processing: i32,
    done:       String,
}

processing נושא את מזהה המשימה הנוכחית; done נושא את התוצאה הסופית. כל מעבר הוא ערך State חדש, בלי שדות ניתנים לשינוי שמפוזרים בתוך shape.

Choice ו-Generics

choice יכול להיות גנרי בדיוק כמו shape:

choice Maybe<T> {
    some: T,
    none: Void,
}

Maybe<i32> הוא "מספר שלם אופציונלי". Maybe<String> הוא "מחרוזת אופציונלית". אותו דפוס מופיע בספרייה הסטנדרטית של Zero, והוא מתאים הרבה יותר מערך null מסמן: אין דרך לשכוח את המקרה .none ברגע שמבצעים match על הטיפוס.

מתי להשתמש ב-Choice, ב-Shape או ב-Enum

סיכום קצר מתוך shapes ו-enums:

  • Shape: רשומה עם כמה שדות, שכולם קיימים יחד.
  • Enum: אחת מ-N תוויות, בלי נתונים נוספים.
  • Choice: אחד מ-N וריאנטים, שכל אחד נושא מטען.

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

הבא בתור: היכולת World

choice ו-match מכסים את צד הנתונים של Zero. הפרק הבא עוסק באפקטים: איך תוכניות Zero מתקשרות עם העולם החיצון. הוא מתחיל ביכולת World, האובייקט ששולט על כל פעולת קלט/פלט.

שאלות נפוצות

מה זה choice ב-Zero?

choice הוא טיפוס האיחוד המתויג של Zero: ערך שהוא אחד מכמה וריאנטים בעלי שם, וכל וריאנט נושא טיפוס מטען משלו. דוגמה: choice Result { ok: i32, err: String }. ערך Result הוא או ok שנושא i32 או err שנושא String. בונים אותו עם Result.ok(42) או Result.err("bad").

איך match עובד ב-Zero?

match value { .variantA => binding { ...body } .variantB => binding { ...body } } מפצל לפי הוריאנט ש-value מחזיק. כל זרוע מתאימה וריאנט, נותנת שם לקישור של המטען ומריצה את הגוף שלה. הקומפיילר מוודא שכיסיתם כל וריאנט: הבדיקה הממצה היא היתרון המרכזי על פני if/else if.

איך בונים ערך choice?

בונים אותו על ידי ציון הטיפוס והוריאנט והעברת המטען: let r: Result = Result.ok(42) או let r = Result.err("validation failed"). טיפוס המטען חייב להתאים למטען שהוצהר לוריאנט: העברת טיפוס שגוי היא שגיאת קומפילציה.

מה ההבדל בין choice ל-enum?

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

למה עדיף match על פני if-else עבור choice?

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

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל