ביטויים, ליטרלים קבועים וששת הפסאודו-משתנים
התחביר המינימליסטי של Smalltalk: הצהרת משתנים מקומיים ומשותפים, השמות, ליטרלים קבועים, מערכים סטטיים מול דינמיים, Cascade (נקודה-פסיק), וששת הפסאודו-משתנים השמורים בשפה.
מודל מנטלי ועיקרון מנחה: תחביר מינימליסטי שבו כל אבן בניין היא עצם חי
אם שפות כמו Java או ++C הן מפעל עם עשרות מכונות ייעודיות ולוחות בקרה סבוכים, הרי ש-Smalltalk היא ערכת לגו אלגנטית שבה כל החלקים מתחברים בדיוק באותו מנגנון: עצמים המקבלים הודעות.
כל ביטויי השפה מורכבים מחמישה אלמנטים תחביריים בלבד: 6 פסאודו-משתנים, ליטרלים קבועים, משתנים והשמות, בלוקים ושליחת הודעות, והחזרת ערך ממתודה.
האות הראשונה של שם משתנה קובעת את תחום ההכרה שלו (אות קטנה למשתנה מקומי, אות גדולה למשתנה משותף), ורק ששת הפסאודו-משתנים נקבעים אוטומטית ע"י זמן הריצה ולעולם אינם ניתנים להשמה!
1. סעיף 6.1: ממה מורכבים ביטויים (Expressions) ב-Smalltalk?
חמשת היסודות שמרכיבים כל שורת קוד בשפה, משתנים מקומיים מול משותפים, ופעולת ההשמה
אני שמח שאנחנו צוללים יחד אל תוך התחביר של שפת Smalltalk. היופי הגדול של השפה הזו הוא המינימליזם שלה: התחביר שלה קצר, ממוקד ומתבסס כמעט כולו על רעיון אחד – שליחת הודעות לעצמים. למעשה, כל ביטוי שתכתוב אי-פעם ב-Smalltalk מורכב מחמישה אלמנטים תחביריים בלבד:
- 6 פסאודו-משתנים:
true,false,nil,self,super,thisContext. - ביטויים קבועים (ליטרלים): מספרים, תווים, מחרוזות, סימבולים ומערכים.
- הצהרת משתנים, השמות ומשתנים מקומיים.
- בלוקים ושליחת הודעות.
- החזרת ערך ממתודה.
בוא נפתח במשתנים ובהשמות. ב-Smalltalk יש חלוקה ברורה ואלגנטית בין שני סוגי משתנים, והאות הראשונה של שם המשתנה מספרת לנו מיד מי הוא:
🔹 משתנים מקומיים (אות ראשונה קטנה)
כוללים משתני מופע, פרמטרים של מתודות ובלוקים, ומשתנים זמניים. לפי המוסכמה, שמות המשתנים המקומיים מתחילים תמיד באות קטנה (כגון x או y). אם השם מורכב מיותר ממילה אחת, הוא יכיל אות גדולה בראש כל מילה החל מהמילה השנייה (CamelCase, למשל savedLink או nextValue).
משתנים זמניים מוצהרים בין שני קווים אנכיים: למשל | x y | מצהיר על שני משתנים מקומיים זמניים.
🔷 משתנים משותפים (אות ראשונה גדולה)
כוללים משתנים גלובליים, משתני מחלקה, pool dictionaries ושמות של מחלקות. לפי המוסכמה, שמות משתנים אלו מתחילים תמיד באות גדולה, וכל מילה נוספת תתחיל גם היא באות גדולה (כגון Object, SmallInteger, OrderedCollection).
⬅️ השמות (Assignments)
השמות למשתנים מתבצעות באמצעות הסימן := או באמצעות חץ שמאלה ← (אשר לעיתים מופיע כקו תחתון _ עקב מגבלות קידוד). לדוגמה: הביטוי x := 1 מבצע השמה של המספר 1 למשתנה x.
🎯 מקבל ההודעה (self)
מתייחס תמיד אל העצם שהמתודה שלו הופעלה כעת. זהו פסאודו-משתנה שמקבל את ערכו באופן אוטומטי על ידי מערכת זמן הריצה, ולא ניתן לבצע לו השמה (אסור לכתוב self := ...).
2. ליטרלים קבועים: מספרים, תווים, מחרוזות, סימבולים ומערכים (סטטיים מול דינמיים)
איך מייצגים עצמים קבועים בקוד ומה ההבדל הקריטי בין מערך של זמן הידור למערך של זמן ריצה?
כשאנחנו כותבים ערך קבוע בקוד (ליטרל), ב-Smalltalk אנחנו בעצם יוצרים מופע של מחלקה מתאימה. בוא נכיר את כל ששת סוגי הליטרלים הקבועים ואת הכללים המדויקים שלהם:
54 | 2r101 | 54.7 | 2.4e7
מספרים שלמים נכתבים בצורה הרגילה (54). ניתן לכתוב מספרים בבסיסים שונים עם האות r (Radix): למשל 2r101 מייצג את המספר 5 בבסיס 2! מספרים ממשיים מוצגים כרגיל (54.7) או עם חזקה: 2.4e7 השווה ל-$2.4 imes 10^7$.
$a | $b | Character tab
נרשמים באמצעות סימן דולר $ לפני התו, למשל $a המייצג את התו 'a'. מופעים של תווים שאינם דפיסים (כמו טאב או רווח) ניתן ליצור באמצעות שליחת הודעות למחלקה Character, למשל ההודעה Character tab.
'Hello' | ' ''Donald '
מוקפות במרכאות בודדות, למשל 'Hello'. כדי לכלול מרכאה בודדת בתוך המחרוזת עצמה, יש לרשום אותה פעמיים ברצף, למשל ' ''Donald ' (שלעיתים נראה בדפוס כ-' "Donald ').
#Hello
מתחילים בסימן סולמית # מלווה באוסף תווים, למשל #Hello. הם דומים למחרוזות, אך ההבדל הקריטי הוא שסימבולים הם ייחודיים במערכת – מובטח שקיים רק מופע אחד ויחיד (Singleton per symbol) עבור כל סימבול!
#(1 'ab' $a)
מכילים אלמנטים שערכם ידוע בזמן הידור (Compile-time) (מספרים, תווים, מחרוזות, סימבולים ותת-מערכים סטטיים) המופרדים ברווחים בתוך #( ). אין צורך בסימן # פנימי עבור תת-מערכים סטטיים: #(10 #(100 100)) זהה ל-#(10 (100 100)), וביטויים בתוך מערך סטטי אינם מחושבים אלא נספרים כאלמנטים מילוליים (למשל מערך שטוח #(10 100 100) או כתיבת אסימונים מופרדים ברווח ללא חישוב).
{ 1 . 2 . 1+2 }
נרשמים בתוך סוגריים מסולסלים {} ומכילים ביטויים המוערכים בזמן ריצה (Run-time) ומופרדים בנקודות (.)! לדוגמה, { 1 . 2 . 1+2 } מכיל 3 אלמנטים שהם 1, 2 וערך הביטוי (3). כלל קריטי: חובה להשאיר רווח לאחר הנקודה, אחרת המערכת תפרש זאת כמספר עשרוני (כגון 1.2)!
3. מבנים נוספים, סדרת משפטים ומנגנון ה-Cascade (נקודה-פסיק)
הערות, בלוקים, חזרה ממתודה, הפרדת משפטים בנקודה, והסוד של שליחת מספר הודעות לאותו מקבל ב-Cascade
• הערות: נרשמות בתוך מרכאות כפולות, כגון "comment", ויכולות להשתרע על פני מספר שורות.
• בלוקים: נרשמים בתוך סוגריים מרובעים [...]. אלו עצמים המייצגים פונקציות חסרות שם שיכולות לקבל ארגומנטים ולהכיל משתנים זמניים.
^) וסדרת משפטים (.)
• חזרה ממתודה: מתבצעת באמצעות חץ כלפי מעלה (^) מלווה בעצם המוחזר. אם מתודה מסתיימת ללא ציון החזרה מפורש, היא מחזירה אוטומטית את self.
• סדרת משפטים: ביטויים המופרדים בנקודה (.). לדוגמה: n := 4. n := n factorial. 'hello' size.
🔗 Cascade (נקודה-פסיק ;): שליחת סדרת הודעות לאותו המקבל!
בניגוד לשפות כמו C או Java שבהן נקודה-פסיק מציינת סוף משפט, ב-Smalltalk סוף משפט מצוין בנקודה (.), ואילו נקודה-פסיק (;) היא אופרטור ה-Cascade!
משמעותו של Cascade היא סדרת הודעות המופרדות בנקודה-פסיק ונשלחות כולן ברצף לאותו המקבל המקורי (המקבל נרשם רק פעם אחת בתחילת הביטוי).
לדוגמה, בביטוי 3 + 4 ; + 5:
1. תחילה נשלחת ההודעה + 4 אל העצם המקורי 3 (ומחזירה 7, אך ערך ביניים זה אינו הופך למקבל הבא!).
2. לאחר מכן, בגלל ה-;, ההודעה + 5 נשלחת שוב אל העצם המקורי 3 (ולא אל 7!).
3. הערך המוחזר מהביטוי כולו הוא תמיד תוצאת ההודעה האחרונה בשרשרת, ולכן התוצאה היא 8 (ולא 12)!
4. סעיף 6.2: ששת הפסאודו-משתנים (Pseudo-variables)
למה הם נקראים "פסאודו-משתנים", מי קובע את ערכם, ומה ההבדל המדויק בין self ל-super?
השפה מגדירה מראש 6 פסאודו-משתנים. מדוע אנחנו קוראים להם "פסאודו-משתנים" (משתנים מדומים)? משום שערכם נקבע באופן אוטומטי על ידי מערכת זמן הריצה, והמתכנת אינו יכול לשלוט בערכם — לא ניתן לבצע אליהם השמה לעולם!
מופעים יחידים (Singletons) של המחלקות הבוליאניות True ו-False בהתאמה. שים לב לאות הקטנה לעומת שם המחלקה באות גדולה!
זהו המופע היחיד (Singleton) של המחלקה UndefinedObject. כל המשתנים במערכת מאותחלים ל-nil כברירת מחדל לפני שבוצעה אליהם השמה.
שניהם מתייחסים לאותו עצם בדיוק — העצם שקיבל את ההודעה (המקבל של המתודה המתבצעת)! ההבדל הוא שכאשר שולחים הודעה ל-super, איתור המתודה (Method Lookup) משתנה ומתחיל ממחלקת-העל של המחלקה המכילה את המתודה שבה נעשה שימוש ב-super.
מתייחס למסגרת העליונה (Top Frame) של מחסנית זמן הריצה, כלומר מייצג כעצם חי את הקשר הביצוע הנוכחי של המתודה או הבלוק המתבצעים כעת!
כיצד הביטוי 3 + 4 ; + 5 מחזיר 8 (ולא 12), ומה ההבדל בין #(...) ל-{...}?
מיני-סימולציה 6.1: חוקר ששת הפסאודו-משתנים והליטרלים הקבועים
מפענח התחביר החי (Syntax Parser): מערכים סטטיים מול דינמיים, השמות ו-Cascade
בחר ביטוי Smalltalk כדי לראות כיצד המהדר ומערכת זמן הריצה מפרקים כל אסימון (Token), אילו עצמים נוצרים בזיכרון, ומהו הערך המוחזר:
הגשר הנרטיבי: הכרנו את העצמים, המשתנים והליטרלים – עכשיו איך שולחים להם הודעות ובאיזה סדר הן מוערכות?
ראינו שכל ביטוי ב-Smalltalk בנוי מעצמים וממשתנים. אבל הקסם האמיתי מתרחש כשאנחנו מתחילים לשלוח הודעות!
בלשונית הבאה (סעיף 6.3) נכיר את שלוש תבניות ההודעות (אונרית, בינרית ומילות-מפתח), נבין מדוע הביטוי 3 + 4 * 5 מחזיר ב-Smalltalk דווקא 35 ולא 23, ונבנה יחד סימולטור שפותח שלב-אחר-שלב ביטויים מורכבים כמו 2 raisedTo: 1+3 factorial!
שליחת הודעות: אונריות, בינריות ומילות-מפתח, סדר קדימויות ו-Cascade
שלושת סוגי ההודעות בשפה, חוקי הקדימות המוחלטים (סוגריים ← אונרית ← בינרית ← מילות-מפתח), הערכה משמאל לימין ללא קדימות מתמטית מובנית, שרשור Cascade וזיהוי הודעות מילות-מפתח מרובות.
מודל מנטלי ועיקרון מנחה: פירמידת הקדימויות (אונרי ← בינרי ← מילות-מפתח) והערכה משמאל לימין
במקום טבלת קדימויות של 15 רמות אופרטורים כמו ב-++C, ב-Smalltalk יש רק 3 הילוכים של הודעות: הילוך ראשון ומהיר ביותר להודעות אונריות (ללא ארגומנטים), הילוך שני לבינריות (תווים מיוחדים), והילוך שלישי למילות-מפתח (עם נקודתיים).
אין קדימות לכפל על פני חיבור! מכיוון שגם + וגם * הן הודעות בינריות בעלות אותה דרגת קדימות בדיוק, הן מוערכות תמיד משמאל לימין: 3 + 4 * 5 = 35 (וכדי לקבל 23 חובה לכתוב 3 + (4 * 5)).
כל מילות המפתח המופיעות ברצף ללא סוגריים מתחברות להודעה אחת גדולה (word1:word2:word3:). כדי לשלוח הודעת מילת-מפתח פנימית כארגומנט להודעה חיצונית, חובה לתחום אותה בסוגריים!
1. שלוש תבניות ההודעות ב-Smalltalk
איך מבחינים במבט אחד בין הודעה אונרית, בינרית והודעת מילות-מפתח?
כל תכנות ב-Smalltalk מבוסס על שליחת הודעות. כל בקשת שליחת הודעה מורכבת תמיד משני חלקים: מקבל ההודעה (Receiver) שמופיע ראשון (ושיכול להיות ליטרל בודד, משתנה, או תוצאה של הערכת ביטוי קודם), ואחריו ההודעה עצמה. בשפה קיימים בדיוק שלושה סוגי הודעות:
1. הודעה אונרית (Unary)
מורכבת ממילה אחת בלבד וללא ארגומנטים (וללא נקודתיים). מקובל להתחיל אותה באות קטנה.
התחביר: receiver selector.
'hello' size "➔ 5"
2. הודעה בינרית (Binary)
מורכבת מתו אחד או יותר מתוך האוסף המוגדר:
+ - * / = > | < & ~ @ % ? ,.
מקבלת תמיד ארגומנט אחד בדיוק!
התחביר: receiver selector argument.
10 @ 20 "➔ Point(10,20)"
3. מילות-מפתח (Keyword)
מורכבת ממילת מפתח אחת או יותר המסתיימות בנקודתיים (:). כל מילת מפתח מלווה בארגומנט.
התחביר: receiver word1: arg1 word2: arg2.
2 raisedTo: 7 "➔ 128"
2. דרגת קדימות וסדר הערכה של ביטויים מורכבים
ארבעת כללי הברזל של סדר ההערכה והמלכודת המתמטית של אופרטורים בינריים
כאשר אנחנו כותבים ביטוי מורכב הכולל מספר הודעות, ההודעות מוערכות תמיד משמאל לימין, בהתאם לסדר הקדימויות הבא (מהגבוה לנמוך):
- סוגריים עגולים
( )– משנים את סדר ההערכה ומתבצעים תמיד ראשונים. - הודעות אונריות (Unary) – דרגת הקדימות הגבוהה ביותר מבין ההודעות.
- הודעות בינריות (Binary) – דרגת קדימות שנייה (כולן שוות זו לזו ומוערכות משמאל לימין!).
- הודעות מילות-מפתח (Keyword) – דרגת הקדימות הנמוכה ביותר.
🔍 דוגמה 1: ניתוח 2 raisedTo: 1+3 factorial
- שלב 1 (אונרית): ההודעה האונרית
factorialנשלחת לעצם3ומחזירה 6. - שלב 2 (בינרית): ההודעה הבינרית
+נשלחת לעצם1עם הארגומנט6ומחזירה 7. - שלב 3 (מילת-מפתח): הודעת מילות-המפתח
raisedTo:נשלחת לעצם2עם הארגומנט7, ומחזירה 128 ($2^7$). - הוספת סוגריים תשנה את התוצאה: בביטוי
(2 raisedTo: 1) + 3 factorialהסוגריים מחשבים תחילה2 raisedTo: 1(שמחזיר 2), האונרית3 factorialמחזירה 6, והחיבור2 + 6מחזיר 8!
⚠️ הערה קריטית: מלכודת 3 + 4 * 5
סדר הקדימות של אופרטורים מתמטיים ב-Smalltalk הוא משמאל לימין בהיותם הודעות בינריות זהות בעדיפותן, ולא על פי כללי המתמטיקה המקובלים!
• לכן, הביטוי 3 + 4 * 5 יוערך תחילה כ-(3 + 4) שמחזיר 7, ואז 7 * 5 שיחזיר את העצם 35!
• כדי לקבל את התוצאה המתמטית 23, חובה להשתמש בסוגריים: 3 + (4 * 5).
3. הרכבת הודעות מורכבות (Cascade מול סדרת ביטויים) וזיהוי הודעות מילות-מפתח
איך פועלים ביטויי Cascade מתקדמים ואיך שוברים רצף של מילות-מפתח בעזרת סוגריים?
🔗 ניתוח שרשורי Cascade מול סדרת ביטויים
• דוגמה א' (2 raisedTo: 2; * 3 factorial): תחילה ההודעה raisedTo: 2 נשלחת ל-2 (ומחזירה 4). לאחר מכן ההודעה האונרית factorial נשלחת ל-3 (ומחזירה 6). לבסוף, בגלל ה-Cascade (;), ההודעה * 6 נשלחת אל המקבל המקורי של ה-Cascade (שהוא 2, ולא 4!), ולכן המערכת מחזירה 12!
• דוגמה ב' (10@10 - 10 ; corner: 30@30 ; isZero): ההודעה הבינרית @ יוצרת את הנקודה 10@10 שהיא המקבל של שלוש ההודעות הבאות: הראשונה - 10 מחזירה 0@0; השנייה corner: 30@30 נשלחת שוב ל-10@10; והשלישית isZero נשלחת שוב ל-10@10 ומחזירה false (התוצאה הסופית!).
• לעומת זאת, סדרת ביטויים בנקודה (.): מעריכה כל משפט בנפרד. בביטוי 2 raisedTo: 2+2. 2*3 factorial raisedTo: 2. הביטוי הראשון מחזיר 16 ($2^4$), והביטוי השני מחשב 3 factorial (6), כופל 2 * 6 (12), ומעלה בריבוע 12 raisedTo: 2 שמחזיר 144!
🧩 זיהוי הודעות מילות-מפתח ושבירה בסוגריים
הודעת מילת-מפתח מורכבת מצירוף כל מילות המפתח ברצף:
• הביטוי x word1: arg1 word2: arg2 word3: arg3 שולח הודעה אחת שלמה בשם word1:word2:word3: אל העצם x.
• כדי לשבור את הרצף ולשלוח הודעות שונות זו אחר זו, משתמשים בסוגריים: (x word1: arg1 word2: arg2) word3: arg3 ישלח תחילה את word1:word2: אל x, ואז ישלח את word3: אל התוצאה שהוחזרה!
• דוגמה מעשית חיונית: בביטוי st at: (st indexOf: $e) put: $x הסוגריים מפעילים תחילה את ההודעה indexOf: $e על המחרוזת st, ואת המיקום שהוחזר מעבירים כארגומנט למילת המפתח at: בתוך ההודעה הגדולה at:put:! (ללא סוגריים המהדר היה מחפש מתודה לא קיימת בשם at:indexOf:put:!).
פירוק שלבי ההערכה של 2 raisedTo: 1 + 3 factorial (אונרי ← בינרי ← מילת-מפתח)
מיני-סימולציה 6.2: מפענח הודעות מילות-מפתח וסוגריים
מנתח סדר הערכה (Evaluation Visualizer): צעד-אחר-צעד בצבעים!
בחר ביטוי והתקדם שלב-אחר-שלב כדי לראות כיצד המערכת מבודדת תחילה את ההודעות האונריות (בירוק), לאחר מכן את ההודעות הבינריות (בכחול), ולבסוף את הודעות מילות-המפתח (באדום):
הגשר הנרטיבי: הבנו איך שולחים הודעות – אבל איפה מוגדרות המתודות שמגיבות להן, ומהם בלוקים של קוד?
כשעצם מקבל הודעה (כמו add: link after: otherLink), מופעלת המתודה המתאימה במחלקה שלו.
בלשונית הבאה (סעיפים 6.4–6.5) נלמד את ארבעת חלקי המתודה וננתח שורה-אחר-שורה מתודה אמיתית מתוך רשימה מקושרת (LinkedList). מיד לאחר מכן נכיר את אחד הכלים החזקים ביותר ב-Smalltalk: בלוקים ([...]) — פונקציות אנונימיות שאפשר לארוז כעצמים, להעביר להן פרמטרים, ללכוד בהן משתנים מהסביבה החיצונית (Closure), ואפילו לצאת דרכן מהמתודה כולה בעזרת ^!
מתודות (Methods) ובלוקים של קוד (Blocks & Closures)
ארבעת חלקי המתודה, ניתוח מלא של המתודה add:after: במחלקה LinkedList, בלוקים כמופעים של BlockContext, פרמטרים ומשתנים מקומיים בבלוק, הכרה סביבתית (Closure) ויציאה מלאה ממתודה (^).
מודל מנטלי ועיקרון מנחה: הקפאת קוד בתוך עצם (Block) והכוח של Non-Local Return
מתודה היא מתכון קבוע שרשום בספר המתכונים של המחלקה; בלוק ([...]) הוא "פתק הוראות נייד" שארוז כעצם עצמאי — אפשר למסור אותו לעצם אחר, והוא יופעל רק כשישלחו לו את ההודעה value!
הבדל קריטי בערך ההחזרה: מתודה ללא ^ מחזירה אוטומטית את self. בלוק ללא ^ מחזיר את תוצאת הביטוי האחרון בבלוק. ואם בלוק כן מכיל ^ — הוא מבצע יציאה מלאה מהמתודה העוטפת כולה!
בלוק ב-Smalltalk נהנה מהכרה סביבתית (Closure): הוא יכול לגשת למשתנים שהוגדרו מחוץ לבלוק בהקשר שעוטף אותו, בנוסף לפרמטרים ולמשתנים המקומיים שלו עצמו.
1. סעיף 6.4: מבנה המתודה ב-Smalltalk וניתוח add: link after: otherLink
ארבעת החלקים הקבועים של כל מתודה וניתוח מעשי של חיבור חוליה ברשימה מקושרת (LinkedList)
מתודות משויכות למחלקות ומוגדרות לרוב בעזרת ה-System Browser. התחביר להגדרת מתודה ב-Smalltalk בנוי מארבעה חלקים עיקריים וברורים:
בוא ננתח יחד דוגמה אמיתית וחשובה מתוך המחלקה LinkedList (רשימה מקושרת): המתודה add: link after: otherLink, שתפקידה לחבר צומת חדש (link) מיד אחרי צומת קיים (otherLink) ברשימה:
2. סעיף 6.5: בלוקים (Blocks) – פונקציות אנונימיות, פרמטרים, Closure ויציאה ממתודה
איך בלוקים שומרים קוד להערכה נדחית, איך מעבירים להם ארגומנטים, ומה קורה כשכותבים ^ בתוך בלוק?
מהו בלוק? בלוק הוא סדרה של ביטויים הכלואים בתוך סוגריים מרובעים [...].
למעשה, בלוק הוא עצם לכל דבר (מופע של המחלקה BlockContext או BlockClosure) המייצג פונקציה אנונימית (חסרת שם). הקוד שבתוך הבלוק אינו מתבצע בעת הגדרת הבלוק, אלא מוערך אך ורק כאשר שולחים לבלוק את ההודעה value!
לדוגמה, שליחת ההודעה value לבלוק [2 raisedTo: 1+3 factorial] value תחשב את הביטוי שבתוכו ותחזיר את הערך 128.
🚨 הבחנה קריטית: סיום רגיל של בלוק לעומת חזרה מפורשת (^) בתוך בלוק!
• סיום רגיל (ללא ^): אם הבלוק מגיע לסיומו הרגיל, הוא מחזיר לשולח ההודעה value את ערכו של הביטוי האחרון בבלוק, והמתודה החיצונית ממשיכה לרוץ כרגיל.
• חזרה מפורשת עם ^ בתוך בלוק (Non-local return): אם הבלוק מכיל הוראת חזרה מפורשת באמצעות הסימן ^, ביצוע הבלוק יסתיים מיד ותתבצע יציאה מלאה מהמתודה כולה שבה נכתב הבלוק! (בדיוק כפי שראינו בשורה otherLink == lastLink ifTrue: [^ self addLast: link] במתודה של LinkedList!).
📥 פרמטרים של בלוק (value: ועד valueWithArguments:)
פרמטרים נרשמים בתחילת הבלוק, כשנקודתיים (:) מקדימות כל שם פרמטר, וקו אנכי (|) מפריד בינם לבין גוף הבלוק:
• פרמטר אחד: [:x | 2 raisedTo: x+3 factorial] value: 2 מחזיר 256 ($2^{2+6} = 2^8$).
• שני פרמטרים: משתמשים בהודעה כפולה value:value:, לדוגמה: [:x :y | x raisedTo: 1+y factorial] value: 3 value: 2 מחזיר 27 ($3^{1+2} = 3^3$).
• מעל 4 פרמטרים: ניתן להעביר עד 4 פרמטרים עם משפחת value:.... עבור מעל 4 פרמטרים, משתמשים בהודעה valueWithArguments: המקבלת מערך של ערכים!
🔒 משתנים מקומיים בבלוק והכרה סביבתית (Closure)
• משתנים מקומיים בבלוק: נרשמים בין קווים אנכיים |...| מיד לאחר הפרמטרים (אם ישנם). לדוגמה:
[:x :y | | n | n := x raisedTo: y. n := n + (y raisedTo: x). ] value: 2 value: 3
מחשב $n := 2^3 = 8$, מוסיף $3^2 = 9$, ומחזיר 17!
• הכרה סביבתית (Closure): בלוקים יכולים לגשת למשתנים שהוגדרו מחוץ לבלוק בהקשר שעוטף אותם! לדוגמה, אם נגדיר בחוץ y := 2, אזי הבלוק [:x | x raisedTo: y] value: 5 ניגש ל-y החיצוני ומחזיר 25 ($5^2$).
ניתוח המצביעים ב-add: link after: otherLink ומבנה הבלוק
מיני-סימולציה 6.3: סימולטור שורה-אחר-שורה של המתודה add: link after: otherLink
מעבדת הבלוקים וטווח ההכרה (Block Scope, Closure & Non-Local Return)
בחר תרחיש הרצה של בלוק ב-Smalltalk כדי לראות את מסגרת זמן הריצה של הבלוק, את הפרמטרים והמשתנים המקומיים שלו, את הגישה למשתנים חיצוניים (Closure), ואת ההבדל הדרמטי בין סיום בלוק רגיל לבין ^ בתוך בלוק:
הגשר הנרטיבי: עכשיו כשאנחנו יודעים לארוז קוד בתוך בלוקים – איך מממשים תנאים ולולאות בלי מילות המפתח if, for ו-while?
בשפות רגילות, if ו-while הם פקודות תחביריות קשיחות של המהדר. אבל ב-Smalltalk יש לנו בלוקים שמאפשרים דחיית ביצוע (Deferred Evaluation)!
בלשונית הבאה (סעיפים 6.6.1–6.6.2) נראה את אחד הרעיונות היפים ביותר במדעי המחשב: איך תנאים ממומשים כפולימורפיזם טהור בין המחלקות True ו-False, מה ההבדל בין אופרטורים לוגיים עצלים (and: / or:) למלאים (& / |), ואיך לולאות whileTrue:, timesRepeat: ו-to:do: בנויות כולן משליחת הודעות רגילה!
תנאים פולימורפיים (Conditionals) ולולאות בסיסיות (Loops)
היעדר מילות מפתח תחביריות ב-Smalltalk, מימוש פולימורפי של ifTrue:ifFalse: במחלקות True ו-False, אופרטורים לוגיים עצלים מול מלאים, ולולאות whileTrue:, timesRepeat: ו-to:do:.
מודל מנטלי ועיקרון מנחה: פולימורפיזם טהור ודחיית ביצוע במקום מילות מפתח שמורות
במקום שוטר תנועה חיצוני (פקודת if של השפה) שבודק האם הרמזור ירוק או אדום, ב-Smalltalk העצמים true ו-false הם שני פקידים חכמים: true מפעיל תמיד רק את המעטפה הראשונה (trueAlternativeBlock value), ו-false מפעיל תמיד רק את השנייה!
בלולאת whileTrue:, המקבל חייב להיות בלוק ללא פרמטרים ([ n < 10 ]) ולא ערך בוליאני מחושב, כדי שהתנאי יוערך מחדש (self value) בכל איטרציה!
הבנת ההבדל בין סוגריים לאינטרוולים: 1 to: 10 do: [...] שולח הודעה אחת to:do: למספר 1, ואילו (1 to: 10) do: [...] יוצר תחילה עצם מסוג Interval ושולח אליו את הודעת האוסף do:!
1. סעיף 6.6.1: תנאים (Conditionals) – פולימורפיזם טהור בין True ו-False
איך מבצעים משפט תנאי ללא מילת מפתח if, מה מוחזר במקרה של אי-התאמה, ואופרטורים לוגיים עצלים מול מלאים
בשפות תכנות סטנדרטיות (כמו Java או C) קיימות מילות מפתח שמורות עבור מבני בקרה (if, else, while, for). בניגוד מוחלט אליהן, בשפת Smalltalk אין שום תחביר ייעודי או מילות מפתח עבור תנאים ולולאות!
כל ביצוע של משפט תנאי נעשה אך ורק באמצעות שליחת הודעות לעצמים בוליאניים, וכל ביצוע של לולאה נעשה באמצעות שליחת הודעות לבלוקים, למספרים שלמים או לאוספים. הודעות אלו מקבלות בלוקים של קוד כארגומנטים, וכך מיושמת דחיית הביצוע (Deferred Evaluation) עד לבדיקת התנאים!
ביצוע תנאי מבוסס על שליחת אחת מארבע הודעות מילת-המפתח הבאות אל עצם בוליאני (המחזיר true או false):
ifTrue:,
ifFalse:,
ifTrue:ifFalse:,
ifFalse:ifTrue:.
🔍 דוגמת קוד בסיסית וניתוח הביצוע
| x y |
x := 3.
y := 2.
(x > y)
ifTrue: [ x raisedTo: y ]
ifFalse: [ y raisedTo: x ]
- הביטוי
x > yשולח ל-x(3) את ההודעה הבינרית>עם הארגומנטy(2). - המספר 3 מבצע את ההשוואה ומחזיר את העצם
true(מופע יחיד של המחלקהTrue). - לעצם
trueנשלחת ההודעהifTrue:ifFalse:עם שני הבלוקים כארגומנטים. - התוצאה המוחזרת היא ערך הבלוק הראשון: $3^2 = 9$.
💎 המימוש הפולימורפי במחלקות True ו-False
כיצד זה עובד ללא פקודת if חומרתית? באמצעות פולימורפיזם טהור!
ifTrue: trueAlternativeBlock ifFalse: falseAlternativeBlock
^ trueAlternativeBlock value
ifTrue: trueAlternativeBlock ifFalse: falseAlternativeBlock
^ falseAlternativeBlock value
⚠️ התנהגות באי-התאמה: שליחת ifTrue: לעצם false אינה מבצעת את הבלוק ומחזירה nil! ובאופן דומה, שליחת ifFalse: לעצם true אינה מבצעת את הבלוק ומחזירה nil.
⚡ אופרטורים לוגיים: עצלים (Short-circuit: and:, or:) מול מלאים (Eager: &, |) ושלילה (not)
and:, or:)
מקבלים בלוק כארגומנט: expr1 and: [ expr2 ] או expr1 or: [ expr2 ]. הבלוק מוערך (value) אך ורק במקרה הצורך: ב-and: אם המקבל הוא false, הבלוק השני כלל לא מוערך ומוחזר מיד false (self). ב-or: אם המקבל הוא true, הבלוק השני לא מוערך ומוחזר מיד true.
&, |)
הודעות בינריות המקבלות ערך בוליאני שכבר חושב מראש כארגומנט: expr1 & expr2. שני האגפים מחושבים תמיד בהתאם לחוקי הקדימות, ללא תלות בתוצאת האגף הראשון!
not)
הודעה אונרית פולימורפית: במחלקה True מחזירה false, ובמחלקה False מחזירה true.
2. סעיף 6.6.2: שלוש הלולאות הבסיסיות (whileTrue:, timesRepeat:, to:do:)
למה whileTrue: נשלחת לבלוק, איך ממומשות timesRepeat: ו-to:do: מאחורי הקלעים, ומה ההבדל הקריטי בסוגריים סביב אינטרוול?
נשלחת אל בלוק ללא פרמטרים!
עקרון קריטי: בלולאת while ההודעה נשלחת אל בלוק (ולא אל ערך בוליאני ישיר), משום שהתנאי חייב להיות מחושב מחדש בכל איטרציה! הבלוק המקבל חייב להיות ללא פרמטרים ולהחזיר בוליאני.
• בדוגמה n := 1. [ n < 10 ] whileTrue: [ n := n * 2 ]. ערכו הסופי של n יהיה 16.
• קיימות גם whileFalse: וצורות אונריות whileTrue ו-whileFalse ללא בלוק ארגומנט.
whileTrue: aBlock
^ [ self value ] whileTrue: [ aBlock value ]
נשלחת אל עצם מסוג Integer
מיועדת לביצוע מספר מוגדר וידוע מראש של איטרציות.
• בדוגמה n := 1. 10 timesRepeat: [ n := n * 2 ]. הבלוק ירוץ 10 פעמים וערכו של n בסיום יהיה 1024 ($2^{10}$).
• תובנה מבנית: המתודה timesRepeat: ממומשת בעצמה באמצעות whileTrue: ומשתנה מקומי count הרץ מ-1 עד self!
timesRepeat: aBlock
| count |
count := 1.
[ count <= self ] whileTrue: [
aBlock value.
count := count + 1 ]
נשלחת אל עצם מסוג Number
מקבילה ללולאת for: מקדמת מונה מערך התחלתי (self) עד stop ומפעילה את הבלוק עם המונה התורן (aBlock value: nextValue).
• בדוגמה n := 0. 1 to: 10 do: [ :counter | n := n + counter ]. ערכו של n בסיום יהיה 55.
• לקפיצות מותאמות משתמשים ב-to:by:do: (חיובי או שלילי, כגון 0 to: 9 by: 2 do: [ :i | ... ]).
to: stop do: aBlock
| nextValue |
nextValue := self.
[ nextValue <= stop ] whileTrue: [
aBlock value: nextValue.
nextValue := nextValue + 1 ]
⚠️ משמעות קריטית של סוגריים מול אינטרוולים (Interval):
• עם סוגריים: הביטוי (1 to: 10) do: [ :element | ... ] שולח תחילה למספר 1 את ההודעה to: 10 שיוצרת עצם מסוג Interval, ואז שולח אל ה-Interval את הודעת האוסף do:!
• ללא סוגריים: הביטוי 1 to: 10 do: [ :counter | ... ] מפורש כהודעת מילת-מפתח אחת to:do: הנשלחת ישירות למספר 1 (ללא יצירת עצם Interval כלל!).
• (ואילו כתבת בטעות ללא סוגריים 1 to: 10 collect: [...], המהדר היה מחפש מתודה לא קיימת בשם to:collect: במחלקה Number — לכן באיטרטורים של אוספים חובה לכתוב סוגריים סביב (1 to: 10)!).
ניתוב פולימורפי ב-True ו-False והבנייה ההיררכית של הלולאות מעל whileTrue:
מיני-סימולציה 6.4: אופרטורים עצלים (and:/or:) מול מלאים (&/|) והחזרת nil
מעבדת הלולאות והתנאים הפולימורפיים: מעקב איטרציה-אחר-איטרציה!
בחר מנגנון לולאה או תנאי וצפה בטבלת המעקב המלאה של כל איטרציה, כולל הקוד הפנימי שרץ מאחורי הקלעים במחלקות True/False, BlockClosure, Integer ו-Number:
הגשר הנרטיבי: מלולאות בסיסיות לאיטרטורים פונקציונליים על אוספים ולפרימיטיבים של המכונה הווירטואלית!
ראינו כיצד whileTrue:, timesRepeat: ו-to:do: פועלות. אבל הכוח האמיתי של Smalltalk טמון באיטרטורים הפונקציונליים על אוספים!
בלשונית הבאה נכיר את ששת האיטרטורים המרכזיים (do:, collect: ומנגנון ה-species, select:, reject:, detect:ifNone:, ו-inject:into: כולל מימוש occurrencesOf:), נחשוף את המלכודת הקלאסית של מחיקת איברים תוך כדי איטרציה, ונחתום את הפרק עם סעיף 6.7: פרימיטיבים (<primitive: 1>) ומנגנון הנסיגה (Fallback Code) במכונה הווירטואלית!
איטרטורים על אוספים (Collection Iterations) ופרימיטיבים (Primitives)
ששת האיטרטורים הפונקציונליים (do:, collect: ו-species, select:, reject:, detect:ifNone:, inject:into:), מלכודת שינוי אוסף תוך כדי איטרציה, ופרימיטיבים (<primitive: 1>) ומנגנון הנסיגה (Fallback Code) ב-VM.
מודל מנטלי ועיקרון מנחה: פס ייצור פונקציונלי לאוספים וגשר יעיל אל המכונה הווירטואלית
במקום לכתוב לולאת אינדקסים ידנית לכל פעולה על אוסף, האוסף עצמו מציע לנו "תחנות עיבוד": select: היא מסננת, collect: היא פס התמרה שמייצר אוסף חדש (בעזרת species), ו-inject:into: היא מכונת צבירה וקיפול!
לעולם אל תמחק איברים מאוסף תוך כדי ריצת do: עליו! מחיקה מסיטה את האינדקסים וגורמת לדילוג על איברים עוקבים. הפתרון: איטרציה על עותק (range copy do:) או שימוש ב-select: / reject:.
פרימיטיב (<primitive: 1>) מבצע פעולת יסוד ישירות במכונה הווירטואלית (VM). אם הוא מצליח הערך מוחזר מיד, ואם הוא נכשל (למשל גלישת שלם או חיבור עם Float) — מורץ קוד ה-Fallback ב-Smalltalk שמתחתיו!
1. סעיף 6.6.2 (חלק ד'): איטרציות פונקציונליות על אוספים (Collection Iterations)
שש מתודות האיטרציה המרכזיות הנשלחות אל אוספים (Interval, Array, OrderedCollection, Set, String)
ב-Smalltalk אנחנו כמעט לעולם לא כותבים לולאות אינדקסים ידניות כדי לעבד אוספים. במקום זאת, מחלקות האוספים (כגון Interval, Array, OrderedCollection, Set ו-String) מציעות לנו שש מתודות איטרציה פונקציונליות המקבלות בלוק:
1. do: ו-2. collect: (מקבילה ל-Map + מנגנון ה-species)
• do:: מעבר סדרתי על כל איברי האוסף והפעלת הבלוק עליהם, ללא יצירת אוסף חדש (למשל (1 to: 10) do: [:element | ...]).
• collect:: מפעילה את הבלוק על כל איבר באוסף ומחזירה אוסף חדש המורכב מהערכים שהוחזרו מהבלוק!
דוגמה על תחום: (1 to: 10) collect: [ :element | element + 1 ] מחזיר את המערך: #(2 3 4 5 6 7 8 9 10 11).
דוגמה על מחרוזת: 'abcdefghijk' collect: [:char | char asUppercase] מחזיר את המחרוזת החדשה 'ABCDEFGHIJK'.
collect: aBlock
| nextValue result |
result := self species new: self size.
nextValue := start.
1 to: result size do: [ :i |
result at: i put: (aBlock value: nextValue).
nextValue := nextValue + step ].
^ result
למה self species? משום שתוצאת הטרנספורמציה של collect: על Interval אינה בהכרח סדרה חשבונית! לכן ההודעה species במחלקה Interval מחזירה את המחלקה Array, ויוצרת מערך חדש בגודל המתאים (new: self size).
3. select:, 4. reject: ו-5. detect: / detect:ifNone:
• select: (מקבילה ל-Filter): מחזירה אוסף חדש הכולל רק את האיברים שעבורם הבלוק החזיר true:
#(1 2 3 4 5 6 7 8 9 0) select: [ :each | each even ] "➔ #(2 4 6 8 0)"
(1 to: 10) select: [:element | element > 5] "➔ #(6 7 8 9 10)"
• reject: (שלילת סינון): מחזירה אוסף חדש הכולל רק את האיברים שעבורם הבלוק החזיר false:
#( 'This' 'is' 'an' 'Example') reject: [ :each | (each at: 1) isLowercase ] "➔ #('This' 'Example')"
reject: aBlock
^ self select: [ :element | (aBlock value: element) == false ]
• detect: ו-detect:ifNone:: מחזירה את האיבר הראשון באוסף שעבורו הבלוק מחזיר true:
(1 to: 10) detect: [ :element | element > 5 ] "➔ מחזיר 6"
אם לא נמצא איבר מתאים, detect: מעוררת חריגה (Exception) מסוג NotFound. כדי למנוע קריסה משתמשים ב-detect:ifNone: המספקת בלוק חלופי שיופעל במקרה של כישלון!
6. inject:into: (מקבילה ל-Reduce / Fold) ומימוש occurrencesOf:
מבצעת צבירה וקיפול של איברי האוסף. מקבלת ארגומנט ראשון שהוא הערך ההתחלתי של המצבר, ובלוק בעל שני פרמטרים: המצבר (subTotal / acc / sum) והאיבר התורן (next / each). ערך ההחזרה מהבלוק הופך למצבר בשלב הבא:
(1 to: 10) inject: 0 into: [ :subTotal :next | subTotal + next ] "➔ 55"
occurrencesOf: anObject
^ self inject: 0
into: [ :acc :each |
(each = anObject)
ifTrue: [ acc + 1 ]
ifFalse: [ acc ] ]
🚨 מלכודת ידועה: שינוי אוסף תוך כדי איטרציה!
כאשר מוחקים איברים מאוסף מתוך לולאת do: הרצה על אותו אוסף עצמו:
range do: [ :n | n isPrime ifFalse: [ range remove: n ] ]
האיטרטור מדלג על איברים עוקבים בשל הסטת האינדקסים לאחור בעת המחיקה!
✅ הפתרון התקני: ביצוע האיטרציה על גבי עותק שאינו משתנה:
range copy do: [ :n | n isPrime ifFalse: [ range remove: n ] ]
או שימוש ישיר ונקי ב-select: / reject:!
2. סעיף 6.7: פרימיטיבים (Primitives) ומנגנון הנסיגה (Fallback Code)
למה צריך פעולות ברמת המכונה הווירטואלית (VM), איך נרשם <primitive: 1>, ומתי מורץ קוד ה-Smalltalk שמתחתיו?
אף ש-Smalltalk שואפת לבצע הכל באמצעות עצמים והודעות ברמת התוכנה, קיימות פעולות יסוד בסיסיות החייבות להתבצע ישירות על ידי המכונה הווירטואלית (Virtual Machine – VM) בשפת C או בשפת מכונה – משיקולי יעילות וביצועים, או לצורך גישה ישירה לחומרה ולזיכרון. פעולות אלו נקראות פרימיטיבים (Primitives).
⚡ ארבע קבוצות הפעולות המכוסות ע"י פרימיטיבים:
- הקצאת זיכרון לעצמים חדשים.
- מניפולציה ישירה של בתים (Bytes) בזיכרון.
- פעולות אריתמטיקה והשוואה בסיסיות של שלמים (כפי שמופיע במחלקה
SmallInteger). - גישה ישירה ברמה הנמוכה לאיברי מערך.
🛠️ תחביר ההגדרה ומנגנון הנסיגה (Fallback Code)
+ aNumber
<primitive: 1>
^ super + aNumber
• תחביר: נרשם בתחילת המתודה בתוך סוגריים זוויתיים עם primitive: ומספר מזהה חד-חד-ערכי (כגון <primitive: 1> לחיבור ב-SmallInteger).
• מנגנון הנסיגה והכישלון (Fallback): אם הפרימיטיב מצליח, הערך מוחזר מיד והמתודה מסתיימת. אם הפרימיטיב נכשל (למשל: ניסיון לחבר מספר שלם עם שבר עשרוני Float, או גלישת זיכרון Overflow שהופכת את המספר ל-LargePositiveInteger), המכונה הווירטואלית מדלגת על הפרימיטיב וממשיכה להריץ את קוד ה-Smalltalk הרגיל הכתוב מתחתיו כתוכנית גיבוי (^ super + aNumber)!
השוואת איטרטורים ומסלול הביצוע של <primitive: 1> במכונה הווירטואלית
מיני-סימולציה 6.5: Sandbox חי של פרימיטיבים (<primitive: 1>) וקוד Fallback
צינור האיטרטורים הוויזואלי (Collection Pipeline Visualizer) ומלכודת המחיקה
לפניך האוסף (1 to: 10) כעשרה עיגולים חכמים. הפעל עליו איטרטור ובחן כיצד האוסף מסונן, מותמר, מאותר או נצבר בזמן אמת (או מה קורה כשמוחקים איברים במהלך do: ללא copy!):
הגשר הנרטיבי: השלמנו את כל פרק 6 – בוא נסכם את כל מושגי התחביר בכרטיסיות תלת-ממד!
עברנו יחד דרך כל סעיפי התחביר של שפת Smalltalk: מביטויים, ליטרלים ופסאודו-משתנים (6.1–6.2), דרך שליחת הודעות וסדר קדימויות (6.3), מתודות ובלוקים (6.4–6.5), תנאים פולימורפיים, לולאות ואיטרטורים על אוספים (6.6), ועד לפרימיטיבים של המכונה הווירטואלית (6.7). בלשונית המסכמת הבאה נרכז את כל התמונה ונתרגל 15 כרטיסיות זיכרון תלת-ממדיות (3D Flashcards)!
מילון מושגים אינטראקטיבי וסיכום מלא של פרק 6
סיכום אינטגרטיבי של כל תחביר שפת Smalltalk (סעיפים 6.1–6.7), בוחן מלכודות קוד מהיר, ו-15 כרטיסיות זיכרון תלת-ממדיות במצב בחינה ובמצב למידה.
מודל מנטלי ועיקרון מנחה: כל התחביר של Smalltalk נשען על עצמים, הודעות ובלוקים!
6 פסאודו-משתנים (true, false, nil, self, super, thisContext), מערכים סטטיים #() מול דינמיים {}, וסדר קדימויות מוחלט משמאל לימין: סוגריים ← אונרי ← בינרי ← מילות-מפתח.
מתודה ללא ^ מחזירה את self; בלוק ללא ^ מחזיר את הביטוי האחרון שלו; ו-^ בתוך בלוק יוצא מיד מהמתודה העוטפת כולה (Non-local return)!
תנאים פולימורפיים ב-True/False, לולאות הבנויות על whileTrue:, איטרטורים פונקציונליים (collect:, select:, reject:, detect:, inject:into:) ופרימיטיבים (<primitive: 1>) עם קוד Fallback.
איך כל מרכיבי התחביר של Smalltalk משתלבים למערכת הרמונית אחת?
מיני-סימולציה 6.6: בוחן מלכודות קוד קלאסיות מפרק 6
כרטיסיות זיכרון תלת-ממדיות (3D Flashcards) לכל 15 מושגי פרק 6
לחץ על כל כרטיסייה כדי להפוך אותה ולבחון את עצמך על כל אחד מ-15 מושגי המפתח של פרק 6:
נחשפו: 0 / 15סיימנו במלואו את פרק 6 (תחביר שפת Smalltalk)! מה מחכה לנו בפרק 7?
כעת, אחרי ששלטנו בכל רכיבי התחביר של Smalltalk — מביטויים, ליטרלים ופסאודו-משתנים, דרך שלוש תבניות ההודעות וסדר הקדימויות, מתודות ובלוקים, ועד לתנאים פולימורפיים, לולאות, איטרטורים ופרימיטיבים — אנחנו מוכנים להעמיק עוד יותר במנגנון התקשורת של השפה!
• מוסכמות הסוגריים, סדרות ביטויים ושימושים מתקדמים ב-Cascade!