Menu

היכולת World ב-Zero: קלט/פלט מפורש בלי משתנים גלובליים

ב-Zero אין stdout גלובלי, אין מערכת קבצים סביבתית ואין רשת מרומזת. כל מה שנוגע בעולם החיצון עובר דרך יכולת World שמועברת ל-main. כאן תמצאו למה ואיך.

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

הרעיון במשפט אחד

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

ההרשאה הזו היא ערך מטיפוס World. סביבת הריצה בונה ערך כזה לפני הקריאה ל-main, והתוכנית שלכם מעבירה אותו (או חלקים ממנו) לכל מקום שבו היא צריכה לתקשר עם העולם החיצון.

למה אין משתנים גלובליים?

רוב השפות מאפשרות לכל פונקציה, בכל מקום, לכתוב ל-stdout או לפתוח קובץ. ב-JavaScript יש console.log. ב-Python יש print. ב-C יש printf. הנוחות אמיתית, אבל גם המחיר: אי אפשר לדעת מהחתימה של פונקציה אם היא עשויה לבצע קלט/פלט. כדי לדעת, צריך לקרוא את הגוף, ברקורסיה.

Zero נוקטת עמדה אחרת. אין print גלובלי. אין os.Stdout סביבתי. אם הפונקציה שלכם מבצעת קלט/פלט, העובדה הזו חייבת להופיע בחתימה שלה, כי הדרך היחידה לבצע קלט/פלט היא לקבל יכולת.

היתרונות מופיעים בשלושה מקומות:

  1. קריאת חתימה אומרת לכם מה פונקציה יכולה לעשות. פונקציה שלא מזכירה את World לא יכולה לכתוב ל-stdout, לא יכולה לפתוח socket, לא יכולה לקרוא קובץ. מערכת הטיפוסים הופכת את זה להבטחה מוחלטת.
  2. בדיקת קוד טהור היא עניין פשוט. פונקציות טהורות לא צריכות print מדומה או מערכות קבצים מדומות: מלכתחילה אין להן גישה אליהם.
  3. סוכנים יכולים לחשוב באופן מקומי. סוכן AI שמייצר או מתקן קוד Zero יכול לדעת, בלי לקרוא את כל בסיס הקוד, אם לפונקציה שהוא מסתכל עליה יש אפקטים.

השימוש הקנוני

כבר ראיתם את הצורה הבסיסית ב-hello-world:

שלושה דברים קורים כאן:

  • main מצהירה על הפרמטר שלה בתור world: World. סביבת הריצה מעבירה לתוכנית ערך World וקושרת אותו כאן.
  • world.out הוא זרם הפלט הסטנדרטי, שחשוף כשדה של היכולת World.
  • world.out.write(...) כותב מחרוזת. הוא מחזיר ערך שעלול להיכשל (הכתיבה עלולה להיכשל), ו-check מעביר את הכישלון הלאה.

אפשר גם לשנות את שם הפרמטר, w: World או io: World, אבל world היא המוסכמה, וכדאי לשמור עליה לטובת עקביות עם האקוסיסטם הרחב של Zero.

מה יש ב-World

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

  • world.out: פלט סטנדרטי.
  • world.err: שגיאה סטנדרטית.
  • world.in: קלט סטנדרטי.
  • דרך לפתוח קבצים, לקרוא משתני סביבה ולהתחבר דרך הרשת.

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

העברת יכולות בקוד

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

fun log(world: World, message: String) -> Void raises {
    check world.out.write(message)
}

pub fun main(world: World) -> Void raises {
    log(world, "starting\n")
    log(world, "done\n")
}

log מקבלת את world כדי שתוכל לכתוב דרכו. אם log לא הייתה מקבלת את world, הגוף לא היה יכול לקרוא ל-world.out.write: הקישור פשוט לא היה קיים.

זו העברת פרמטרים רבה יותר מאשר בשפה עם קלט/פלט סביבתי. בתמורה, כל גרף הקריאות של main גלוי עכשיו מהחתימות בלבד:

  • main מקבלת את world, ולכן היא עשויה לבצע קלט/פלט.
  • log מקבלת את world, ולכן היא עשויה לבצע קלט/פלט.
  • כל פונקציה בלי world בחתימה שלה לא יכולה.

יכולות צרות יותר

להעביר את כל ה-World לכל פונקציה זו גישה גסה: זה כמו לחלק הרשאות root. הדפוס ש-Zero מעודדת הוא לקחת רק את החלק של World שבאמת צריך:

fun log(out: Stream, message: String) -> Void raises {
    check out.write(message)
}

pub fun main(world: World) -> Void raises {
    log(world.out, "starting\n")
    log(world.out, "done\n")
}

עכשיו log מקבלת גישה רק ל-Stream (אותו טיפוס ש-world.out חושף). היא יכולה לכתוב דרכו, אבל לא יכולה לפתוח קובץ או לקרוא מהרשת. הקורא בחר מה log רשאית לעשות.

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

פונקציות טהורות

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

fun sum(point: Point) -> i32 {
    return point.x + point.y
}

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

יכולות ו-raises

כמעט כל פעולה על יכולת עלולה להיכשל. world.out.write עלול להיכשל כי הזרם סגור. פתיחת קובץ עלולה להיכשל כי הקובץ לא קיים. משטח ה-API של היכולות משולב עם raises ו-check: פעולות שעלולות להיכשל מצהירות על דרכי הכישלון שלהן בחתימות, והקוראים מכירים בהן עם check.

השילוב הזה הוא הלב של סיפור האפקטים של Zero:

  • מה יכול לקרות → raises { ... }.
  • דרך מה → World (או חלק ממנו).
  • איפה → בכל מקום שבו היכולת ו-raises גלויים.

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

הערה על בדיקות

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

הספרייה הסטנדרטית יכולה לספק כלי בדיקה שבונים יכולות מזויפות או כאלה שפועלות בזיכרון בדיוק למטרה הזו. ה-API המדויק יתפתח עם השפה; העיקרון (יכולות הן ערכים שאפשר להחליף) הוא המנוף.

הבא בתור: Raises ו-Check

World הוא חצי מסיפור האפקטים: המשטחים שפונקציה יכולה לגעת בהם. החצי השני הוא כישלון: כשמשהו משתבש, איך זה מועבר הלאה? זה מוסבר בהמשך ב-Raises ו-Check.

שאלות נפוצות

מה זה World ב-Zero?

World הוא אובייקט היכולת שסביבת הריצה מספקת, והוא מעניק לתוכנית Zero גישה לעולם החיצון: stdout, stdin, קבצים, הרשת, משתני סביבה וכן הלאה. סביבת הריצה בונה ערך World ומעבירה אותו ל-main. לפונקציות שצריכות לבצע קלט/פלט צריך להעביר את ה-World (או חלק צר יותר ממנו): אין פתח מילוט גלובלי.

למה main מקבלת פרמטר World?

ב-Zero אין משתנים גלובליים סביבתיים. אין מקבילה ל-printf, ל-console.log או ל-os.Stdout שכל פונקציה יכולה לקרוא לה בלי הרשאה. סביבת הריצה מעבירה ל-main יכולת World, ו-main (וכל פונקציה שהיא קוראת לה) יכולה לבצע קלט/פלט רק דרך הערך הזה. כך כל אפקט גלוי בחתימה של פונקציה.

במה קלט/פלט מבוסס יכולות שונה מקלט/פלט רגיל?

ברוב השפות קלט/פלט הוא מרומז: כל פונקציה יכולה לכתוב ל-stdout או לקרוא ממערכת הקבצים בכל רגע. קלט/פלט מבוסס יכולות הופך את ההרשאה לבצע קלט/פלט לערך: צריך לקבל World (או חלק ממנו) כדי להשתמש בו. פונקציות חישוב טהורות לא מקבלות World ולכן ממש לא יכולות לבצע קלט/פלט, ומערכת הטיפוסים אוכפת את זה.

האם אפשר לקבל את ה-World באופן מרומז איפשהו עמוק במחסנית הקריאות?

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

מה עושה world.out.write?

world.out.write("text\n") כותב את המחרוזת שהועברה לזרם הפלט הסטנדרטי של התוכנית דרך היכולת שסביבת הריצה סיפקה. הוא מחזיר ערך שעלול להיכשל, כי הכתיבה עלולה להיכשל, ולכן עוטפים את הקריאה ב-check כדי להעביר את השגיאה למעלה במחסנית הקריאות.

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

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

להתחיל