חלק V: מחלקות • פרק 11 • סעיף 11.1 (חלק א') תשתית כל העצמים ב-Squeak

המחלקה Object: ארכיטקטורת השורש, מנגנון ההדפסה, זהות מול שוויון וחוזה ה-Hash

איך מחלקה אחת ללא שום משתנה מופע מעניקה כ-400 התנהגויות לכל העצמים במערכת, איך פועל הצינור של printString ו-printOn:, מהו עצם המשערך את עצמו (Self-Evaluating), ואיך נשמר החוזה הקריטי בין = ל-hash.

🏛️
🧠

מודל מנטלי ועיקרון מנחה: חוקת האזרחות של עולם העצמים ב-Smalltalk

💡 האנלוגיה המוחשית

תחשוב על המחלקה Object כמו על "תעודת האזרחות הבסיסית" שמקבל כל עצם שנולד ב-Squeak: עוד לפני שכתבת מתודה אחת במחלקה חדשה, היא כבר יודעת להציג את עצמה כמחרוזת, להשוות את עצמה לאחרים ולהשתלב במילונים!

⚖️ חוק הברזל הפדגוגי

איסור מוחלט להוסיף משתני מופע ל-Object! למופעים של Object אין משתני מופע כלל, משום שהוספת שדה תשנה מיידית את מבנה הזיכרון של כל העצמים במערכת ותהרוס מחלקות קריטיות כמו SmallInteger שה-VM מסתמכת על ייצוגן הפיזי המדויק.

🎯 תובנת המפתח

הפרד תמיד בין התנהגות משותפת (Common Behavior) שאין צורך לדרוס (כמו printString, ~~, ~=) לבין התנהגות ברירת מחדל (Default Behavior) שנועדה לדריסה בתת-מחלקות (כמו printOn: ו-= יחד עם hash).

1. מעמדה הארכיטקטוני של המחלקה Object ומקומה של ProtoObject

מדוע כמעט כל מחלקה יורשת מ-Object, למה נוספה מעליה ProtoObject, ולמה אסור להוסיף לה שדות?

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

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

אגב, אם תסתכל על הקצה העליון ביותר של עץ הירושה בסביבת העבודה Squeak, תגלה שמעל Object נוספה מחלקת-על רזה ומינימלית בשם ProtoObject (שממנה יורשת Object). תפקידה של ProtoObject הוא לרכז אך ורק את ההגדרות הגרעיניות ביותר של עצמים — כמו בדיקת זהות פרימיטיבית בזיכרון (==), בדיקת isNil ומתודת האתחול initialize — עבור עצמים מיוחדים ונדירים (כגון עטיפות Proxy) שאינם מעוניינים לרשת את כל המטען העשיר של Object.

🚫 החלטה הנדסית קריטית: איסור הוספת משתני מופע ל-Object!

למופעים של Object אין משתני מופע כלל, וחל איסור מוחלט להוסיף לה משתני מופע! מדוע? מכיוון שכל מחלקה במערכת יורשת מ-Object, הוספת משתנה מופע ל-Object תשנה מיידית את המבנה הפיזי בזיכרון של כל העצמים והמחלקות במערכת! שינוי מבנה כזה הוא הרה אסון עבור מחלקות קריטיות (כגון SmallInteger), אשר המכונה הווירטואלית (VM) מסתמכת על המבנה והייצוג הפיזי המדויק שלהן לצורך ביצועים.

🧭 כ-400 מתודות ב-Object: שני סוגי התנהגויות

  • 1. התנהגות משותפת (Common Behavior): מתודות גנריות המספקות שירות שלם ואין שום צורך או סיבה לדרוס אותן בתת-מחלקות (למשל printString, ~~, ~=).
  • 2. התנהגות ברירת מחדל (Default Behavior): מתודות בסיסיות שנועדו לכך שתת-מחלקות ידרסו אותן (Overriding) ויספקו מימוש מותאם וייחודי עבורן (למשל printOn:, =).

2. מתודות הדפסה (printString ו-printOn:) ועצמים המשערכים את עצמם

איך תבנית Template Method מפרידה בין יצירת הזרם לבין כתיבת התוכן, ולמה קיים רווח ב-Point עם y שלילי?

איך עצם ב-Smalltalk יודע להפוך את עצמו למחרוזת טקסט קריאה כשאנחנו מדפיסים אותו? הדפסת עצם מתבצעת באמצעות שליחת ההודעה printString אל העצם. למעשה, כאשר אתה מסמן ביטוי בחלון העבודה (Workspace) ומבקש להדפיס אותו (באמצעות ctrl+p / print it), המערכת מעריכה תחילה את הביטוי כולו, ואז שולחת אוטומטית את ההודעה printString אל העצם המוחזר — ולכן אין צורך להקיף ביטויים בסוגריים בחלון העבודה רק כדי להדפיס אותם!

אבל כאן מסתתר עיצוב הנדסי מבריק: המתודה printString היא התנהגות משותפת שאנחנו לא דורסים. מאחורי הקלעים היא שולחת לעצם את ההודעה self printStringLimitedTo: 50000, אשר יוצרת זרם תווים / זרם פלט (WriteStream) ושולחת אל העצם את ההודעה printOn: aStream עם הזרם כארגומנט! המתודה printOn: היא כבר התנהגות ברירת מחדל שאמורה לכתוב אל הזרם aStream סדרת תווים המייצגת את העצם. בוא נראה איך Object מממשת אותה כברירת מחדל:

Object >> printOn: aStream
    "Append to the argument, aStream, a sequence of characters that identifies the receiver."
    | title |
    title := self class name.
    aStream
        nextPutAll: (title first isVowel ifTrue: ['an '] ifFalse: ['a ']);
        nextPutAll: title

בוא נעקוב יחד אחרי הקוד של Object >> printOn:: המתודה שולפת את שם המחלקה של העצם ושומרת אותו במשתנה המקומי title (באמצעות self class name). לאחר מכן היא בודקת האם התו הראשון בשם המחלקה הוא אות תנועה באנגלית או עיצור (title first isVowel), וכותבת לזרם את התווית הדקדוקית המתאימה — 'an ' או 'a ' — ואחריה את שם המחלקה עצמו (למשל 'a Point' או 'an Object'). שים לב שפעולת הכתיבה לזרם מתבצעת באמצעות ההודעה nextPutAll:, אשר מקבלת אוסף כלשהו (כגון מחרוזת או מערך), עוברת על איברי האוסף ומשרשרת אותם בזה אחר זה אל תוך הזרם!

הגדרה מחדש (Overriding) של printOn: במחלקה Point: תודות למנגנון הירושה, כל עצם מבין את printOn:, אך במחלקות המייצגות ערכים אנו דורסים אותה כדי להעניק לעצם ייצוג שימושי ומשמעותי יותר מאשר סתם 'a Point'. הנה המימוש במחלקה Point (בעלת משתני המופע x ו-y המייצגים מיקום במישור):

Point >> printOn: aStream
    "The receiver prints on aStream in terms of infix notation."
    x printOn: aStream.
    aStream nextPut: $@.
    (y isNumber and: [y negative]) ifTrue: [aStream space].
    y printOn: aStream

🔍 למה נוספה שורת הרווח עבור y שלילי ב-Point?

המתודה שולחת printOn: aStream אל x, משרשרת את התו הבודד $@ באמצעות nextPut: $@, ואז מדפיסה את y (כך שעבור הנקודה 3@4 מודפסת המחרוזת '3@4').
אבל למה בודקים (y isNumber and: [y negative]) ifTrue: [aStream space]? אם y הוא מספר שלילי (למשל -4), ללא הרווח היה מודפס הביטוי 3@-4, שב-Smalltalk מתפרש כאופרטור בינארי בלתי חוקי בשם @- ואינו ניתן לפענוח תחבירי תקין של נקודה! הוספת הרווח יוצרת את הפלט 3@ -4, המאפשר פענוח וקריאה מחדש של הטקסט כקוד Smalltalk תקני!

💡 עצמים המשערכים את עצמם (Self-Evaluating Objects)

עצם נקרא Self-Evaluating (או Evaluating Self) כאשר ערכו זהה לעצמו — כלומר תוצאת ה-printString שלו (כשמדפיסים אותו עם ctrl+p) מהווה קוד Smalltalk תקני שהרצתו (ב-do it או print it) מחזירה עצם בעל ערך זהה!
• דוגמאות: מספרים, תווים, סימבולים, נקודות (Point), תחומים (Interval), ומערכים סטטיים (#(...)).
• מערכים דינמיים ({...}): הם self-evaluating רק כאשר כל איבריהם הם self-evaluating! למשל, ביצוע {3@4. 4.5} מחזיר {3@4 . 4.5} (הוא כן self-evaluating), אך ביצוע {3@4. 4+5} מחזיר {3@4 . 9} (הוא אינו זהה לקוד המקור 4+5 שיצר אותו!).

3. זהות (==, ~~) מול שוויון (=, ~=) וחוזה ה-hash

ההבדל בין כתובת זיכרון לתוכן מבני, תפקיד ההודעה species ב-Point, וחוק הברזל של דריסת hash

אחת האבחנות החשובות ביותר בתכנות מונחה עצמים היא ההבדל בין השאלה "האם שני מצביעים מתייחסים בדיוק לאותו עצם פיזי בזיכרון?" (זהות – Identity) לבין השאלה "האם שני עצמים מייצגים את אותו ערך ותוכן?" (שוויון – Equality):

📍 בדיקת זהות פיזית (== ו-~~)

  • == (זהות): ממומשת במחלקת ProtoObject כפרימיטיב המשווה ישירות כתובות זיכרון ומצביעים (האם שני הביטויים מתייחסים לאותו עצם עצמו).
  • ~~ (אי-זהות): ממומשת כהתנהגות משותפת: anObject == self ifTrue: [^false] ifFalse: [^true]

⚖️ בדיקת שוויון ערכי (= ו-~=)

  • = ב-Object (ברירת מחדל): מוגדרת כבדיקת זהות: ^self == anObject. מחלקות המייצגות ערכים דורסות אותה לשוויון מבני.
  • ~= (אי-שוויון): ממומשת ב-Object כ- ^(self = anObject) not — לכן דריסה של = מעדכנת אוטומטית גם את תוצאת ~= ואין סיבה לדרוס את ~=!

בוא נראה כיצד המחלקה Point דורסת את מתודת השוויון = כך שהנקודה (3@4) תהיה שווה לנקודה (3@4) גם כאשר מדובר בשני עצמים נפרדים בזיכרון:

Point >> = aPoint
    self species = aPoint species
        ifTrue: [^x = aPoint x and: [y = aPoint y]]
        ifFalse: [^false]

המתודה מוודאת תחילה ששני העצמים שייכים לאותו ה-species (אותה משפחת טיפוסים לוגית), ורק אם כן — היא בודקת שוויון ערכים בין קואורדינטות ה-x ובין קואורדינטות ה-y.

🔑 חוק בל יעבור ב-OOP: הקשר החיוני בין = לבין מתודת hash!

ההודעה hash המוגדרת ב-Object מחזירה מספר שלם המאפיין ומייצג את העצם לצורך אחסון וחיפוש מהיר כמפתחות במילונים (Dictionary) ובקבוצות (Set).
כלל הברזל הארכיטקטוני קובע: אם דרסת את מתודת השוויון =, חובה עליך לדרוס בהתאמה גם את מתודת ה-hash, כך ששני עצמים שנקבע כי הם שווים (a = b) יחזירו תמיד את אותו ערך hash מדויק (a hash = b hash)! אם תפר את החוזה הזה, מילונים וקבוצות יחפשו את העצם בתא הלא נכון ולא יצליחו למצוא אותו!

עזר גרפי 11.1 א' • ארכיטקטורת Object, הדפסה וזהות מול שוויון

מ-ProtoObject ו-Object דרך זרם printOn: ועד לחוזה השוויון (=) וה-hash

Object Root & Contracts
1. שורש ההיררכיה ב-Squeak ProtoObject גרעין מינימלי: ==, isNil, initialize Object (~400 מתודות) • התנהגות משותפת (printString, ~~, ~=) • התנהגות ברירת מחדל (printOn:, =, hash) 🚫 0 משתני מופע! אסור להוסיף שדות (שמירה על ייצוג SmallInteger ב-VM) 2. צינור ההדפסה (Printing) 1. שליחת printString (משותפת) קוראת ל-printStringLimitedTo: 50000 ↓ 2. יצירת WriteStream ושליחת ההודעה printOn: aStream ↓ 3. דריסת printOn: בתת-מחלקה ב-Object: 'a ' / 'an ' + ClassName ב-Point: '3@4' או '3@ -4' (Self-Evaluating) 3. זהות (==) מול שוויון (=) ו-hash זהות פיזית (p1 == p2 ➔ false) שני עצמים נפרדים בכתובות זיכרון שונות שוויון ערכי (p1 = p2 ➔ true) בדיקת species והשוואת ערכי x ו-y 🔑 חוזה ה-Hash (חובה ב-OOP!) אם p1 = p2 אז חובה: p1 hash = p2 hash (עבור מפתחות ב-Dictionary וב-Set)
🖨️

מיני-סימולציה 11.1 א': סייר מנגנון ההדפסה (printOn:) ועצמים המשערכים את עצמם

לחץ על כל ביטוי כדי לעקוב אחרי כתיבת הזרם ולבדוק אם הוא Self-Evaluating
🔑
סימולציה אינטראקטיבית גדולה 11.1 א'

מעבדת זהות (==), שוויון (=) וחוזה ה-hash במילונים

במעבדה זו ניצור שני מופעים נפרדים של נקודה בזיכרון: p1 := 3 @ 4 (בכתובת זיכרון 0x10A0) ו-p2 := 3 @ 4 (בכתובת זיכרון 0x20B4). בחר אילו מתודות לדרוס ובדוק מה מחזירות ==, = וחיפוש במילון (Dictionary):

⚙️ בחר מצב מימוש במחלקת העצם שלנו:
🔬 תוצאות ההשוואה וחיפוש המפתח במילון:
⚡

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

הכרנו את החצי הראשון של Object: מנגנון ההדפסה (printString ו-printOn:) והחוזה שבין זהות (==), שוויון (=) ו-hash. בלשונית הבאה נשלים את התמונה של המחלקה Object: נחקור את מתודות הרפלקציה ושייכות למחלקה (class, isMemberOf:, isKindOf:, respondsTo:), נבין מדוע משתני מופע כמצביעים דורשים זהירות בשכפול עצמים (shallowCopy, copyTwoLevel, deepCopy ו-copy + postCopy), ונכיר את כלי ניפוי השגיאות והאתחול (halt, assert:, subclassResponsibility, shouldNotImplement ו-initialize)!

סוף יחידה 11.1 א' • המחלקה Object (חלק א')
חלק V: מחלקות • פרק 11 • סעיף 11.1 (חלק ב') רפלקציה, שכפול בזיכרון וניהול חריגים

המחלקה Object (המשך): שייכות למחלקה, העתקה ושכפול עצמים, ניפוי שגיאות ואתחול

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

🧬
🧠

מודל מנטלי ועיקרון מנחה: אל תשאל עצם "מי אתה?" – שלח לו הודעה! וכשאתה משכפל – היזהר ממצביעים משותפים!

💡 האנלוגיה המוחשית

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

⚖️ חוק הברזל הפדגוגי

הימנע מבדיקות טיפוס מפורשות (isKindOf:, respondsTo:, isArray) שמפרות את הכימוס והפולימורפיזם — סמוך על הקישור הדינמי! ובעת שכפול, דרוס את postCopy (המופעלת ע"י copy) כדי לשכפל נקודתית שדות פנימיים.

🎯 תובנת המפתח

מתודת new ב-Squeak שולחת אוטומטית initialize למופע החדש; כשאתה דורס את initialize בתת-מחלקה, פתח תמיד בקריאה ל-super initialize!

1. רפלקציה ושייכות למחלקה (Class Membership) בזמן ריצה

המתודות class, isMemberOf:, isKindOf: ו-respondsTo: — ומדוע שימוש יתר בהן נחשב ל-Anti-Pattern?

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

1. class

מחזירה את עצם המחלקה הישירה של המקבל.
• לדוגמה: הביטוי 4 class מחזיר את עצם המחלקה SmallInteger, והביטוי (3@4) class מחזיר את עצם המחלקה Point.

2. isMemberOf: aClass

מקבלת מחלקה כפרמטר ומחזירה true אך ורק אם המקבל הוא מופע מדויק וישיר של aClass!
• לדוגמה: 4 isMemberOf: SmallInteger מחזיר true, ואילו 4 isMemberOf: Number מחזיר false (למרות ש-SmallInteger היא תת-מחלקה של Number!).

3. isKindOf: aClass

מקבלת מחלקה כפרמטר ומחזירה true אם המקבל הוא מופע של aClass או של כל תת-מחלקה שלה במעלה עץ הירושה.
• לדוגמה: 4 isKindOf: SmallInteger, 4 isKindOf: Number ו-4 isKindOf: Object מחזירים שלושתם true, אך 4 isKindOf: Point מחזיר false.

4. respondsTo: aSymbol

מקבלת סלקטור (שם של הודעה) כסימבול (עם קידומת סולמית #) ומחזירה true אם העצם כולל מתודה מתאימה ומבין את ההודעה.
• לדוגמה: 4 respondsTo: #+ ו-'hello' respondsTo: #size מחזירים שניהם true.

⚠️ כלל הנדסי חשוב (מדוע זהו Anti-Pattern?): באופן כללי, בתכנות מונחה עצמים טהור לא מומלץ לבדוק במפורש את סוג העצם (isKindOf:, isMemberOf:) או אם הוא מבין הודעה כלשהי (respondsTo:) בקוד הרגיל שלנו! בדיקות כאלו פוגעות בגמישות ובפולימורפיזם; במקום זאת, עדיף לשלוח את ההודעה ישירות ולסמוך על מנגנון הקישור הדינמי שיאתר את המתודה המתאימה במעלה היררכיית הירושה.

2. העתקה ושכפול עצמים (shallowCopy, copyTwoLevel, deepCopy, copy + postCopy)

מדוע משתני מופע כמצביעים יוצרים שיתוף זיכרון מסוכן בשכפול רדוד, ואיך דריסת postCopy פותרת זאת באלגנטיות?

כשאנחנו משכפלים עצם, אנחנו רוצים בדרך כלל לקבל עצם חדש ועצמאי שהמבנה וההתנהגות שלו זהים לאלה של העצם המקורי, מבלי שהעותק החדש והעצם המקורי ישתפו ביניהם מידע פנימי שעלול להשתנות (למשל, אם נשכפל נקודה Point, לא נרצה ששינוי ב-x של העותק ישנה גם את הנקודה המקורית). אלא שבשפת Smalltalk כל משתני המופע ממומשים כמצביעים לזיכרון (Pointers)! לכן המחלקה Object מציעה מספר רמות שכפול:

1. שכפול רדוד – shallowCopy (מבוסס על basicSize ו-basicAt:)

שיתוף מצביעים פנימיים!

המתודה shallowCopy (הממומשת על בסיס המתודות הפרימיטיביות basicSize ו-basicAt:) משכפלת את עצם השורש בלבד; כל משתני המופע הפנימיים שלו מועתקים כמצביעים זהותיים ומשותפים בין העותק למקור! בוא נראה דוגמה מוחשית עם מערך דו-ממדי:

a1 := {{'a'. 'b'}. {'c'. 'd'}}.
a2 := a1 shallowCopy.
(a1 at: 1) at: 1 put: 'z'.

אם נבדוק כעת את a2, נגלה שגם בו האיבר הראשון הפך ל-'z' (כלומר {{'z'. 'b'}. {'c'. 'd'}}), משום שתתי-המערכים ברמה הראשונה משותפים זהותית בין a1 ל-a2!

2. copyTwoLevel

משכפלת את השורש ומשכפלת רדודה כל אחד מהאיברים ברמה הראשונה (רמה אחת נוספת פנימה). בדוגמה הדו-ממדית שלנו שינוי ב-a1 לא ישפיע על a2; אך אם נגדיר מערך עם שלוש רמות קינון (a1 := {{{'a'}. {'b'}}. {{'c'}. {'d'}}}), השכפול של הרמה השלישית יישאר משותף!

3. deepCopy

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

4. copy + postCopy (הגישה התקנית!)

המתודה copy ב-Object ממומשת כך: copy ^ self shallowCopy postCopy ברירת המחדל של postCopy מחזירה את self. תת-מחלקות נדרשות לדרוס אך ורק את postCopy ולשכפל בה ספציפית שדות שאסור שיהיו משותפים!

3. ניפוי שגיאות ומצבים חריגים (error handling), בדיקות ואתחול (initialize)

המתודות halt, assert:, deprecated:, doesNotUnderstand:, error:, subclassResponsibility, shouldNotImplement ו-initialize

בפרוטוקול error handling במחלקה Object מוגדר אוסף עשיר של מתודות תשתית לניפוי שגיאות, לתכנון לפי חוזה ולטיפול במצבים חריגים:

• halt

משמשת לניפוי שגיאות: שליחת ההודעה (למשל self halt מתוך מתודה) משהה את ביצוע התוכנית ומקפיצה מיידית את חלון ה-Debugger על מחסנית הקריאות הפעילה לבחינת ערכי המשתנים בזמן אמת.

• assert: aBlockOrBoolean

תומכת במודל Design by Contract (תכנון לפי חוזה) להגדרת תנאי כניסה ויציאה למתודות. מקבלת בלוק או ערך בוליאני, ואם התנאי אינו מתקיים (false) – מפסיקה את הביצוע, מעוררת חריגת AssertionFailure ופותחת את ה-Debugger.

• deprecated: anExplanationString

מתריעה למתכנת שהמתודה מיושנת ומציעה חלופה קיימת. למשל, המתודה הישנה doIfNotNil: ב-Object הוחלפה ב-ifNotNilDo: ומשוכתבת עם הודעת deprecated: לפני קריאה למתודה החדשה.

• doesNotUnderstand: aMessage

מתודת הגיבוי של הקישור הדינמי! כאשר חיפוש מתודה נכשל במעלה עץ הירושה, ההודעה doesNotUnderstand: נשלחת לעצם כהזדמנות אחרונה לטיפול בשגיאה (או לבניית Proxy דינמי) לפני קריסה ב-Debugger.

• error: aString / error

מעוררת באופן יזום מצב חריג (Exception) עם הודעת שגיאה מותאמת.

• subclassResponsibility מול shouldNotImplement

subclassResponsibility: מגדירה מתודות מופשטות (Abstract Methods) ומעוררת חריגה המציינת שעל תת-המחלקה לממש את המתודה.
shouldNotImplement: משמשת כשמתודה שנורשה ממחלקת-על אינה מתאימה סמנטית לתת-המחלקה ("ביטול" מתודה מורשת) ומעוררת שגיאה שאין להשתמש בה בעצם זה.

🔍 בדיקות (Testing): isNil ו-notNil כיוצאות דופן

אף ש-Object כוללת מתודות בדיקה כגון isArray, isBoolean, isBlock, השימוש בהן אינו מומלץ משום שהוא מפר את עקרון הכימוס (Encapsulation) והפולימורפיזם (עדיף לשלוח הודעה לעצם ולתת לו לבחור את התגובה המתאימה). עם זאת, המתודה isNil (של ProtoObject) והמתודה notNil (של Object) הן יוצאות דופן ושימושיות מאוד לבדיקה האם משתנה או עצם מאותחלים!

🚀 אתחול עצמים: initialize ו-super initialize

המתודה initialize (המוגדרת ב-ProtoObject) אחראית לאתחול עצמים. בסביבת Squeak, מימוש ברירת המחדל של new שולח אוטומטית למופע החדש את ההודעה initialize מיד לאחר יצירתו! תת-מחלקות הדורסות את initialize כדי לאתחל את משתני המופע שלהן חייבות לפתוח תמיד בקריאה ל-super initialize כדי שמחלקות-העל יאתחלו כהלכה את משתני המופע שהוגדרו בהן.

עזר גרפי 11.1 ב' • אנטומיית השכפול בזיכרון (Pointers)

השוואת מצביעי הזיכרון: shallowCopy מול copyTwoLevel, deepCopy ו-copy + postCopy

Memory Cloning Graph
1. shallowCopy (שכפול רדוד) a1 (מקור) a2 (עותק) תת-מערך משותף! {'z'. 'b'} שינוי בתת-המערך ב-a1 משנה גם את a2! 2. copyTwoLevel מול deepCopy copyTwoLevel (שתי רמות) משכפל שורש + רמה 1 (פותר 2D), אך ברמה 3 ({{{'a'}}}) עדיין יש שיתוף! deepCopy (שכפול עמוק מלא) משכפל רקורסיבית את כל גרף העצמים בכל רמות העומק! 3. הגישה התקנית: copy + postCopy Object >> copy ^ self shallowCopy postCopy ↓ תת-המחלקה דורסת רק postCopy: משכפלת נקודתית רק את השדות שאסור שיהיו משותפים עם המקור!
🧭

מיני-סימולציה 11.1 ב': מעבדת שייכות למחלקה (isMemberOf: מול isKindOf:) וטיפול בשגיאות

לחץ על כל בדיקה כדי לראות את התוצאה בזמן ריצה ואת ההסבר הפדגוגי
🖥️
סימולציה אינטראקטיבית גדולה 11.1 ב'

מעבדת השכפול בזיכרון ה-RAM: shallowCopy, copyTwoLevel ו-deepCopy

בחר את מבנה המערך המקורי a1 (דו-ממדי או תלת-ממדי), בחר את מתודת השכפול ליצירת a2, ואז לחץ על "שנה את האיבר הפנימי ב-a1 ל-'z'" וראה בזמן אמת האם הזיכרון של a2 נפגע!

1. בחר עומק קינון של המערך a1:
2. בחר מתודת שכפול עבור a2 := a1 ...:
🔬 תמונת הזיכרון בזמן אמת (a1 מול a2):
⚡

הגשר הנרטיבי: סיימנו את מחלקת השורש Object – איך בנויה היררכיית המספרים שיורשת ממנה?

אחרי שהכרנו לעומק את המחלקה Object ואת כל השירותים שהיא מעניקה לכל עצם במערכת, אנחנו עוברים אל משפחת המחלקות הראשונה שיורשת ממנה: מערכת המספרים (Numbers) והמחלקה המופשטת Magnitude. איך ייתכן שב-Smalltalk אפילו המספר 4 הוא עצם מלא? איך הגדרת שתי מתודות בלבד ב-Magnitude (< ו-=) מפעילה אוטומטית את כל אופרטורי ההשוואה? ואיך Smalltalk מחשבת שברים מדויקים (Fraction) ומספרים שלמים ענקיים ללא גלישה (Overflow)? בוא נעבור ללשונית 11.2!

חלק V: מחלקות • פרק 11 • סעיף 11.2 עצמים מתמטיים תחת Kernel-Numbers

מערכת המספרים (Numbers): מהמחלקה Magnitude ועד לשברים מדויקים ושלמים ללא גבולות

כיצד מספרים ב-Smalltalk פועלים כעצמים מלאים, תבנית ה-Template Method ב-Magnitude, חילוק שלמים (//) ושארית (\\) ב-Number, המרה אוטומטית בין SmallInteger ל-LargeInteger, ודיוק מתמטי ב-Fraction.

🔢
🧠

מודל מנטלי ועיקרון מנחה: מספרים אינם "ביטים גולמיים" – הם עצמים חכמים שלעולם לא מאבדים דיוק בשקט!

💡 האנלוגיה המוחשית

המחלקה המופשטת Magnitude היא כמו "מנוע השוואה אוניברסלי": ברגע שתת-מחלקה מספקת לה רק שתי אבני בניין (< ו-=), היא מרכיבה מהן אוטומטית את כל שאר פעולות הסדר (>, <=, >=, min:, max:, between:and:)!

⚖️ חוק הברזל הפדגוגי

בניגוד לשפות כמו C או Java שבהן חריגה מגודל שלם גורמת לגלישה (Overflow) וחילוק שלמים מאבד את השבר, ב-Smalltalk גלישה מ-SmallInteger מומרת אוטומטית ל-LargePositiveInteger / LargeNegativeInteger, וחילוק / יוצר שבר מדויק Fraction!

🎯 תובנת המפתח

האופרטור // מבצע חילוק שלמים עם עיגול המנה כלפי מטה, ועליו ממומש האופרטור \\ (שארית החלוקה / מודולו) בנוסחה האלגנטית: ^ self - (self // aNumber * aNumber).

1. היררכיית המספרים ב-Smalltalk והמחלקה המופשטת Magnitude

למה מספרים הם עצמים מלאים תחת הקטגוריה Kernel-Numbers, ואיך עובדת תבנית ה-Template Method ב-Magnitude?

בשפות תכנות כמו Java או C, התרגלנו להפרדה חדה: יש "אובייקטים", ויש "טיפוסים פרימיטיביים" (כמו int או float) שאינם אובייקטים. ב-Smalltalk התמונה טהורה ואחידה לחלוטין: המספרים הם עצמים מלאים לכל דבר ועניין, והם מאורגנים תחת הקטגוריה Kernel-Numbers. בוא נתבונן במבנה עץ הירושה שלהם:

Object
  └── Magnitude (Abstract: <, =, >, <=, >=, min:, max:, between:and:)
        └── Number (Abstract: +, -, *, /, //, \\, @, sqrt, squared, raisedTo:, sin, log, even, odd, positive, negative, asFloat, asInteger)
              ├── Integer (Abstract: factorial, gcd:, isPrime, atRandom)
              │     ├── SmallInteger
              │     ├── LargePositiveInteger
              │     └── LargeNegativeInteger
              ├── Float (e.g. pi, e)
              └── Fraction (numerator / denominator)

בוא נתחיל מהקומה הראשונה מתחת ל-Object: המחלקה המופשטת Magnitude. מחלקה זו מאגדת את כל הישויות במערכת הניתנות להשוואה ולסדר (לא רק מספרים, אלא גם תווים Character, תאריכים וזמנים!). איך היא עובדת? ב-Magnitude מוגדרות שתי מתודות מופשטות בלבד (באמצעות subclassResponsibility): < ו-=. וכל שאר מתודות ההשוואה — >, <=, >=, min:, max:, ו-between:and: — ממומשות ב-Magnitude באופן מלא על גבי < ו-= (זוהי דוגמה מופתית לתבנית העיצוב Template Method)!

2. המחלקה Number, חילוק שלמים (//, \\), SmallInteger ו-Fraction

המתודות המופשטות ב-Number, המימוש האלגנטי של שארית החלוקה \\, המרה שקופה בשלמים ושברים מצומצמים

מתחת ל-Magnitude נמצאת המחלקה המופשטת Number, המוסיפה את כל מנגנוני החישוב האריתמטיים:

➕ אופרטורים ופונקציות ב-Number

  • אופרטורים אריתמטיים בסיסיים: +, -, *, / מוגדרים ב-Number כמתודות מופשטות הנשענות על subclassResponsibility.
  • הודעת @: יוצרת מופע של נקודה (Point) — למשל 10 @ 20.
  • פונקציות מתמטיות מובנות: sqrt, squared, raisedTo:, sin, log (וב-Float קבועים כגון pi ו-e; וב-Integer פעולות כגון factorial, gcd:, isPrime, atRandom).
  • בדיקות והמרות: even, odd, positive, negative, וכן asFloat, asInteger.

➗ מנגנון החילוק והשארית בשלמים (// ו-\\)

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

Number >> \\ aNumber
    ^ self - (self // aNumber * aNumber)

🚀 SmallInteger והמרת טיפוסים אוטומטית ושקופה

המחלקה SmallInteger מייצגת מספרים שלמים בטווח קבוע המנוהל ישירות במכונה הווירטואלית (VM) ביעילות גבוהה (הוא נשמר באופן ישיר בזיכרון במקום להיות מצביע לעצם).
הקסם של Smalltalk: המתכנת לעולם לא צריך לבחור בין SmallInteger ל-LargeInteger! כאשר מתרחשת חריגה מגבולות הטווח (Overflow), המערכת מזהה זאת וממירה את המספר אוטומטית ושקופה ל-LargePositiveInteger או ל-LargeNegativeInteger ללא שום קטיעה וללא אובדן מידע!

🍰 המחלקה Fraction: שברים פשוטים ללא אובדן דיוק!

כאשר אנחנו מחלקים שני מספרים שלמים באמצעות / ותוצאת החילוק אינה שלמה, Smalltalk אינה מבצעת עיגול לאובדן מידע (אינה חותכת לשלם ואינה הופכת לנקודה צפה מקורבת), אלא יוצרת מופע של המחלקה Fraction!
עצם Fraction שומר על מונה (numerator) ומכנה (denominator) מדויקים, והוא תמיד נשמר בצורה המצומצמת ביותר — לדוגמה, הביטוי 6 / 8 יחזיר אוטומטית את השבר המצומצם (3/4)!

עזר גרפי 11.2 • היררכיית Magnitude ו-Number

תבנית Template Method ב-Magnitude, חילוק // ו-\\, והמרה אוטומטית ב-Integer וב-Fraction

Kernel-Numbers Architecture
1. Magnitude (תבנית Template Method) מתודות מופשטות (subclassResponsibility): <      = ↓ מממשות אוטומטית את: >    <=    >= min:    max:    between:and: כל יורש של Magnitude מקבל אותן בחינם! 2. חילוק שלמים ושארית ב-Number / (חילוק מדויק) ➔ יוצר Fraction! 6 / 8  ➔  (3/4) // (חילוק שלמים) ➔ מעגל כלפי מטה 7 // 2 ➔ 3  |  -7 // 2 ➔ -4 \\ (שארית החלוקה / מודולו): ^ self - (self // aNumber * aNumber) 3. המרת Integer אוטומטית SmallInteger מיוצג ישירות ב-VM (ללא מצביע) לביצועים מהירים בטווח הקבוע ↕ גלישה (Overflow)? המרה שקופה! LargePositiveInteger LargeNegativeInteger דיוק בלתי מוגבל ללא קטיעת ספרות!
📐

מיני-סימולציה 11.2: סייר היררכיית המספרים ומתודות Magnitude / Number

לחץ על כל מחלקה בעץ כדי לראות את תפקידה ואת המתודות שלה
🧮
סימולציה אינטראקטיבית גדולה 11.2

מעבדת החישוב ב-Smalltalk: / (שבר מדויק), // ו-\\, והמרה ל-LargePositiveInteger

בחר שני מספרים שלמים או הרץ תרחישים מוכנים כדי לראות כיצד / יוצר שבר מצומצם (Fraction), כיצד // מעגל כלפי מטה וכיצד \\ מחושב לפי הנוסחה self - (self // aNumber * aNumber):

תרחישי בדיקה מומלצים (לחץ להרצה):
🔬 תוצאות ההערכה ב-Squeak:
⚡

הגשר הנרטיבי: ראינו שהמחלקה Magnitude היא בסיס למספרים – אבל מי עוד יורש מ-Magnitude? תווי הטקסט (Character)!

גילינו שכל דבר שניתן להשוואה ולסידור יורש מ-Magnitude. בדיוק בגלל זה גם המחלקה Character (תווים בודדים כמו $a או Character space) יורשת ישירות מ-Magnitude! ומה קורה כשאנחנו מאגדים רצף סדור של תווים יחד? אנחנו מקבלים מחרוזות (ByteString) וסימבולים ייחודיים (Symbol). בוא נעבור ללשונית הבאה (11.3 ו-11.4) ונבין את ההבדל הקריטי ביניהם בזיכרון!

חלק V: מחלקות • פרק 11 • סעיפים 11.3 ו-11.4 ייצוג טקסט, מחרוזות וסמלים ייחודיים

תווים (Character), מחרוזות (ByteString) וסימבולים (Symbol): מוטביליות מול ייחודיות בזיכרון

יצירת תווים ליטרליים ולא-דפיסים, טבלת CharacterTable, היררכיית האוספים המובילה אל String, וההבדל הארכיטקטוני העמוק בין מחרוזת רגילה (ByteString) לבין סימבול ייחודי ובלתי ניתן לשינוי (Symbol).

🔤
🧠

מודל מנטלי ועיקרון מנחה: מחרוזת היא דף טיוטה לעריכה; סימבול הוא תעודת זהות חד-פעמית בזיכרון!

💡 האנלוגיה המוחשית

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

⚖️ חוק הברזל הפדגוגי

עבור שתי מחרוזות ByteString בעלות תוכן זהה: s1 = s2 יחזיר true אך s1 == s2 יחזיר false, והן ניתנות לשינוי (at:put:). עבור שני סימבולים Symbol: #hello == #hello יחזיר תמיד true, והם בלתי ניתנים לשינוי (Immutable)!

🎯 תובנת המפתח

בזכות היותו של כל Symbol עצם ייחודי במערכת (Interned / Singleton פנימי), השוואת סימבולים מהירה להפליא — מספיק להשוות מצביעי זיכרון (==) במקום להשוות אות-אחר-אות!

1. סעיף 11.3: תווים (Character) – יורשים ישירים של Magnitude

תחביר $, תווים לא דפיסים, Character value:, בדיקות תווים ומשתנה המחלקה CharacterTable

בוא נכיר תחילה את אבן הבניין הבודדת של כל טקסט: המחלקה Character. המחלקה Character מוגדרת תחת הקטגוריה Collections-Strings, אך מבחינת עץ הירושה היא יורשת ישירות מהמחלקה המופשטת Magnitude! המשמעות היא שניתן להשוות בין תווים ישירות באמצעות <, >, between:and: וכל שאר מתודות הסדר של Magnitude (למשל $a < $z מחזיר true).

1. יצירת תווים ליטרליים ולא-דפיסים
  • תווים ליטרליים: נרשמים באמצעות הסימן $ — למשל $a, $Z, $7.
  • תווים מיוחדים שאינם דפיסים: נוצרים באמצעות מתודות מחלקה ב-Character: Character space, Character cr, Character tab, Character backspace, Character escape, Character enter
2. קוד ASCII/Unicode ו-CharacterTable
  • Character value: anInteger: מתודת מחלקה המקבלת מספר שלם המייצג ערך קוד ASCII או Unicode ומחזירה את מופע התו המתאים (למשל Character value: 65 מחזיר $A).
  • משתנה המחלקה CharacterTable: מאחסן טבלת מופעים מוכנים מראש של תווי ה-ASCII.
3. מתודות בדיקה והמרה ב-Character
  • מתודות בדיקה: isDigit, isLetter, isAlphaNumeric, isLowercase, isUppercase, isVowel.
  • המרה למחרוזת: המתודה asString ממירה את התו הבודד למחרוזת באורך 1 (למשל $a asString מחזיר 'a').

2. סעיף 11.4: מחרוזות (ByteString) וסימבולים (Symbol)

היררכיית האוספים של String, ההבדל המהותי בין ByteString ל-Symbol, וכל מתודות הטיפול במחרוזות

בשונה מתו בודד, מחרוזת (String) היא אוסף סדור של תווים הנגישים לפי אינדקס. לכן המחלקה String אינה יורשת מ-Magnitude כענף נפרד של מספרים, אלא משתלבת בתוך עץ האוספים (Collection). בוא נראה את מקומה המדויק בהיררכיה:

Object
  └── Collection
        └── SequenceableCollection
              └── ArrayedCollection
                    ├── Array
                    ├── String (מופשטת)
                    │     ├── ByteString (קונקרטית - מחרוזת רגילה הניתנת לשינוי)
                    │     └── Symbol (ייחודית בזיכרון - Interned & Immutable)
                    └── Text

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

📝 מחרוזת רגילה – ByteString

'hello'
  • תחביר: נרשמת בין גרשיים בודדים: 'hello'.
  • אינה ייחודית בזיכרון: ייתכנו שתי מחרוזות שונות בזיכרון בעלות תוכן זהה לחלוטין. לכן עבור שתי מחרוזות כאלו הביטוי s1 = s2 יחזיר true (שוויון תוכן), אך השוואת הזהות s1 == s2 תחזיר false (שני אובייקטים נפרדים בזיכרון).
  • ניתנת לשינוי (Mutable): ניתן לערוך ולעדכן את התווים שבתוכה במקום באמצעות s1 at: 1 put: $H.

💎 סימבול ייחודי – Symbol

#hello
  • תחביר: נפתח בסימן סולמית: #hello.
  • עצם ייחודי במערכת (Interned Object / Singleton פנימי): המערכת מבטיחה שלעולם לא יתקיימו שני סימבולים עם אותו תוכן כאובייקטים נפרדים. שתי מחרוזות סימבול בעלות אותם תווים מצביעות תמיד על אותו העצם בדיוק בזיכרון (s1 == s2 מחזיר תמיד true!).
  • בלתי ניתן לשינוי (Immutable): לא ניתן לשנות את תוכנו של סימבול לאחר יצירתו.
  • יתרון ביצועים אדיר: השוואת סימבולים מהירה להפליא, כי מספיק להשוות את מצביעי הזיכרון שלהם (זהות פיזית ==) במקום להשוות אות-אחר-אות!

🧰 מתודות מרכזיות במחלקה String (ומורשות מ-Collection):

1. פעולות גישה ועדכון

size, at:, at:put:, indexOf:, findString:, replaceFrom:to:with:, replaceAll:with:

2. שרשור והעתקה

, (שרשור מחרוזות), copyFrom:to:, copyUpTo:, copyReplaceAll:with:

3. המרות

asLowercase, asUppercase, asNumber, asDate, asArray, reverse

4. בדיקות ואיטרציה

includes:, isAllDigits, do:, collect:, detect:

עזר גרפי 11.3–11.4 • תווים, מחרוזות וסימבולים בזיכרון

השוואת ייצוג הזיכרון: ByteString (אובייקטים נפרדים וניתנים לעריכה) מול Symbol (עצם יחיד ו-Immutable)

ByteString vs. Symbol
1. מחרוזת רגילה: ByteString ('hello') s1 := 'hello' (0xA1) ניתן לעדכון: at: 1 put: $H s2 := 'hello' (0xB2) אובייקט נפרד בזיכרון! s1 = s2   ➔ true  (שוויון תוכן תו-אחר-תו) s1 == s2  ➔ false (זהויות נפרדות בזיכרון) Mutable: ניתן לשינוי באמצעות at:put: 2. סימבול ייחודי: Symbol (#hello) #hello (Interned @ 0xF0) מופע יחיד במערכת! (Singleton פנימי) #hello = #hello   ➔ true #hello == #hello  ➔ true (אותה כתובת תמיד!) Immutable: אסור לערוך סימבול (at:put: זורק שגיאה!)
🔠

מיני-סימולציה 11.3: חוקר המחלקה Character, תווים לא-דפיסים וטבלת ה-ASCII

לחץ על כל תו כדי לראות את היצירה שלו ואת תוצאות מתודות הבדיקה
💎
סימולציה אינטראקטיבית גדולה 11.4

מעבדת הזיכרון: ByteString מול Symbol ופעולות על מחרוזות

נסה להריץ פעולות השוואה (= מול ==), ניסיון שינוי תו במקום (at: 1 put: $H) ומתודות של המחלקה String על מחרוזת רגילה ('hello') לעומת סימבול (#hello):

בחר ניסוי ב-Workspace של Squeak:
🔬 ניתוח הזיכרון והתוצאה:
⚡

הגשר הנרטיבי: כל הבדיקות שעשינו (isVowel, =, ==, includes:) מחזירות true או false – איך ממומשת המערכת הבוליאנית ב-Smalltalk ללא שום משפט if תחבירי?

שים לב שבכל הסעיפים הקודמים השתמשנו בביטויים כמו ifTrue: ['an '] ifFalse: ['a '] או (y isNumber and: [y negative]) ifTrue: [...]. בשפות רגילות, if הוא מילת מפתח של המהדר. אבל ב-Smalltalk אין מבני בקרה פרימיטיביים בשפה! הכל עובד באמצעות שליחת הודעות ופולימורפיזם טהור בין המחלקות Boolean, True ו-False. בוא נעבור ללשונית 11.5 ונחזה באחד המימושים האלגנטיים ביותר במדעי המחשב!

חלק V: מחלקות • פרק 11 • סעיף 11.5 בקרת זרימה ללא מילות מפתח תחביריות

ערכים בוליאניים (Boolean): מופעים יחידים (Singletons), פולימורפיזם טהור והערכה מקוצרת

איך שפת Smalltalk מממשת משפטי תנאי ופעולות לוגיות ללא שום מילת מפתח if או switch, באמצעות המחלקה המופשטת Boolean, תת-המחלקות True ו-False, והבחנה בין אופרטורים עצלים (and:, or:) למלאים (&, |).

⚖️
🧠

מודל מנטלי ועיקרון מנחה: תנאי ב-Smalltalk אינו "פקודת פיצול" – הוא שליחת הודעה לעצם שיודע אם הוא אמת או שקר!

💡 האנלוגיה המוחשית

במקום שוטר תנועה חיצוני (משפט if) שבודק שלט ומחליט לאן לפנות, ב-Smalltalk יש שני פקידים חכמים בלבד: הפקיד true (שתמיד מפעיל את המעטפה הראשונה ומתעלם מהשנייה) והפקיד false (שתמיד מפעיל את המעטפה השנייה ומתעלם מהראשונה)!

⚖️ חוק הברזל הפדגוגי

המחלקה המופשטת Boolean יורשת מ-Object, ולה שתי תת-מחלקות קונקרטיות בלבד: True ו-False. לכל אחת מהן קיים מופע יחיד בדיוק במערכת (Singleton) — המשתנים-פסאודו true ו-false!

🎯 תובנת המפתח

הבחן תמיד בין אופרטורים לוגיים עצלים / הערכה מקוצרת (and:, or:) המקבלים בלוק קוד ומעריכים אותו רק בעת הצורך, לבין אופרטורים מלאים / Eager (&, |) שמעריכים תמיד את שני הצדדים מראש!

1. ארכיטקטורת Boolean, מופעים יחידים (Singletons) ותנאים ללא IF

איך פולימורפיזם טהור בין המחלקות True ו-False מייתר לחלוטין את הצורך במבני בקרה תחביריים?

הגענו אל אחת הפנינים היפות ביותר של שפת Smalltalk. בשפות כמו C, Java או Python, מילות המפתח if, else ו-switch הן חלק קשיח מתחביר השפה ומהמהדר. ב-Smalltalk אין מבני בקרה פרימיטיביים בשפה כלל! הכל — כולל תנאים והסתעפויות — מתנהל דרך העברת הודעות ופולימורפיזם טהור.

איך הקסם הזה בנוי? תחת המחלקה Object מוגדרת המחלקה המופשטת Boolean. למחלקה זו יש שתי תת-מחלקות קונקרטיות בלבד:

  • True: מייצגת אמת לוגית. קיים לה מופע יחיד בדיוק במערכת (Singleton), המיוצג על ידי המשתנה-פסאודו true.
  • False: מייצגת שקר לוגי. קיים לה מופע יחיד בדיוק במערכת (Singleton), המיוצג על ידי המשתנה-פסאודו false.

עכשיו תראה איך פועלות מתודות התנאי (ifTrue:ifFalse:, ifTrue:, ifFalse:, ifFalse:ifTrue:) שמקבלות בלוקים של קוד כארגומנטים: כאשר אנחנו כותבים (x > 0) ifTrue: [block1] ifFalse: [block2], הביטוי x > 0 מוערך ומחזיר או את העצם true (מופע של True) או את העצם false (מופע של False). ואז נשלחת אליו ההודעה ifTrue:ifFalse:!

True >> ifTrue: trueBlock ifFalse: falseBlock
    "מריצה את הבלוק הראשון ומתעלמת מהשני!"
    ^ trueBlock value

True >> not
    ^ false
False >> ifTrue: trueBlock ifFalse: falseBlock
    "מריצה את הבלוק השני ומתעלמת מהראשון!"
    ^ falseBlock value

False >> not
    ^ true

תעצור לרגע להתפעל מזה: במחלקה True, המתודה ifTrue:ifFalse: פשוט שולחת את ההודעה value לבלוק הראשון ומתעלמת לחלוטין מהבלוק השני — בלי שום בדיקת תנאי פנימית! ובמחלקה False, אותה מתודה בדיוק פשוט שולחת value לבלוק השני ומתעלמת מהראשון! באותו אופן בדיוק, המתודה not (שלילה) ממומשת ב-True על ידי החזרת הקבוע false, וב-False על ידי החזרת הקבוע true. זהו פולימורפיזם טהור בהתגלמותו!

2. אופרטורים לוגיים: הערכה מקוצרת (and:, or:) מול הערכה מלאה (&, |)

למה false & (1/0 = 0) מתרסק בשגיאת חלוקה באפס, בעוד ש-false and: [1/0 = 0] עובר בשלום?

בדיוק מאותו עיקרון של שליחת הודעות ובלוקים נובע גם ההבדל הקריטי בין שתי משפחות של אופרטורים לוגיים ב-Smalltalk:

⚡ 1. אופרטורים עצלים – הערכה מקוצרת (Short-Circuit: and: ו-or:)

ההודעות and: ו-or: מקבלות בלוק קוד ([...]) כארגומנט, ומעריכות אותו אך ורק במקרה הצורך!
• ב-and:: נשלח value לבלוק אך ורק אם המקבל הוא true. אם המקבל הוא false, המתודה ב-False יודעת מיד שהתוצאה היא שקר ומחזירה false מבלי להריץ את הבלוק!
• ב-or:: אם המקבל הוא true מוחזר מיד true ללא הרצת הבלוק; רק אם המקבל הוא false נשלח value לבלוק.
היתרון: משפר ביצועים ומונע שגיאות ריצה (למשל הגנה מפני חלוקה באפס או פנייה ל-nil בתנאי השני).

⚙️ 2. אופרטורים מלאים – הערכה להוטה (Eager: & ו-|)

האופרטורים הבינאריים & ו-| אינם מקבלים בלוק, אלא מקבלים ערך בוליאני מחושב מראש כארגומנט רגיל!
לפי חוקי השפה הרגילים של Smalltalk, כל ארגומנט של הודעה חייב להיות מחושב במלואו לפני שההודעה עצמה נשלחת אל המקבל.
התוצאה: בביטוי עם & או |, שני הצדדים יוערכו תמיד במלואם — גם אם הצד השמאלי כבר קבע את התוצאה!

עזר גרפי 11.5 • ארכיטקטורת Boolean והערכה מקוצרת

פולימורפיזם בין המופעים היחידים true ו-false, והשוואת and: (עצל) מול & (מלא)

Pure Polymorphic Booleans
1. היררכיית Boolean ומופעים יחידים (Singletons) Boolean (מופשטת) מחלקה: True מופע יחיד: true ifTrue: b1 ifFalse: b2 ➔ ^ b1 value מחלקה: False מופע יחיד: false ifTrue: b1 ifFalse: b2 ➔ ^ b2 value 2. הערכה מקוצרת (and:/or:) מול מלאה (&/|) false and: [1 / 0 = 0]  ➔  false (בטוח!) הארגומנט הוא בלוק. המחלקה False מחזירה מיד false מבלי לשלוח value לבלוק — הקוד 1/0 כלל לא מורץ! false & (1 / 0 = 0)  ➔  💥 ZeroDivide Error! הארגומנט אינו בלוק אלא ביטוי רגיל המחושב מראש לפני שליחת ההודעה & — ולכן מתרסק בחלוקה באפס!
🔀

מיני-סימולציה 11.5: מנתב הפולימורפיזם הבוליאני (איך עובד תנאי ללא if)

החלף את המקבל בין true ל-false ועקוב אחרי טבלת המתודות המופעלת
⚡
סימולציה אינטראקטיבית גדולה 11.5

מעבדת ה-Short-Circuit החיה: and: / or: (עם בלוק) מול & / | (הערכה מלאה)

הרץ כל אחד מארבעת הביטויים הבאים וראה צעד-אחר-צעד את סדר ההערכה של המכונה הווירטואלית ב-Smalltalk — מתי הבלוק [1 / 0 = 0] מדולג בבטחה, ומתי הביטוי (1 / 0 = 0) מתרסק בחלוקה באפס!

בחר ביטוי להרצה במעבדה:
🔬 מעקב ביצוע צעד-אחר-צעד (Execution Trace):
⚡

הגשר הנרטיבי: השלמנו את כל חמשת הסעיפים של פרק 11 – בוא נחבר את כל התמונה ונתרגל!

סיימנו לעבור לעומק על כל חמשת הסעיפים של פרק 11: מהמחלקה Object (ו-ProtoObject), דרך מערכת המספרים (Magnitude, Number, Integer, Fraction, Float), תווים (Character), מחרוזות וסימבולים (ByteString מול Symbol), ועד למימוש הפולימורפי הטהור של Boolean (True ו-False). בלשונית המסכמת הבאה נבחן את עצמנו באמצעות 15 כרטיסיות תלת-ממד אינטראקטיביות ונתכונן לתחנה הבאה שלנו: פרק 12 – אוספים ב-Squeak!

חלק V: מחלקות • פרק 11 • יחידה 11.6 אינטגרציה ואינדקס מושגים

מילון מושגים אינטראקטיבי וסיכום עקרונות פרק 11 (מחלקות בסיסיות ב-Squeak)

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

🏆
🧠

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

💡 תבנית Template Method

פגשנו אותה לאורך כל הפרק: ב-Object (printString קוראת ל-printOn:; copy קוראת ל-shallowCopy postCopy; ~= נשענת על =) וב-Magnitude (כל אופרטורי ההשוואה נשענים על < ו-=).

⚖️ חוזי זיכרון וזהות

ל-Object אסור להוסיף משתני מופע (כדי לא לשבור את SmallInteger ב-VM); דריסת = מחייבת דריסת hash; ו-Symbol מבטיח זהות יחידה בזיכרון (==) ואי-השתנות לעומת ByteString.

🎯 פולימורפיזם במקום תחביר

אנחנו נמנעים מבדיקות isKindOf:/respondsTo: וסומכים על קישור דינמי — ואפילו תנאים בוליאניים (ifTrue:ifFalse:, and:, or:) ממומשים פולימורפית בין המופעים היחידים true ו-false!

עזר גרפי 11.6 • מפת העל של פרק 11 (סעיפים 11.1–11.5)

הסינרגיה בין ProtoObject ו-Object אל Magnitude, Number, Character, String ו-Boolean

Chapter 11 Master Map
ProtoObject ➔ Object (0 משתני מופע · ~400 מתודות) printString/printOn: · ==/=/hash · class/isKindOf: · copy/postCopy · halt/assert: · initialize 11.2–11.3: Magnitude מופשטות: <, = | ממומשות: >, <=, >=, min:, max: • Number: Integer (Small/Large), Float, Fraction • חילוק: / (שבר), // (עיגול מטה), \\ (מודולו) • Character: $a, space, cr, value:, CharacterTable 11.4: Collection ➔ String • ByteString ('hello'): אינה ייחודית (== false), ניתנת לשינוי (at:put:) • Symbol (#hello): ייחודי בזיכרון (== true), בלתי ניתן לשינוי (Immutable) 11.5: Boolean (פולימורפיזם טהור) • True (true) ו-False (false) – Singletons • תנאים ללא if: ifTrue:ifFalse: שולחת value • עצלים (Short-Circuit): and: , or: (בלוק) • מלאים (Eager): & , | (מעריכים שני צדדים)
⚡

מיני-סימולציה 11.6: בוחן אבחנה מהיר – 4 מלכודות קלאסיות בפרק 11

לחץ על כל דילמה כדי לוודא שלא תיפול בה במבחן
🃏
סימולציה אינטראקטיבית גדולה 11.6

כרטיסיות זיכרון תלת-ממדיות (3D Flashcards) לכל 15 מושגי פרק 11

לחץ על כל כרטיסייה כדי להפוך אותה ולבחון את עצמך על כל אחד מ-15 מושגי היסוד של פרק 11:

נחשפו: 0 / 15
🚀

סיימנו במלואו את פרק 11! מה מחכה לנו בפרק 12?

בפרק 11 הכרנו לעומק את עמודי התווך של ספריית המחלקות ב-Squeak: מחלקת השורש Object (ו-ProtoObject), היררכיית המספרים (Magnitude, Number, Integer, Fraction, Float), תווי הטקסט (Character), מחרוזות וסימבולים (ByteString ו-Symbol), והמימוש הפולימורפי של המערכת הבוליאנית (Boolean, True ו-False).

🎯 התחנה הבאה שלנו – פרק 12: אוספים ב-Squeak (החותם את חלק V): בפרק 12 נחקור את מבנה היררכיית האוספים, את ההבחנה בין אוספים בני-מיון, מבוססי-מפתח ומבוססי-תוכן, ואת המחלקות המרכזיות: OrderedCollection, SortedCollection, Array, Interval, Set, Bag, Dictionary ו-IdentityDictionary!
🕹️

מעבדת חקר אינטראקטיבית

❓

5 שאלות פתוחות מדורגות + 5 שאלות אמריקאיות עם הסברים מלאים לכל יחידה

🚀 התחנה הבאה שלנו: פרק 12 – אוספים ב-Squeak

סיימנו במלואו את פרק 11: מחלקות בסיסיות ב-Squeak (סעיפים 11.1 עד 11.5). התחנה הבאה שלנו היא פרק 12: אוספים ב-Squeak, החותם את חלק V: