האנטומיה של פונקציה
הצורה הכללית של פונקציה ב-Zero:
fun name(param1: Type1, param2: Type2) -> ReturnType {
// גוף
return value
}
החלקים:
fun: מילת המפתח שפותחת פונקציה.name: שם הפונקציה.(param1: Type1, ...): רשימת הפרמטרים. לכל פרמטר יש טיפוס מפורש.-> ReturnType: טיפוס ההחזרה.{ ... }: הגוף, בלוק של פקודות.return value: יוצא מהפונקציה עםvalue.
דוגמה קטנה ומוחשית:
fun double(value: i32) -> i32 {
return value * 2
}
זו כל הפונקציה. היא מקבלת i32 אחד, מחזירה i32 אחר ולא מבצעת קלט/פלט. בתוך הגוף, value הוא קישור בסגנון let: אפשר להשתמש בו כמו בכל משתנה מקומי אחר.
קריאה לפונקציות
קריאות נראות בדיוק כמו שהייתם מצפים:
let result = double(21)
הארגומנט חייב להתאים לטיפוס הפרמטר. התוצאה נקשרת ל-result, שהקומפיילר מסיק שהוא i32 כי double מחזירה i32.
דוגמה מלאה שמחברת פונקציית עזר ו-main. לחצו על Run כדי לראות אותה בפעולה:
אתם אמורים לקבל math works\n ב-stdout.
pub ונראות
כברירת מחדל, פונקציה שהוצהרה בקובץ היא פרטית לאותו קובץ (או מודול: כללי הנראות מתהדקים ככל שפרויקטים גדלים). כדי לחשוף פונקציה מחוץ למודול שלה, הוסיפו לפניה pub:
pub fun greet() -> String {
return "hello\n"
}
בלי pub, קוד במודולים אחרים לא יכול לקרוא ל-greet. סביבת הריצה צריכה לקרוא ל-main מחוץ לכל מודול שהמשתמש הגדיר, ולכן main תמיד pub.
כדאי לאמץ את כלל ברירת המחדל הפרטית. סמנו רק את מה שאתם מתכוונים שיהיה ממשק; כל השאר נשאר בתוך המודול.
טיפוסי החזרה
כל פונקציה מצהירה על טיפוס ההחזרה שלה אחרי ->. טיפוסי החזרה נפוצים:
fun answer() -> i32 { return 42 }
fun ok() -> bool { return true }
fun label() -> String { return "ready\n" }
fun nothing() -> Void { }
Void הוא טיפוס ההחזרה של פונקציה שעושה את העבודה שלה דרך תופעות לוואי במקום לייצר ערך. פונקציית Void לא צריכה return מפורש: מספיק להגיע לסוף הגוף.
fun log(world: World, message: String) -> Void raises {
check world.out.write(message)
}
קריאות לפונקציה שמוותרות על הערך
אם פונקציה מחזירה ערך ולא אכפת לכם ממנו, עדיין צריך להכיר בערך המוחזר. הדרך המקובלת היא לקשור אותו עם let:
ignored הוא קישור ששאר הפונקציה אף פעם לא קוראת. המוסכמה להשתמש בשם ignored (או _) מסמנת שהוויתור מכוון. זה יותר חיכוך מאשר לזרוק ערך מוחזר בשקט, וזו בדיוק הכוונה: בשפה שבה סוכנים קוראים ומייצרים קוד, ערך שלא נקרא הוא לעיתים קרובות באג שכדאי לחשוף.
התפקיד של raises
פונקציה שעלולה להיכשל מצהירה על כך בחתימה. ראינו את זה ב-main:
pub fun main(world: World) -> Void raises {
check world.out.write("hello\n")
}
פסוקית raises יכולה להיות כללית (כל שגיאה) או ספציפית:
fun validate(ok: Bool) -> i32 raises { InvalidInput } {
if ok == false {
raise InvalidInput
}
return 42
}
raises { InvalidInput } פירושו "הפונקציה הזו יכולה להיכשל עם InvalidInput, ועם שום דבר אחר." הקוראים חייבים להשתמש ב-check (או בצורת טיפול מפורטת יותר) כדי להעביר את השגיאה הלאה או לטפל בה.
Raises ו-Check מעמיק בזה, כולל מה קורה עם כמה טיפוסי שגיאה ואיך check מתקשר עם פסוקית raises של הקורא.
פונקציות גנריות
כשרוצים שפונקציה תעבוד עם יותר מטיפוס אחד, מצהירים על פרמטרי טיפוס בסוגריים משולשים:
fun makePair<T, U>(left: T, right: U) -> Pair<T, U> {
return Pair { left: left, right: right }
}
T ו-U הם פרמטרי טיפוס: הקורא מחליט מה הם. הקריאה makePair(40, 2_u8) נותנת Pair<i32, u8>. ראו Generics לתמונה המלאה, כולל shapes גנריים ומגבלות.
איפה פונקציות גרות
בתוכנית קטנה כותבים פונקציות ישירות בקובץ ה-.0. בחבילה מפזרים פונקציות על פני קבצים תחת src/ והקומפיילר פותר עבורכם הפניות בין קבצים. היסודות נשארים אותו דבר, fun, פרמטרים, טיפוס החזרה, גוף, בלי קשר למקום הפיזי של הפונקציה.
הערות סגנון
כמה מוסכמות שתראו בדוגמאות הרשמיות:
- שמות פונקציות באותיות קטנות, עם מילים מחוברות (
makePair) או מופרדות ב-camelCase. הספרייה הסטנדרטית נוטה ל-camelCase. - ערך החזרה אחד לכל פונקציה. אם צריך להחזיר כמה דברים, בנו עבורם
shapeקטן: זה ברור יותר מהחזרת tuple של זוג של tuples. - פונקציות
Voidמבצעות רק קריאותcheck; פונקציות שמחשבות ערך נמנעות מקלט/פלט כשאפשר. ההפרדה הזו היא חלקית תרבותית וחלקית נאכפת: פונקציית חישוב טהורה לא מקבלתworldולכן ממש לא יכולה לבצע קלט/פלט.
הנקודה האחרונה שווה התעכבות. מכיוון שקלט/פלט נמצא מאחורי היכולת World ואת World מעבירים במפורש, החתימה של פונקציה אומרת לכם אם היא עשויה לבצע קלט/פלט. פונקציות שהחתימה שלהן לא מזכירה את World טהורות ביחס לעולם החיצון. זו תכונה שסוכנים (ובני אדם) יכולים לסמוך עליה בלי לקרוא את הגוף.
הבא בתור: If/Else
ראיתם את if מופיע בדרך אגב. המסמך הבא מכסה ביטויי if/else בפירוט, כולל איך הם מתקשרים עם קישורים ומה חסר בהם בכוונה (אין כפייה ל-truthy, אין אופרטור טרנרי).
שאלות נפוצות
איך מצהירים על פונקציה ב-Zero?
משתמשים ב-fun: fun name(param: Type) -> ReturnType { body }. הוסיפו pub לפני כדי שהפונקציה תהיה נראית מחוץ למודול שלה. הוסיפו raises אחרי טיפוס ההחזרה אם הפונקציה עלולה להיכשל. לדוגמה: pub fun double(value: i32) -> i32 { return value * 2 }.
מה עושה מילת המפתח pub?
pub?pub מסמנת הצהרה כציבורית: נראית לקוד שמחוץ למודול הנוכחי שלה. בלי pub, פונקציה פרטית לקובץ (או לחבילה) שבו היא הוצהרה. נקודת הכניסה המקובלת pub fun main חייבת להיות ציבורית כדי שסביבת הריצה תוכל למצוא אותה ולקרוא לה.
איך מחזירים ערך מפונקציה ב-Zero?
כותבים return value בתוך גוף הפונקציה. הביטוי חייב להתאים לטיפוס ההחזרה שהוצהר. פונקציה עם טיפוס החזרה Void לא מחזירה כלום ולא צריכה פקודת return מפורשת: מספיק להגיע לסוף הגוף.
האם פונקציות ב-Zero יכולות לקבל כמה פרמטרים?
כן. כותבים אותם בסוגריים מופרדים בפסיקים, כל אחד עם שם וטיפוס: fun add(a: i32, b: i32) -> i32 { return a + b }. כל פרמטר הוא קישור בסגנון let בתוך גוף הפונקציה. Zero דורשת טיפוסים מפורשים לפרמטרים: אין הסקת טיפוסים לפרמטרים בהצהרת פונקציה.
מה המשמעות של raises בחתימת פונקציה?
raises בחתימת פונקציה?raises מצהיר שהפונקציה עלולה להיכשל. raises לבדו מאפשר כל טיפוס שגיאה; raises { InvalidInput } מגביל אותו לשגיאה ספציפית בעלת שם. הקוראים חייבים להשתמש ב-check (או במבנה אחר לטיפול בכישלון) כדי להכיר באפשרות של כישלון: הם לא יכולים להתעלם ממנה בשקט.