האוניברסיטה הפתוחה • קורס 20906 • סיכום מקיף

סיכום קורס: תכנות מונחה עצמים

סיכום מדויק, תמציתי ומקיף של כל 18 פרקי הקורס, העקרונות, קטעי הקוד, שאלות הניתוח והדוגמאות המרכזיות

חלקים I–II: הפשטה ועצמים (פרקים 1–3) חלקים III–V: Smalltalk, Squeak ואוספים (פרקים 4–12) חלק VI: סימולציית N המלכות ו-Morphic (פרקים 13–17) חלק VII: מטה-מחלקות ומודל 5 הרמות (פרק 18)

חלק I: מבוא ומנגנוני הפשטה (פרקים 1–2)

מדוע לומדים Smalltalk כ-OOP טהורה, בעיית הקבועים החוזרים, ו-7 שלבי התפתחות מנגנוני ההפשטה.

1.1–1.11 מטרות הקורס, למה Smalltalk וסביבת העבודה Squeak

  • ספרי הלימוד והמקורות: הקורס מבוסס על שני ספרי מקור מרכזיים: (1) Squeak by Example (Black et al., 2007) – ללימוד שפת Smalltalk בסביבת Squeak (מתמקד בפרקים 4–9, 11–12, 17–18), ו-(2) Object-Oriented Programming (Timothy Budd, 2002) – לעקרונות תכנות מונחה עצמים בחתך שפות כגון C, C++, Ada, Modula-2, Eiffel, Java ו-Smalltalk (מתמקד בפרקים 2–3, 10, 13–16, 18).
  • מדוע כדאי ללמוד Smalltalk? בדומה ל-LISP ול-LOGO, שפת Smalltalk היא שפה מונחית עצמים "טהורה" (Pure OOP) המתבססת על מנגנון הפשטה אחד ויחיד, פשוט וקליט: כל דבר הוא עצם, והתכנות מתבצע אך ורק על ידי הגדרת עצמים ושליחת הודעות ביניהם. היא כמעט נטולת תחביר ומכריחה את הסטודנט לחשוב ולתכנת בגישת OOP טהורה.
  • סביבת העבודה Squeak: סביבת פיתוח בקוד פתוח תואמת Smalltalk-80, הממומשת כולה ב-Smalltalk עצמה ובעלת מנשק גרפי עשיר (ששימש אף את אולפני Disney ביצירת סרטים).

2.1 דוגמה לשימוש במנגנוני הפשטה: מניעת חזרה על קבועים

מנגנוני הפשטה מוכרים בשפות תכנות: משתנים, קבועים, טיפוסי נתונים, פרוצדורות/פונקציות, מודולים, טיפוסי נתונים מופשטים (ADT), מחלקות וירושה.
לדוגמה, נניח שב-C++ מוגדר מערך int a[100]; ולולאה for (i = 0; i < 100; i++) cout << a[i];. להלן השוואת 4 מנגנוני הפשטה למניעת חזרה על הקבוע 100 ביתרונותיהם וחסרונותיהם:

שיטת הפשטה קוד מרכזי הערכת מנגנוני ההפשטה (יתרונות וחסרונות)
ללא הפשטה (קבוע כפול) int a[100]; ... i < 100 הבעיה בגישה זו: הקבוע 100 מופיע פעמיים; אם גודל המערך יעודכן ל-200 ויישכח עדכון תנאי הלולאה, תיווצר חוסר עקביות ושגיאות ריצה.
פקודת מאקרו ב-C++ #define N 100 מגדיר את גודל המערך במקום אחד, אך מבוצע ע"י הקדם-מעבד (Preprocessor) כהחלפת טקסט עיוורת לפני ההידור, ללא בדיקת טיפוסים או תחום הכרה (Scope).
שימוש ב-enum ב-C++ enum {N = 100}; int a[N]; יתרון לעומת מאקרו: enum הוא חלק משפת התכנות עצמה (כפוף לבדיקות המהדר, ל-Scope ולסימבולים ב-Debugger).
חיסרון לעומת קבוע ישיר: דורש שורת קוד נוספת ומפצל את הגדרת המערך וגודלו לשתי שורות נפרדות במקום שורה אחת.
מאקרו חישוב גודל #define SIZEOF(arr) (sizeof(arr)/sizeof(arr[0])) משלב את שני היתרונות: הגדרת המערך וגודלו בשורה אחת (int a[100];) והגודל אינו מופיע פעמיים, אך עדיין תלוי במאקרו קדם-מעבד.
תמיכה מובנית ב-Java a.length או for (int k : a) ב-Java תמיכה מובנית בתוך השפה: המערך הוא עצם היודע את אורכו (a.length), ובלולאת for-each תחביר הלולאה מופשט עוד יותר ללא מונה אינדקס כלל (for (int x : a))!

2.2 ★ 7 שלבי התפתחות מנגנוני ההפשטה (סעיפים 2.2.1–2.2.7)

1. אסמבלר (Assembler): ייצוג כתובות ואופרטורים בסיסיים באופן סימבולי במקום מספרי, תוך היעזרות במהדר/אסמבלר לתרגום לשפת מכונה, אך הקוד אינו מובנה.
2. ייצוג נתונים / הפשטה מבוססת נתונים (Fortran, Cobol): דגש בייצוג מבנה הנתונים הגלובלי (`Global Data`) והגדרת פעולות (`Proc 1..4`) הפועלות עליו באופן המזכיר מסדי נתונים.
3. פרוצדורות מקוננות / הפשטה מבוססת תהליכים (Pascal, C): דגש בתהליך ובאלגוריתם (תכנות מלמעלה למטה Top-Down) כאשר כל תהליך מגדיר לוקלית תת-תהליכים ומבני נתונים. החיסרון (למשל במחסנית ב-C): הסתרת המידע לוקה בכך שכדי לשתף את המחסנית בין `init()`, `push(val)`, `top()` ו-`pop()`, נדרשים משתנים גלובליים (`int datastack[100]; int datatop = 0;`).
4. מודול (Modula-2): אוסף של פרוצדורות ונתונים עם יכולת לשלוט במה שיוחצן (Export) ומה שיוסתר. המגבלה הקריטית של מודול: מודול הוא כמו עצם בודד או מחלקה עם עצם (מופע) יחיד בלבד!
5. טיפוס נתונים מופשט (ADT ב-Ada): מאפשר להגדיר טיפוס חדש שייתכנו ממנו כמה מופעים שלכל אחד סט אופרטורים משלו. בהגדרת `adt List` עם `list = Nil | CELL of integer * list`, כל פעולה כגון `empty(l)` או `tail(l)` מבצעת פיצול מקרים (`case l of Nil => ... | CELL(x, l') => ...`).
6. מחלקה (Class ב-OOP): המחלקה היא טיפוס נתונים מופשט שבו הדגש מושם בייצוג הנתונים (העצמים) במקום בייצוג הפעולות!
• לדוגמה, מוגדרות המחלקות `EmptyList` (שבה `empty = true; head = error; tail = error`) ו-`NonEmptyList(x, l)` (שבה `empty = false; head = x; tail = l`).
• השוואת מטריצת ADT מול OOP: בייצוג כ-ADT החלוקה היא לפי הפעולות (`empty?`, `head`, `tail` חוצות את העמודות `nil` ו-`cons`), ואילו בייצוג מונחה עצמים (OOP) החלוקה היא לפי טיפוסי העצמים (`nil` / `EmptyList` ו-`cons` / `NonEmptyList`), כשכל מחלקה מאגדת את מימוש כל הפעולות עבורה.
7. ירושה (Inheritance): מנגנון הפשטה המאפשר שיתוף קוד בין מחלקות. בניסוי המחשה מפורסם, ספרייה בשפת Ada באורך 125K שורות קוד מומשה מחדש בשפת C++ (בעזרת ירושה) באמצעות 10K שורות קוד בלבד!

חלק II: עצמים – המושג "עצם" (פרק 3)

סוגי עצמים, 3 מאפייני העצם (זהות, מצב, התנהגות), עצמים בשפת C, זהות עצמית, בדיקת שוויון וסוגי העתקה.

3.1–3.3 סוגי עצמים, 3 מאפייני העצם ודוגמאות

  • שאלת מפתח: איזה מושג חשוב יותר ב-OOP – "עצם" או "מחלקה"? פתרון: המושג "עצם" הוא בסיסי יותר כמובן, אך למעשה מושג ה"מחלקה" הוא חשוב יותר (כי הוא מנגנון ההפשטה המגדיר קבוצות של עצמים).
  • סיווג העצמים לשני מרחבים:
    1. עצמים השייכים למרחב הבעיה (`problem space`): מייצגים ישויות מעולם הבעיה, כגון עובד, שעון נוכחות, פריט במלאי, פטיש בחנות, מספר מרוכב $e+2i$, כלי רכב נושא לוחית זיהוי 19-070-35, או רשימה ממרחב הבעיה (כגון רשימת קניות או רשימת נוכחות תלמידים).
    2. עצמים השייכים למרחב התכנית / הפתרון (`program / solution space`): מייצגים מבני עזר תכנותיים, כגון קובץ, או רשימה ממרחב הפתרון (כגון רשימת בלוקים פנויים במנהל זיכרון או שרשרת גיבוב). שימו לב כי רשימה כגון (13, "hello", true) יכולה להשתייך הן למרחב הבעיה והן למרחב הפתרון בהתאם להקשר השימוש בה.

🔨 שלושת מאפייני העצם (זהות, מצב והתנהגות):

1. זהות (Identity) תכונה המבדילה את העצם מכל שאר העצמים (לרוב כתובת הזיכרון שבה שמור העצם). מאפשרת לקבוע אם שני שמות מתייחסים לאותו עצם ממש. זהות העצם אינה משתנה לעולם!
2. מצב (State – המאפיין הסטטי) אוסף הערכים בתכונות העצם (למשל בפטיש: מוצג למכירה; במספר מרוכב: חלק ממשי וחלק מדומה; ברכב: מהירות, מיקום, צבע). המצב יכול להשתנות לאורך חיי העצם.
3. התנהגות (Behavior – המאפיין הדינמי) אוסף הפעולות שהעצם מבצע בתגובה להודעות שהוא מקבל (למשל בפטיש: נעיצת מסמרים; ברשימה: הוספת/מחיקת איבר, מיון; ברכב: נסיעה, האצה, עצירה).

3.4 האם בשפת C אפשר להגדיר עצמים? (סעיפים 3.4.1–3.4.3)

1. פונקציה (דוגמת מונה `counter`) int counter(void) { static int n = 0; return ++n; }
• זהות: מופע הפונקציה.
• מצב: ערך המשתנה הסטטי n.
• החיסרון: ההתנהגות מנוונת (הודעה אחת בלבד – הפעלת הפונקציה) ומופע יחיד.
2. מחסנית כמודול קובץ מנשק stack.h (push, pop) וקובץ מימוש stack.c עם static int stack[100], sp;.
• מצב והתנהגות: המשתנים הסטטיים ואוסף הפונקציות בקובץ.
• החיסרון: הזהות מנוונת – יש רק מופע אחד של המחסנית!
3. מצביע לקובץ (`FILE *fin`) הדומה ביותר לעצם אמיתי!
• זהות: המצביע לקובץ (ייתכנו מצביעים רבים לקבצים שונים!).
• מצב: תוכן הקובץ.
• התנהגות: הפעולות על המצביע (fopen, feof, fgetc, סגירת הקובץ).

3.5 ★ זהות עצמית, בדיקת שוויון וסוגי העתקה

  • שם מול זהות והמזהה השמור לעצם הנוכחי: שם העצם נבדל מזהותו (ייתכנו שני עצמים בעלי אותו שם, עצם בעל כמה שמות, או עצם ללא שם כלל, אך לא ייתכנו שני עצמים בעלי אותה זהות). המזהה השמור לעצם הנוכחי: ב-C++ וב-Java: this | ב-Eiffel: Current | ב-Smalltalk: self.
  • שני סוגי השוואה: (1) השוואת הזהות: שני שמות שווים אמ"ם הזהות זהה (מצביעים לאותו עצם ממש). (2) השוואה מבנית: שני שמות שווים אמ"ם לשני העצמים יש מבנה זהה ומצב זהה.

📋 סיווג סוגי העתקת עצמים ב-Java (העתקה זהותית, רדודה, רב-שכבתית ועמוקה – חובה למבחן!):

בהעתקת רשימה מקושרת של נקודות class PList { public Point head; public PList tail; }:

  • 1. העתקה זהותית: המצביע לעצם משוכפל; בסיום ההעתקה יש שני שמות לאותו עצם ממש ונותרת זהות אחת בלבד.
  • 2. העתקה מבנית: מצב העצם מועתק לעצם חדש (בסיום יש שני עצמים בעלי זהויות שונות). נחלקת ל:
    • העתקה רדודה (`oneLevelCopy`): השורש מועתק בהעתקה מבנית (PList res = new PList();), אך העצמים הפנימיים מועתקים זהותית (res.head = head; res.tail = tail;).
    • העתקה רדודה לעומק 2 (`twoLevelCopy`): יוצרת נקודה חדשה בראש (res.head = new Point(head.x, head.y);) ומעתיקה את הזנב לעומק אחד (res.tail = tail.oneLevelCopy();).
    • העתקה עמוקה (`deepCopy`): הן השורש והן כל חוליות הזנב והנקודות הפנימיות מועתקים רקורסיבית/איטרטיבית בהעתקה מבנית עמוקה (לכל חוליה נוצרים new Point(...) ו-new PList() חדשים).
    • העתקה רקורסיבית (`copy`): ב-PList.copy() כתוב: res.head = head.copy(); if (tail != null) res.tail = tail.copy();.
      ניתוח עומק ההעתקה (האם זו העתקה רדודה או עמוקה?): עומק ההעתקה נקבע כאן ע"י העצמים הפנימיים והאופן שבו הם מממשים את copy()! אם המחלקה Point מוגדרת עם שדות קבועים public final int x, y; (כלומר Point היא עצם בלתי משתנה / Immutable שמצבו אינו יכול להשתנות לעולם), Point.copy() יכולה לממש בחוכמה return this; (העתקה זהותית של הנקודה מבלי לשכפלה לשווא בזיכרון), בעוד חוליות ה-PList משוכפלות מבנית לכל אורך הרשימה!

3.6 מאפיינים סטטיים ודינמיים של עצמים (סעיפים 3.6.1–3.6.2)

📦 3.6.1 מאפיינים סטטיים: • תכונה (`attribute` / `field` / `property` / `instance variable` / `data member`): המקום שבו נשמר המצב.
• מבנה (`structure`): אוסף התכונות.
• מצב (`state`): אוסף הערכים במבנה.
• עצם נצחי (`Immutable Object`): עצם שמצבו אינו משתנה לאורך חייו; המבנה שלו מורכב מתכונות נצחיות בלבד.
⚡ 3.6.2 מאפיינים דינמיים (הבחנה בין הודעה לשיטה): • הודעה (`Message`): שם סימבולי (לעיתים עם ארגומנטים נלווים) שהלקוח שולח אל העצם.
• שיטה / מתודה (`Method`): הקוד המתאים שהעצם בורר ומריץ בהתאם למהות ההודעה.
• פרוטוקול (`Protocol`): אוסף ההודעות שהעצם מבין.
הבחנה קריטית: עצמים שונים עשויים להבין את אותה הודעה, אבל להגיב עליה באופן שונה (להפעיל עבורה שיטה שונה).

חלק III: מבוא לשפת סמולטוק וסביבת Squeak (פרקים 4–5)

ארכיטקטורת קובצי Squeak, 3 כפתורי העכבר, כלי הפיתוח ופיתוח המשחק המלא Quinto.

4.1–4.4 קובצי סביבת Squeak, 3 כפתורי העכבר ותפריט העולם

💾 3 הקבצים המרכיבים את סביבת Squeak (סעיפים 4.1–4.2):
  • SqueakX.image (קובץ התמונה): מכיל את "תמונת" סביבת הריצה ומצב כל האובייקטים (כולל מחלקות ומתודות) כפי שנשמרה לאחרונה.
  • SqueakX.sources: קובץ לקריאה בלבד המכיל את קובצי המקור המתאימים לגרסה X ואילך.
  • SqueakX.changes (קובץ השינויים): מכיל את רשימת כל השינויים שהתבצעו בקובצי המקור (מומלץ לגבות את ה-image וה-changes יחד!).
🖱️ 3 כפתורי העכבר והעולם `World` (סעיפים 4.2–4.4):
  • כפתור אדום (שמאלי): בחירת פריטים וסימון טקסט; בלחיצה על רקע ה-World מעלה את תפריט העולם (`World Menu`) (ניתן לקבעו למסך בלחיצה על צלמית הנעץ בכותרתו!).
  • כפתור צהוב (ימני): מעלה תפריטים תלויי-הקשר (Context Menus).
  • כפתור כחול (מרכזי / גלגלת): מקיף את העצם במסגרת צלמיות ה-morphic halo (להזזה, סיבוב ושינוי גודל).
  • שמירה ויציאה (4.4): save and quit בתחתית תפריט העולם שומרת את תמונת המסך בדיוק כפי שהייתה.

4.5–4.10 כלי העבודה ב-Squeak, חיפוש מחלקות/מתודות והמתודה shout

  • חלון עבודה (`Workspace`) מול חלון הודעות (`Transcript` – קיצור `Ctrl+t`): Transcript show: 'hello world'; cr. מדפיס ל-Transcript ויורד שורה (בזכות ה-Cascade ;).
  • 4 פקודות הביצוע והבחינה ב-Workspace:
    1. do it (Ctrl+d): מבצע את הביטוי המסומן (למשל 3 + 4 מחשב 7 אך לא מציג דבר).
    2. print it (Ctrl+p): מבצע את הביטוי, שולח לעצם המוחזר את ההודעה printString ומציג את הייצוג הטקסטואלי שלו (למשל Time now).
    3. inspect it (Ctrl+i): פותח Inspector – חלונית שמאל מציגה את self ואת משתני המופע, חלונית ימין מציגה את הערך הנבחר, והחלונית התחתונה משמשת כ-Workspace שבו self קשור לעצם הנבחן (למשל self squared על 7 יחזיר 49).
    4. explore it (Ctrl+Shift+i / Shift+Alt+i): פותח Explorer המציג את העצם במבנה עץ היררכי (ניתן לעבור מ-Explorer ל-Inspector ולהיפך דרך הכפתור בחלק הימני התחתון).
  • טעינת חבילות עם `SqueakMap` ודפדפן המערכת `System Browser`:
    • SqueakMap: כלי לטעינת חבילות תוכנה מרשת האינטרנט.
    • 4 חלוניות ה-System Browser (משמאל לימין): (1) קטגוריות (Categories) $\to$ (2) מחלקות (Classes) $\to$ (3) פרוטוקולים (Protocols, או --all--) $\to$ (4) מתודות (Methods). לחיצה על כפתור ? במרכז מציגה את תיעוד המחלקה.
    • 4 דרכים לחיפוש מחלקה: (1) ניווט בחלונית הקטגוריות, (2) שליחת ההודעה Boolean browse (או סימון שם המחלקה ו-Ctrl+b), (3) פתיחת Hierarchy Browser (כפתור hierarchy), (4) find class... (f) בתפריט ההקשר של חלונית הקטגוריות.
    • חיפוש מתודות ב-Method Finder: לפי שם/חלק משם (למשל time) או לפי דוגמת מקבל, ארגומנטים ותוצאה מופרדים בנקודה: 4. 16 $\implies$ 4 squared --> 16.
// הגדרת המתודה shout במחלקה String (פרוטוקול converting):
shout
    "Answer a String converted to Uppercase followed by an exclamation mark."
    ^ self asUppercase, '!'
// משולש צהוב בפינה הימנית-עליונה של חלונית העריכה מציין שינויים שטרם נשמרו -> שומרים עם accept (Ctrl+s).
// הרצה ב-Workspace עם print it:  'hello' shout  -->  'HELLO!'

5.1–5.7 ★ הרצת תכנית ראשונה: המשחק Quinto, דיבוג הבאג וייצוא fileOut

המשחק Quinto מתנהל על לוח משובץ ($10 \times 10$): לחיצה על כל משבצת הופכת את מצבן (צהוב חיוור $\leftrightarrow$ כחול כהה) של 4 המשבצות השכנות הצמודות אליה (מעליה, מתחתיה, מימינה ומשמאלה – ללא אלכסונים וללא המשבצת שנלחצה עצמה!). התכנית בקטגוריה SBE-Quinto מורכבת משתי מחלקות:

1. המחלקה SBECell (סעיפים 5.2–5.4): יורשת מ-SimpleSwitchMorph (כפתור דו-מצבי) עם משתנה מופע יחיד mouseAction:
SBECell >> initialize
    super initialize.
    self label: ''.
    self borderWidth: 2.
    bounds := 0@0 corner: 16@16.
    offColor := Color paleYellow.
    onColor := Color paleBlue darker darker.
    self useSquareCorners.
    self turnOff

SBECell >> mouseAction: aBlock   "פרוטוקול accessing"
    ^ mouseAction := aBlock

SBECell >> mouseUp: anEvent      "פרוטוקול event handling"
    mouseAction value
2. המחלקה SBEGame: יורשת מ-BorderedMorph עם משתנה מופע יחיד cells (מסוג Matrix) ומתודת cellsPerSide ^ 10:
SBEGame >> initialize
    | sampleCell width height n |
    super initialize.
    n := self cellsPerSide.
    sampleCell := SBECell new.
    width := sampleCell width. height := sampleCell height.
    self bounds: (5@5 extent: ((width*n)@(height*n)) + (2 * self borderWidth)).
    cells := Matrix new: n tabulate: [:i :j | self newCellAt: i at: j]

SBEGame >> newCellAt: i at: j
    | c origin |
    c := SBECell new.
    origin := self innerBounds origin.
    self addMorph: c.
    c position: ((i-1) * c width) @ ((j-1) * c height) + origin.
    c mouseAction: [self toggleNeighboursOfCellAt: i at: j].
    ^ c   "חובה! בלעדיו מוחזר self שהוא SBEGame!"
🐞 סעיפים 5.6–5.7: מלכודת הבאג ב-newCellAt:at: ושמירת התכנית (`fileOut` / `fileIn`):
• מלכודת ערך החזרה ב-newCellAt:at:: ללא השורה ^ c בסוף newCellAt:at:, המתודה מחזירה כברירת מחדל את ^ self (עצם ה-SBEGame!). לכן כל איברי המטריצה cells מתמלאים במצביעים ל-SBEGame במקום ל-SBECell, ובלחיצה על תא המתודה toggleNeighboursOfCellAt:at: שולחת (cells at: i-1 at: j) toggleState ל-SBEGame וזורקת MessageNotUnderstood: SBEGame>>toggleState!
• ייצוא וייבוא קוד: ייצוא קטגוריה לקובץ טקסט SBE-Quinto.st מתבצע באמצעות fileOut מתפריט ההקשר של חלונית הקטגוריות. ייבוא הקובץ לסביבה אחרת מתבצע ע"י גרירת הקובץ אל חלון ה-Squeak ובחירה ב-filein entire file.

חלק IV: פיתוח תוכנה בשפת סמולטוק (פרקים 6–9)

תחביר השפה, פרימיטיבים, קדימויות הודעות, בלוקים ואיטרטורים, מודל העצמים, משתני מופע של מחלקה, Singleton, Traits, כלי הפיתוח וה-Debugger.

6.1–6.2 ליטרלים, מערכים סטטיים/דינמיים, 6 פסאודו-משתנים ופרימיטיבים

אלמנט תחבירי דוגמאות מפתח כללים ודגשים למבחן
משתנים מקומיים מול משותפים מקומי: | x y |, myVar
משותף: Transcript, Color
מקומיים (משתני מופע, פרמטרים של מתודה/בלוק, משתנים זמניים) מתחילים באות קטנה. משותפים (גלובליים, משתני מחלקה, שמות מחלקות, `pool dictionaries`) מתחילים באות גדולה.
מספרים, תווים, מחרוזות וסימבולים 2r101 (=5), 2.4e7, $a, Character tab, 'Mc''Donald', #Hello כדי לכלול גרש בתוך מחרוזת רושמים שתי מרכאות בודדות ''. הערות נכתבות במרכאות כפולות "comment". סימבול (#Hello) מובטח להיות ייחודי בזיכרון (עותק יחיד) ובלתי ניתן לשינוי (Immutable).
מערך סטטי (זמן הידור) מול דינמי (זמן ריצה) סטטי: #(1 'ab' $a)
דינמי: {1. 2. 1+2}
מלכודת מבחן: במערך סטטי #(...) האיברים מופרדים ברווחים ונקבעים בזמן הידור ללא שערוך הודעות! לכן #(10@10 100@100) אינו מכיל 2 נקודות אלא 6 איברים: #(10 #@ 10 100 #@ 100)! במערך דינמי {1. 2. 1+2} הביטויים מופרדים בנקודה ומשוערכים בזמן ריצה ל-#(1 2 3) (חובה רווח אחרי הנקודה ב-1. 2 אחרת יתקבל #(1.2 3)!).
6 פסאודו-משתנים (Pseudo-variables) קבועים: true, false, nil
דינמיים: self, super, thisContext
ערכם נקבע ע"י מערכת זמן הריצה ולא ניתן לבצע להם השמה (:= או _). nil הוא המופע היחיד של UndefinedObject (כל המשתנים מאותחלים ל-nil). thisContext מתייחס למסגרת העליונה של מחסנית זמן הריצה (הקשר הביצוע של המתודה או הבלוק).
פרימיטיבים (Primitives) <primitive: 1> פעולות בסיסיות המבוצעות ישירות ע"י ה-VM (הקצאת זיכרון, מניפולציית בתים, חיבור/השוואה ב-SmallInteger, גישה למערך). מתודה עם <primitive: X> כוללת אחריו קוד Smalltalk אלטרנטיבי שמתבצע רק אם הפעולה הפרימיטיבית נכשלה!

6.3–7.6 ★ 3 סוגי ההודעות, כללי קדימות, מבנה מתודה ושרשור Cascade

סדר הקדימויות בשפת Smalltalk (סעיפים 6.3 ו-7.1–7.4):

  1. 1. סוגריים עגולים ( ... ): משנים את סדר ההערכה (אך בניגוד לסוגריים מרובעים [...] אינם משפיעים על מספר הפעמים שהביטוי מוערך!).
  2. 2. הודעות אונריות (Unary – הקדימות הגבוהה ביותר!): מילה אחת (ללא נקודתיים וללא ארגומנטים), כגון 3 factorial.
  3. 3. הודעות בינריות (Binary – הקדימות השנייה!): תו אחד או יותר מתוך הסימנים המיוחדים (+ - * / \ ~ < > = @ % | & ? ,) עם ארגומנט יחיד.
    ⚠️ כלל זהב: לכל האופרטורים הבינריים יש קדימות זהה והם מוערכים משמאל לימין! (ניתן להעמיס אופרטורים אך לא לשנות את קדימותם או מספר האופרנדים שלהם).
    • דוגמאות חישוב: 3 + 4 * 5 $\implies$ 35 | 2 + 5 * 4 + 3 $\implies$ 31 | 2 raisedTo: 1 + 3 factorial $\implies$ קודם אונרי 3 factorial (=6), אז בינרי 1 + 6 (=7), ולבסוף מילת-מפתח 2 raisedTo: 7 = 128! | 2 * 3 factorial raisedTo: 2 $\implies$ (2 * 6) raisedTo: 2 = 144!
  4. 4. הודעות מילות-מפתח (Keyword – הקדימות הנמוכה ביותר!): מזהה (Selector) המורכב מרצף המילים הארוך ביותר המלוות בנקודתיים : וארגומנט.
    • חשיבות הסוגריים בהודעות מילת-מפתח: בביטוי st := 'hello'. st at: (st indexOf: $e) put: $x (שמחליף את 'e' ב-'x' ומחזיר $x כשהמחרוזת הופכת ל-'hxllo'), ללא הסוגריים המערכת הייתה מנסה לשלוח ל-st הודעה אחת שגויה בשם at:indexOf:put:!
📝 מבנה מתודה ודוגמת `LinkedList>>add:after:`: מתודה כוללת: (1) תבנית המתודה (`method pattern`), (2) הערה `"..."`, (3) הצהרת משתנים זמניים `| savedLink |`, ו-(4) משפטים מופרדים בנקודה `.`:
LinkedList >> add: link after: otherLink
    | savedLink |
    lastLink == otherLink ifTrue: [^ self addLast: link].
    savedLink := otherLink nextLink.
    otherLink nextLink: link.
    link nextLink: savedLink.
    ^ link
🌊 דוגמאות מרכזיות לשימוש ב-Cascade (`;`): שולח סדרת הודעות לאותו מקבל שרשום לפני ה-; הראשונה, ומחזיר את ערך ההודעה האחרונה:
• 3 + 4; + 5 $\implies$ שתיהן נשלחות ל-3 $\implies$ מחזיר 8.
• 2 raisedTo: 2; * 3 factorial $\implies$ המקבל הוא 2; ההודעה השנייה * 6 נשלחת ל-2 (ולא ל-4!) $\implies$ מחזיר 12!
• 10@10 - 10; corner: 30@30; isZero $\implies$ המקבל הוא 10@10; בסוף (10@10) isZero מחזיר false! אבל בלי ה-; הראשונה: 10@10 - 10 corner: 30@30; isZero $\implies$ המקבל של ה-Cascade הוא התוצאה של 10@10 - 10 שהיא הנקודה 0@0, ולכן (0@0) isZero מחזיר true!
• (10@10 + 20; corner: 30@30) area $\implies$ מלבן מ-10@10 עד 30@30 ששטחו 400.
ניתוח ללא שימוש ב-Cascade (בלי ה-;): (10@10 + 20 corner: 30@30) area יוצר מלבן מ-30@30 עד 30@30 ששטחו 0!

6.5–6.6 בלוקים, תנאים, מימוש הלולאות ואיטרטורים

  • בלוקים (`BlockContext`): פונקציות אנונימיות [ :x :y | | n | ... ]. מוערכים ע"י value, value:, value:value: (עד 4 פרמטרים), או valueWithArguments: anArray (למעל 4 פרמטרים). בלוקים יכולים לגשת למשתנים שהוגדרו מחוץ לבלוק, ופקודת החזרה ^ בתוך בלוק גורמת ליציאה מיידית מן המתודה שבה נכתב הבלוק.
  • מימוש מבני בקרה, תנאים ולולאות ב-Smalltalk:
    // 1. מימוש תנאים במחלקות True ו-False:
    True >> ifTrue: trueAlternativeBlock ifFalse: falseAlternativeBlock
        ^ trueAlternativeBlock value
    False >> ifTrue: trueAlternativeBlock ifFalse: falseAlternativeBlock
        ^ falseAlternativeBlock value
    
    // 2. מימוש רקורסיבי של לולאת whileTrue: במחלקה BlockContext:
    BlockContext >> whileTrue: aBlock
        ^ [self value] whileTrue: [aBlock value]
    
    // 3. מימוש timesRepeat: ב-Integer ו-to:do: ב-Number באמצעות whileTrue:
    Integer >> timesRepeat: aBlock
        | count |
        count := 1.
        [count <= self] whileTrue: [aBlock value. count := count + 1]
    
    Number >> to: stop do: aBlock
        | nextValue |
        nextValue := self.
        [nextValue <= stop] whileTrue: [aBlock value: nextValue. nextValue := nextValue + 1]
  • הבחנה בין 1 to: 10 do: [...] לבין (1 to: 10) do: [...]: ללא הסוגריים נשלחת למספר 1 ההודעה to:do:; עם הסוגריים נוצר תחילה עצם מסוג Interval ואליו נשלחת ההודעה do: (תוצאת הביצוע זהה: 55). וב-Interval>>collect: aBlock נעשה שימוש ב-result := self species new: self size כאשר Interval>>species מחזירה את המחלקה Array!

8.1–8.3 ★ מודל העצמים, 3 סוגי עצמים ב-Squeak, כימוס, מתודות מחלקה ו-Dog/Hyena

  • סיווג העצמים ב-Squeak לשלושה סוגים לפי ייצוגם בזיכרון:
    1. עצמים רגילים עם משתני מופע: מועברים באמצעות מצביע (`reference`).
    2. שלמים מסוג SmallInteger: מועברים על פי ערך (`by value`) ונשמרים ישירות בתוך המצביע עצמו (Immediate Value) במקום כעצם נפרד בזיכרון.
    3. עצמים מאונדקסים (`Indexed Objects`): כגון מערכים (`Array`), המתייחסים לרצף זיכרון המכיל אוסף של עצמים.
  • גבולות הכימוס (Encapsulation) ב-Smalltalk מול Java ו-C++:
    • ב-Java ו-C++ גבולות הכימוס מוגדרים בתחום המחלקה (מופע אחד יכול לגשת ישירות למשתנים הפרטיים של מופע אחר מאותה מחלקה).
    • ב-Smalltalk גבולות הכימוס מוגדרים בתחום המופע: משתני המופע נגישים ישירות רק למופע עצמו (ולמתודות בתתי-מחלקות, בדומה ל-protected ב-Java), אך לא למופע אחר מאותה מחלקה!
    • דוגמת יישום (`Rectangle>>containsRect: aRect`): הביטוי ^ aRect origin >= origin and: [aRect corner <= corner] ניגש ישירות למשתני המופע origin ו-corner של המקבל (self), אך חייב לקרוא למתודות הגישה aRect origin ו-aRect corner עבור הארגומנט! היתרון: ניתן להעביר ל-containsRect: מלבן ממימוש אחר לגמרי (שמיוצג ע"י נקודה, רוחב וגובה) כל עוד הוא מספק מתודות גישה origin ו-corner!
    • מתודות ב-Smalltalk: כולן ציבוריות (`public`). שיוך לפרוטוקול private או הוספת קידומת pvt לשם המתודה הם מוסכמה בלבד ואינם חוסמים גישה מחוץ למחלקה.

🎨 ניתוח שליחת הודעה למחלקה (`Color blue`) ושני ההבדלים בין מתודות מחלקה ל-static ב-Java:

• היכן מוגדרת Color blue? המתודה blue מוגדרת ב-Color class (כמתודת מחלקה, ולא ב-Color!). לכן:
  1. Color new blue $\implies$ שגיאה! (`MessageNotUnderstood: Color>>blue`) כי מופע של Color אינו מבין מתודת מחלקה!
  2. Color blue $\implies$ תקין! שולח את ההודעה לעצם המחלקה Color ומחזיר מופע צבע כחול.
  3. Color blue blue $\implies$ שגיאה! כי Color blue מחזיר מופע של Color, ושליחת blue שנייה למופע נכשלת!
• 2 הבדלים מהותיים בין מתודות מחלקה ב-Smalltalk למתודות static ב-Java:
  1. קישור דינמי מול סטטי: ב-Java מתודות static מקושרות בזמן הידור (קישור סטטי), ואילו ב-Smalltalk מתודות מחלקה מקושרות בזמן ריצה (קישור דינמי) ותומכות בירושה ובדריסה מלאה!
  2. הפעלה דרך מופעים: ב-Smalltalk מתודות מחלקה ניתנות להפעלה רק דרך עצם המחלקה ולא דרך מופעים, ואילו ב-Java מתודות static ניתנות להפעלה גם דרך המחלקה וגם דרך מופעים.

🐕 משתני מופע של מחלקה (`class instance variables`): דוגמת `Dog` ו-`Hyena`:

במטה-מחלקה Dog class מוגדר instanceVariableNames: 'count', והמחלקה Hyena יורשת מ-Dog.
• לכל תת-מחלקה נוצר עותק פרטי ונפרד משלה של count (בניגוד ל-static ב-Java/C++ או ל-classVariables ב-Smalltalk שבהם קיים עותק יחיד משותף!). כמו כן, מחלקה אינה יכולה לגשת ישירות למשתני מופע של עצם, ועצם אינו יכול לגשת ישירות למשתני מופע של מחלקה.
• מדוע Hyena new מקדם רק את count של Hyena ולא את של Dog? כי Hyena יורשת את Dog class>>new (שמבצעת count := count + 1. ^ super new). בזמן ריצת Hyena new המקבל (self) הוא Hyena ולכן מקודם ה-count של Hyena, ואילו הקריאה ל-super new מתחילה את החיפוש ממחלקת-העל של המחלקה שבה כתובה המתודה (מחלקת-העל של Dog class שהיא Object class!) ולכן מדלגת על Dog class>>new! (כנ"ל ב-Hyena initialize).

8.3–8.4 ★ דפוס Singleton, מחלקות מופשטות ו-Traits

1. מימוש תבנית היחיד (Singleton) ב-`WebServer`: משתמשים במשתנה מופע של מחלקה uniqueInstance:
WebServer class >> uniqueInstance
    uniqueInstance ifNil: [uniqueInstance := self new].
    ^ uniqueInstance
מדוע self new ולא WebServer new? כדי שאם תת-מחלקה תירש מ-WebServer ותקבל את ההודעה uniqueInstance, ייווצר מופע מסוג תת-המחלקה (self) ולא מסוג WebServer!
2. מחלקות מופשטות ו-Traits: • מתודה מופשטת שולחת self subclassResponsibility (למשל Number>>* aNumber; שרשרת ההפשטה: Magnitude → Number → Integer → SmallInteger הקונקרטית).
• Traits: שיתוף אוספי מתודות בין מחלקות ללא קשר ירושה:
Trait named: #TAuthor uses: {} category: 'SBE-Quinto'
TAuthor >> author  ^ 'on'
// שילוב במחלקה עם מילת המפתח uses:
BorderedMorph subclass: #SBEGame
    uses: TAuthor instanceVariableNames: 'cells' ...

8.5–8.7 ★ איתור מתודות, הגדרה מחדש מול הרחבה, self מול super ומשתנים משותפים

  • הגדרה מחדש (Overriding) מול הרחבה (Extension):
    • הגדרה מחדש (Overriding): EllipseMorph>>defaultColor ^ Color yellow דורסת לחלוטין את Morph>>defaultColor ^ Color blue.
    • הרחבה (Extension): BorderedMorph>>initialize מרחיבה את מתודת האב ע"י קריאה ל-super initialize ואחריה אתחול משתני המופע שלה.

כלל הברזל: שליחה ל-self מול שליחה ל-super ו-doesNotUnderstand::

• self ו-super מתייחסות שתיהן לאותו עצם מקבל בדיוק!
• שליחה ל-self (איתור דינמי): החיפוש מתחיל במחלקה של המקבל (ידועה רק בזמן ריצה).
• שליחה ל-super (איתור סטטי): החיפוש מתחיל במחלקת-העל של המחלקה שבה נמצאת המתודה המבצעת את השליחה ל-super (ידועה מראש בזמן הידור!).
• הודעה שאינה מובנת: אם החיפוש מגיע לראש ההיררכיה (ProtoObject) ללא הצלחה, ה-VM שולחת ל-self את ההודעה doesNotUnderstand: (המוגדרת ב-Object, יוצרת חריג MessageNotUnderstood ופותחת את ה-Debugger; ניתן לדרוס אותה בתת-מחלקה!).
  • 3 סוגי המשתנים המשותפים ב-Smalltalk:
    1. משתנים גלובליים (`Global Variables`): מוגדרים במרחב השמות Smalltalk (מופע של SystemDictionary), כגון: Transcript (מופע של TranscriptStream), Smalltalk, Sensor (מופע של EventSensor), World (מופע של PasteUpMorph), ActiveHand (מופע של HandMorph), Undeclared (מופע של SystemDictionary) ו-SystemOrganization (מופע של SystemOrganizer). הוספת גלובלי חדש: השמה ב-Workspace או Smalltalk at: #Name put: obj (והסרה ב-Smalltalk removeKey: #Name).
    2. משתני מחלקה (`Class Variables`): מוגדרים ב-classVariableNames: (למשל ב-Character: 'CharacterTable ClassificationTable...'), משותפים לעצם המחלקה, לכל מופעיה ולכל תתי-המחלקות שלה, ונגישים ממתודות מופע וממתודות מחלקה. מאותחלים ב-ClassName class>>initialize (יש להפעילה מפורשות כי היא מופעלת אוטומטית רק כשהמחלקה נטענת לראשונה לזיכרון!) או באתחול עצל במתודת גישה (שמונע גישה ישירה ומוסיף תקורה).
    3. מאגרים משותפים (`Pool Dictionaries`): מילוני משתנים המשותפים לקבוצת מחלקות שאינן קשורות בהיררכיית ירושה (כיום מומלץ להגדירם כתת-מחלקות של SharedPool ולציינם תחת poolDictionaries:).

9.1–9.10 ★ כלי הפיתוח ב-Squeak, Monticello ודיבוג המתודה suffix ב-Debugger

  • כפתורי ה-System Browser ו-5 צבעי כפתור inheritance: browse (Ctrl+b) (ובחלונית הקטגוריות מציג back... למחלקות שהוצגו לאחרונה), senders, implementors, versions, hierarchy, vars, source, וכפתור inheritance שצבעו מציין: ורוד = דריסה ללא קריאה ל-super; ירוק = דריסה תוך שימוש ב-super; זהב = מוגדרת מחדש בתת-מחלקה; כתום = ורוד + זהב; סגול = ירוק + זהב.
    • הפעלה תכנותית דרך SystemNavigation default: SystemNavigation default browseClass: EllipseMorph, browseHierarchy: EllipseMorph, ו-browseAllCallsOn: #drawOn:.
  • ניהול מארזים ב-Monticello: מארז SqueakLink כולל: (1) את כל המחלקות בקטגוריה SqueakLink ובקטגוריות המתחילות ב-SqueakLink- (למעט מתודות בפרוטוקולים המתחילים ב-*), ו-(2) כל מתודה בכל מחלקה אחרת (כגון Object>>asSQL) שנמצאת בפרוטוקול ששמו *squeaklink או מתחיל ב-*squeaklink-! כתוצאה מכלל זה (הממומש במחלקה PackageInfo), כל מחלקה וכל מתודה שייכת למארז אחד בלבד. גיבוי מקומי של קובצי .mcz נשמר בתיקייה package-cache.
  • Inspector מול Explorer: ב-Inspector הפריט השני הוא all inst vars ו-self קשור תמיד לעצם הראשי; ב-Explorer המשתנה self בחלונית התחתונה מתקשר דינמית לאיבר/המשתנה הספציפי שנבחר כרגע בעץ!
  • כלי סביבה נוספים: Process Browser (תהליכים רצים), Message Names (חיפוש מתודות ללא פתיחת חלון חדש), Change Set Browser (simple change sorter) ו-Change Sorter (dual change sorter להעברת שינויים בין מקבצי שינויים), File List Browser, ושחזור קוד שהלך לאיבוד מתוך קובץ השינויים (.changes).

🐞 מקרה הבוחן של ה-Debugger: איתור ותיקון 3 הבאגים במתודה `String>>suffix`:

// הגרסה השגויה הראשונית שנכשלת בהרצת 'readme.txt' suffix (זורקת Error: Object is not in the collection):
String >> suffix
    | dot dotPosition |
    dot := FileDirectory dot.   "באג 1: מחזיר את המחרוזת '.' בעוד (self at: i) מחזיר תו $. (ו- $. = '.' הוא false!)"
    dotPosition := (self size to: 1 by: -1) detect: [:i | (self at: i) = dot]. "באג 3: קורס אם אין נקודה במחרוזת!"
    ^ self copyFrom: dotPosition to: self size   "באג 2: מחזיר '.txt' כולל הנקודה במקום 'txt'!"

// הגרסה המתוקנת והמלאה לאחר 3 התיקונים ב-Debugger:
String >> suffix
    | dot dotPosition |
    dot := FileDirectory dot asCharacter.   "תיקון 1: המרה לתו $. "
    dotPosition := (self size to: 1 by: -1) detect: [:i | (self at: i) = dot] ifNone: [self size]. "תיקון 3"
    ^ self copyFrom: (dotPosition + 1) to: self size   "תיקון 2: יחזיר 'txt', ואם אין נקודה יחזיר מחרוזת ריקה ''"

חלק V: מחלקות, ירושה ואוספים ב-Squeak (פרקים 10–12)

5 מרכיבי המחלקה, מחלקות הבסיס (Object, Number, Character, String, Symbol, Boolean) והיררכיית האוספים (Collections).

10.1–10.6 המושג "מחלקה" (Class): 3 תפקידים, 5 מרכיבים ובבואה (Reflection)

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

💡 שאלת ניתוח מעמיקה: האם ההתנהגות של כל העצמים מאותה מחלקה חייבת בהכרח להיות זהה?
תשובה: כן ולא! ככלל, בשפות מונחות עצמים מקובל שכל העצמים מאותה מחלקה מתנהגים בהתאם למוגדר במחלקה, אבל אפשרי בהחלט לתכנן מחלקה שבה כל עצם קובע לעצמו מימוש שונה של המתודות! לדוגמה, ניתן לממש מחסנית class Stack ב-C++ שבה void (*push)(int element); ו-int (*pop)(void); מוגדרים כמצביעים לפונקציה (Function Pointers) במשתני המופע במקום הגדרת הפונקציה עצמה!
🎯 3 התפקידים של מחלקה:
  1. תבנית ליצירת עצמים חדשים (למשל ביטוי new המקבל את שם המחלקה).
  2. הצהרה על הפרוטוקול שהעצמים "מבינים" (משמש שפות עם הטפסה סטטית לאיתור שגיאות בזמן הידור).
  3. ישות בפני עצמה בזמן ריצה (מיוצגת ע"י עצם ב-Smalltalk וב-Java Reflection).
🧱 5 המרכיבים של מחלקה:
  • 1. כור יצירה / מכרה עצמים (`Forge`): אוסף ההודעות ליצירת עצמים חדשים (חתימות הבנאים).
  • 2. בית-חרושת לייצור עצמים (`Mill`): המימוש של הבנאים.
  • 3. פרוטוקול (`Protocol`): אוסף ההודעות שהעצמים מבינים (חתימת המתודות).
  • 4. התנהגות (`Behavior`): המימוש של המתודות.
  • 5. מבנה (`Structure`): אוסף משתני המופע (מבנה הזיכרון של כל עצם).
דוגמה והמחשה בשפות שונות אילו מ-5 מרכיבי המחלקה קיימים בה? יצירת עצמים חדשים
10.5.1 מנשק ב-Java (interface printable) מגדיר את הפרוטוקול (`Protocol`) בלבד, אך לא את ההתנהגות ולא את המבנה. אינו מאפשר ליצור עצמים חדשים
10.5.2 מחלקה אנונימית ב-Java (new printable { ... }) כוללת מבנה (private int s;) והתנהגות (void print()), אך חסרת `Forge` ליצירת מופעים נוספים. נוצר מופע יחיד בלבד; אינה מאפשרת ליצור עצמים נוספים
10.5.3 מחלקה מופשטת ב-++C (virtual void draw() = 0;) ניתן להגדיר בה פרוטוקול, מבנה, מתודות ובנאים (עבור תתי-מחלקות). אינה מאפשרת ליצור עצמים חדשים ישירות
10.5.4 מחלקה ב-Smalltalk (המחלקה Point) מבנה (instanceVariableNames: 'x y'), מתודות-מופע (setX:setY:, x, y) ומתודות-מחלקה ליצירה: Point class>>x: xInteger y: yInteger ^ self basicNew setX: xInteger setY: yInteger. כן – המחלקה עצמה היא עצם המגיב להודעות יצירה!
🪞 בבואה (Reflection) ב-Java: לדוגמה, פונקציית השמה דינמית assignField(Object o, String fieldName, Object value) מבצעת: Class c = o.getClass(); Field f = c.getField(fieldName); f.set(o, value);.
• האם getClass() מחזירה עצם? המתודה o.getClass() אכן מחזירה עצם לכל דבר (מופע של המחלקה java.lang.Class), ולכן ניתן לקרוא עליו למתודה c.getField(...) וגם לקרוא עליו שוב ושוב ל-getClass()!
• שרשור קריאות getClass(): מה מחזירה הקריאה o.getClass().getClass().getClass().getClass(); ב-Java? היא מחזירה תמיד את העצם המייצג את המחלקה java.lang.Class! הסיבה לכך היא שב-Java (מערכת בת 3 רמות בלבד, כמפורט בפרק 18) כל עצם-מחלקה הוא מופע של המחלקה Class, והמחלקה Class היא מופע של עצמה (Class.class.getClass() == Class.class)!

11.1 המחלקה Object (הדפסה, שוויון ו-hash, שייכות, העתקה וניפוי שגיאות)

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

1. מתודות הדפסה (`printString`, `printOn: aStream`) ועצמים מסוג `self evaluating`: • ב-Object>>printOn: aStream נכתב לזרם 'an ' (אם שם המחלקה מתחיל בתנועה title first isVowel) או 'a ' ואחריו שם המחלקה (באמצעות nextPutAll:).
• ב-Point>>printOn: aStream מוגדר:
Point >> printOn: aStream
    x printOn: aStream.
    aStream nextPut: $@.
    (y isNumber and: [y negative]) ifTrue: [aStream space].   "שורות 3-4"
    y printOn: aStream
• שאלת הבנה (מדוע קיימות שורות 3–4 ב-Point>>printOn:?): אם שיעור ה-y הוא מספר שלילי (למשל בנקודה 3 @ -4), ללא הוספת הרווח (aStream space) היה מודפס הרצף 3@-4, והמפענח של Smalltalk היה מפרש את שני התווים הצמודים @- כאופרטור בינרי אחד לא-מוגדר במקום האופרטור @ ואחריו המספר השלילי -4!
• קדימות אופרטורים בהדפסה (השמטת סוגריים ב-3@4 printString): הביטוי (3@4) printString מחזיר את המחרוזת '3@4', אך ללא סוגריים 3@4 printString מריץ קודם את ההודעה האונארית 4 printString (שמחזירה '4') ואז יוצר עצם Point ששיעור ה-y שלו הוא מחרוזת: 3@'4'!
• עצמים שהם `self evaluating`: המערך הדינמי {3@4. 4.5} מדפיס {3@4 . 4.5} ולכן הוא self evaluating, ואילו {3@4. 4+5} מדפיס {3@4 . 9} ולכן ביטוי המקור אינו זהה לייצוג המודפס.
2. זהות (`==`) מול שוויון (`=`), אי-שוויון (`~=`) והמתודה `hash`: • ב-Object המתודה = ממומשת כבדיקת זהות: ^ self == anObject (ואי-שוויון ~= מחזיר את שלילת =).
• ב-Point>>= aPoint נבדק שוויון הסוג והערכים: self species = aPoint species ifTrue: [^ x = aPoint x and: [y = aPoint y]] ifFalse: [^ false].
• חוק ברזל למבחן: אם מגדירים מחדש את =, חובה להגדיר מחדש גם את hash כך ששני עצמים שווים (a = b) יחזירו תמיד אותו מספר hash!
3. שייכות למחלקה (`class`, `isMemberOf:`, `isKindOf:`, `respondsTo:`): • 4 isMemberOf: SmallInteger $\implies$ true, אך 4 isMemberOf: Number $\implies$ false (בודקת אם העצם הוא מופע בדיוק של המחלקה הנתונה).
• 4 isKindOf: Number וגם 4 isKindOf: Object $\implies$ true (בודקת אם העצם הוא מסוג הארגומנט או כל תת-מחלקה שלו).
• 4 respondsTo: #+ וגם 'hello' respondsTo: #size $\implies$ true.
4. העתקה ושכפול (`shallowCopy`, `copyTwoLevel`, `deepCopy`, `copy` + `postCopy`): • עבור מערך דו-ממדי a1 := {{'a'. 'b'}. {'c'. 'd'}}: ב-a2 := a1 shallowCopy שינוי תת-מערך (a1 at: 1) at: 1 put: 'z' ישנה גם את a2 כי תתי-המערכים משותפים! ב-a2 := a1 copyTwoLevel שינוי במערך דו-ממדי לא ישפיע על a2, אך במערך תלת-ממדי {{{'a'}}...} שינוי ברמה השלישית עדיין ישפיע על שניהם (ולכך נדרשת deepCopy).
• המתודה Object>>copy מבצעת shallowCopy ואז שולחת לעותק את ההודעה postCopy (תתי-מחלקות דורסות את postCopy להשלמת השכפול מעבר לרדוד).
5. ניפוי שגיאות, חוזים, בדיקות ואתחול: • self halt (פתיחת ה-Debugger), self assert: [aBlock] (תכנות לפי חוזה Design by Contract – זורק AssertionFailure אם התנאי false), self deprecated: 'use ifNotNilDo:' (כמו ב-doIfNotNil:), doesNotUnderstand:, self error: 'msg', self subclassResponsibility, ו-self shouldNotImplement (לביטול מתודה שנורשה אך אינה מתאימה לתת-המחלקה).
• מתודות בדיקה כגון isArray, isBoolean, isBlock אינן מומלצות לשימוש כי הן מפרות את עקרון הכימוס, למעט ProtoObject>>isNil ו-Object>>notNil השימושיות. מתודת האתחול initialize מוגדרת ב-ProtoObject ונקראת אוטומטית מתוך new.

11.2–11.5 מספרים, תווים, מחרוזות, Symbol ובוליאניים

🔢 11.2 מספרים (`Magnitude → Number`) ו-11.3 תווים (`Character`): • ב-Magnitude (מופשטת) האופרטורים < ו-= מוגדרים כמתודות מופשטות ומשמשים למימוש כל השאר (<=, >, >=, between:and:, min:, max:).
• ב-Number האופרטורים +, -, *, / הם מופשטים. // מבצע חלוקת שלמים מעוגלת כלפי מטה, ו-\\ מחזיר את השארית.
• תחת Number יורשות: Float (עם מתודות מחלקה לקבועים Float pi ו-Float e), Fraction (נוצר ב-Fraction numerator:denominator: או ע"י / בין שלמים ונשמר תמיד בצורה מצומצמת: 6/8 → (3/4)), ו-Integer (מופשטת, עם factorial, atRandom, isPrime, gcd: ו-3 תתי-המחלקות SmallInteger, LargePositiveInteger, LargeNegativeInteger המומרות אוטומטית זו לזו!).
• Character (יורשת מ-Magnitude): נוצרת עם $a, Character tab / cr / space / backspace / escape / enter או Character value: asciiCode.
🔤 11.4 מחרוזות (`ByteString` מול `Symbol`) ו-11.5 בוליאניים: • היררכיית המחרוזות: Object → Collection → SequenceableCollection → ArrayedCollection → String (מופשטת) → ByteString / Symbol.
• השוואת ByteString מול Symbol: עבור s1 := 'hello'. s2 := 'hello': המחרוזות שוות (s1 = s2 הוא true), אך הן מופעים נפרדים בזיכרון (s1 == s2 הוא false), וביצוע s1 at: 1 put: $H או s1 := s1, ' world' משנה רק את s1 מבלי להשפיע על s2! לעומת זאת, שני Symbol זהים (#hello) הם תמיד אותו עצם בזיכרון (==) ואינם ניתנים לשינוי (`Immutable`)!
• מתודות String חשובות: שרשור עם פסיק ,, replaceAll:with:, replaceFrom:to:with: (משנות במקום), copyReplaceAll:with: (יוצרת עותק חדש), findString:, ו-'123+12' asNumber → 123!
• Boolean → True / False: false ifTrue: [...] מחזיר nil, ו-true ifFalse: [...] מחזיר nil. ב-and: ו-or: כאשר אין צורך להעריך את הבלוק מוחזר self.

12.1–12.4 ★ אוספים ב-Squeak: היררכיית Collection, 8 סיווגי יכולות ו-5 דרכי מימוש

  • 7 הפרוטוקולים המשותפים ב-`Collection`: (1) גישה: size, capacity, at:, at:put: | (2) בדיקה: isEmpty, includes:, contains:, occurrencesOf: | (3) הוספה: add:, addAll: | (4) הסרה: remove:, remove:ifAbsent:, removeAll: | (5) אנומרציה: do:, collect:, select:, reject:, detect:, detect:ifNone:, inject:into:, count:, anySatisfy: | (6) הסבה: asBag, asSet, asOrderedCollection, asSortedCollection, asSortedCollection:, asArray | (7) יצירה (מתודות מחלקה): new, new:, with:, with:with: (עד 4 איברים), withAll:.
  • 5 קטגוריות המימוש של אוספים: (1) מימושי מערך (Array – מאחסן איברים במשתנה מופע מאונדקס של האוסף ולכן גודלו קבוע), (2) מימושים סדורים (OrderedCollection, SortedCollection – מאחסנים איברים במערך פנימי השמור באחד ממשתני המופע של האוסף, כך שניתן להקצות מערך גדול יותר לפי הצורך!), (3) מימושי Hash (Set, Dictionary – משתמשים במערך כטבלת גיבוב; ו-Bag שמשתמש בפנים ב-Dictionary שבו האלמנטים הם המפתחות ומספר המופעים הוא הערכים!), (4) מימושים מקושרים (LinkedList – רשימה מקושרת סטנדרטית), (5) מימושי אינטרוול (Interval – ממומש באמצעות 3 שלמים בלבד: start, stop, step).
מחלקת אוסף סדור / גישה דינמי (Growable)? כפילויות? דרכי יצירה ומתודות ייחודיות
Array אינדקס 1..n לא (גודל קבוע) כן (הטרוגני; יש גם הומוגניים ByteArray, IntegerArray, FloatArray) Array new: 5 ($\to$ 5 איברי nil), Array with: 1 with: 2, #(...), {...}. גישה ב-at: ו-at:put:.
OrderedCollection לפי סדר הכנסה כן כן add: (מוסיף בסוף), addFirst:, addLast:, insert:before:, at:, at:put:, remove: (מסיר את המופע הראשון שמתאים!).
SortedCollection ממוין (<= או sortBlock:) כן כן SortedCollection sortBlock: [:s1 :s2 | s1 area <= s2 area].
Interval אינדקס at: (אך Immutable!) לא לא Interval from: 1 to: 10 by: 2 או 1 to: 10 by: 2. תומך ב-at: אך אסור לבצע עליו at:put:!
Dictionary / IdentityDictionary לפי מפתח (Keyed) כן (Hash) מפתחות ייחודיים, ערכים חופשיים זוגות Association (#one->1). יצירה עם Dictionary newFrom: {#one->1} או Dictionary withAll: #(7 3 1 3) (מצמיד מפתחות סידוריים 1..4!). מתודות: at:put:, at:ifAbsent:, removeKey:, keys (מחזיר Set!), values (מחזיר Array!), associations. IdentityDictionary משווה מפתחות ב-== (יעיל ל-Symbol).
Set לא (אין at:!) כן (Hash) לא (מנפה כפילויות: add: 3; add: 9/3 שומר איבר אחד!) Set newFrom: #(1 2 2 3), union:, intersection:, includes:.
Bag לא כן כן (קבוצה עם חזרות) ממומש ע"י Dictionary (איבר $\to$ מונה מופעים occurrencesOf:).

12.5–12.6 ★ מימוש כל האיטרטורים במחלקה Collection ו-4 כללי הזהב

// מימוש איטרטורים מרכזיים באוספים (do:, collect:, select:, reject:, detect:, inject:into:):
SequenceableCollection >> do: aBlock
    1 to: self size do: [:index | aBlock value: (self at: index)]
// גרסאות נוספות של do: -> doWithout:, withIndexDo:, reverseDo:, ובמילון: keysDo:, valuesDo:, associationsDo: (במילון do: סורקת את הערכים values!)

Collection >> collect: aBlock
    | newCollection |
    newCollection := self species new.
    self do: [:each | newCollection add: (aBlock value: each)].
    ^ newCollection
// זהירות: collect: על String תחזיר String ולכן הבלוק חייב להחזיר תווים (Character), אחרת תיזרק שגיאה!

Collection >> select: aBlock
    | newCollection |
    newCollection := self species new.
    self do: [:each | (aBlock value: each) ifTrue: [newCollection add: each]].
    ^ newCollection

Collection >> reject: aBlock
    ^ self select: [:element | (aBlock value: element) == false]

Collection >> detect: aBlock
    ^ self detect: aBlock ifNone: [self errorNotFound: aBlock]

Collection >> detect: aBlock ifNone: exceptionBlock
    self do: [:each | (aBlock value: each) ifTrue: [^ each]].
    ^ exceptionBlock value

Collection >> inject: thisValue into: binaryBlock
    | nextValue |
    nextValue := thisValue.
    self do: [:each | nextValue := binaryBlock value: nextValue value: each].
    ^ nextValue
// הודעות איטרציה נוספות: count: [:each | ...] ו-anySatisfy: [:each | ...]

⚠️ 4 הערות שימושיות וכללי מפתח לעבודה עם אוספים:

  1. המתודה add: מחזירה את האיבר שהתקבל להוספה ולא את האוסף! (OrderedCollection new add: 1 מחזיר 1). לקבלת האוסף שולחים ; yourself.
  2. אסור להסיר איבר מאוסף בזמן שמבצעים עליו איטרציה! במקום זאת משכפלים את האוסף באמצעות copy ומבצעים את האיטרציה על העותק המשוכפל.
  3. הגדרת = מחייבת הגדרת hash: אוספים משתמשים ב-= וב-hash לבדיקת שוויון בין איברים.
  4. עדיף להשתמש בעצמים `Immutable` כמפתחות במילונים וכאיברים בקבוצות (`Set`): כי ערך ה-hash של עצמים `Mutable` עלול להשתנות במהלך חייהם, ואז שני עצמים שווים עלולים להיחשב כשונים.

חלק VI: סימולציה – חידת המלכות והסביבה הגרפית Morphic (פרקים 13–17)

ייצוגים מתמטיים וגרפיים לחידת N המלכות, הפתרון המבוזר ב-Smalltalk, הכללה ל-N×N×N וממשק Morphic.

13–14.4 ★ רקע, ייצוג פרמוטציה ו-4 אופני הייצוג של חידת N המלכות

  • הגדרת בעיית 8 המלכות ועקרונות הסימולציה:
    • הגדרת חידת 8 המלכות: הצב 8 מלכות על לוח שחמט כך שאף מלכה אינה מאיימת על אף מלכה אחרת (אין שתי מלכות באותה שורה, באותה עמודה או באותו אלכסון).
    • רקע היסטורי: הוצגה ב-1848 ע"י שחקן השחמט מקס בזל (Max Bezzel), ונחקרה ע"י גאוס (C.F. Gauss, 1865), פוליה (G. Polya, 1918) ולוקאס (E. Lucas, 1960). במדעי המחשב היא דוגמה קלאסית לאלגוריתמי Backtracking ולסימולציה בעזרת עצמים (הגישה שהולידה את שפת Simula).
    • ייצוג פתרון כפרמוטציה: מכיוון שבכל עמודה ובכל שורה יש בדיוק מלכה אחת, ניתן לייצג פתרון כפרמוטציה מעל $\{0, 1, 2, 3, 4, 5, 6, 7\}$, למשל הפתרון החוקי: $$\begin{pmatrix} 0 & 1 & 2 & 3 & 4 & 5 & 6 & 7 \\ 7 & 3 & 0 & 2 & 5 & 1 & 6 & 4 \end{pmatrix}$$
1. ייצוג אלגברי יהא $A = \{0, 1, 2, \dots, N-1\}$ ויהא $B = A \times A = \{(0,0), (0,1), \dots, (N-1, N-1)\}$. תת-קבוצה $C \subseteq B$ של $N$ איברים היא פתרון לחידת המלכות אם לכל $Q_k(i_k, j_k), Q_l(i_l, j_l) \in C$ (כאשר $k \ne l$) מתקיימים כל 4 התנאים:
1. לא באותה עמודה: $i_k \ne i_l$
2. לא באותה שורה: $j_k \ne j_l$
3. לא באותו אלכסון: $i_k + j_k \ne i_l + j_l$
4. לא באותו אלכסון נגדי: $i_k - j_k \ne i_l - j_l$
2. ייצוג בחשבון מודולרי יהא $A = \{0, 1, 2, \dots, N^2-1\}$. תת-קבוצה $B \subseteq A$ של $N$ איברים היא פתרון אם לכל $x, y \in B$ כך ש-$x \ne y$ מתקיימים:
1. לא באותה עמודה: $x \not\equiv y \pmod N$
2. לא באותה שורה: $\lfloor x/N \rfloor \ne \lfloor y/N \rfloor$
3. לא באותו אלכסון: $(x \bmod N) + \lfloor x/N \rfloor \ne (y \bmod N) + \lfloor y/N \rfloor$
4. לא באותו אלכסון נגדי: $|(x \bmod N) - \lfloor x/N \rfloor| \ne |(y \bmod N) - \lfloor y/N \rfloor|$
3. קבוצה יציבה מקסימלית – Maximum Stable Set (14.3) בגרף $G=(V,E)$, קבוצת צמתים $V' \subseteq V$ היא יציבה (stable) אם אין ב-$E$ אף קשת המחברת שני צמתים ב-$V'$.
• נייצג כל משבצת בלוח כצומת ב-$V$ ($|V|=N^2$), ותהא ב-$E$ קשת בין כל שני צמתים שנמצאים על אותה עמודה, אותה שורה או אותו אלכסון (יחס איום).
• פתרון: כל קבוצה יציבה $V' \subseteq V$ בגודל $\sqrt{|V|} = N$.
4. קליק מקסימלי – Maximum Clique (14.4) בגרף $G=(V,E)$, קבוצת צמתים $V' \subseteq V$ היא קליק (clique) אם בין כל שני צמתים ב-$V'$ יש קשת ב-$E$ המחברת אותם.
• נייצג כל משבצת כצומת ב-$V$, ותהא ב-$E$ קשת בין כל שני צמתים שאינם נמצאים על אותה עמודה, אותה שורה או אותו אלכסון (יחס אי-איום, הגרף המשלים ל-14.3).
• פתרון: כל קליק $V' \subseteq V$ בגודל $\sqrt{|V|} = N$ ($\binom{N}{2}$ קשתות).

14.5–14.6 ★ 8 סימטריות הלוח ופתרונות זרים ממש

  • 8 סיבובי הסימטריה היוצרים פתרונות שקולים איזומורפית:
    • 4 סיבובים דו-ממדיים נגד כיוון השעון: ב-$90^\circ$, ב-$180^\circ$, ב-$270^\circ$, וב-$360^\circ$ (זהות עצמית).
    • 4 סיבובים תלת-ממדיים של $180^\circ$ במרחב (שיקופים): סביב הציר האופקי, סביב הציר האנכי, סביב הציר האלכסוני ($i \ne j$), וסביב הציר האלכסוני הנגדי ($i = j$).
N (Board Size) 1 2 3 4 5 6 7 8 9 10
סך כל הפתרונות עבור N 1 0 0 2 10 4 40 92 352 724
פתרונות זרים ממש (עצמאיים) 1 0 0 1 2 1 6 12 46 92

* מספר הפתרונות הזרים ממש: עבור $N = 1, 2, 3, 4, 5, 6, 7, 8$ מספר הפתרונות הזרים ממש (שאינם שקולים איזומורפית תחת 8 הסימטריות) הוא 1, 0, 0, 1, 2, 1, 6, 12 בהתאמה.

15.1 ★ פתרון מונחה עצמים לחידת 8 המלכות: המחלקה Board ושרשרת המלכות

בפתרון מונחה העצמים (פרק 15), לכל מלכה מוקצית עמודה כנחלתה הבלעדית בלוח, והמלכות דנות ביניהן באמצעות שליחת הודעות רק לגבי השורה (row) שכל אחת תתפוס. המחלקה Board מחזיקה משתנה מופע יחיד בלבד (lastQueen) המצביע לראש רשימה מקושרת של 8 מלכות שבסופה מלכה מנוונת (QueenTerminator):

// הגדרת המחלקה Board, אתחול שרשרת המלכות והמתודה solve:
Object subclass: #Board
    instanceVariableNames: 'lastQueen'
    classVariableNames: '' poolDictionaries: '' category: 'OOD-8Queens'

Board >> initialize
    lastQueen := QueenTerminator new.
    (1 to: 8) do: [:i |
        lastQueen := Queen new setColumn: i neighbor: lastQueen]

Board >> solve
    Transcript clear.
    lastQueen firstSolution
        ifTrue: [
            Transcript show: lastQueen result printString; cr.
            [lastQueen nextSolution] whileTrue: [
                Transcript show: lastQueen result printString; cr].
            Transcript show: 'End of Solutions.'; cr]
        ifFalse: [
            Transcript show: 'No Solution.'; cr]

// הרצה ופלט:
// board := Board new initialize. board solve
// OrderedCollection (1 5 8 6 3 7 2 4) ... (92 פתרונות)

15.2–15.4 ★ המחלקה Queen: שלושת משתני המופע ו-7 מתודות הפרוטוקול

למחלקה Queen יש 3 משתני מופע: row (השורה הנוכחית 1..8), column (מספר העמודה הקבוע 1..8), ו-neighbor (המצביע הקבוע למלכה השכנה משמאל).

Object subclass: #Queen
    instanceVariableNames: 'row column neighbor'
    classVariableNames: '' poolDictionaries: '' category: 'OOD-8Queens'

// --- 15.3 ארבע מתודות הפרוטוקול החיצוני ---
Queen >> setColumn: aColumn neighbor: aQueen
    column := aColumn.
    neighbor := aQueen

Queen >> firstSolution
    neighbor firstSolution.
    row := 1.
    ^ self moveToNextNonAttackedRow

Queen >> nextSolution
    ^ self advance and: [self moveToNextNonAttackedRow]

Queen >> result
    "add row to the list"
    ^ neighbor result addLast: row; yourself

// --- 15.4 שלוש מתודות התקשורת הפנימית ("פרטיות") בין המלכות ---
Queen >> advance
    row = 8
        ifTrue: [
            neighbor nextSolution ifFalse: [^ false].
            row := 0].
    row := row + 1.
    ^ true

Queen >> moveToNextNonAttackedRow
    [neighbor canAttack: row x: column]
        whileTrue: [self advance ifFalse: [^ false]].
    ^ true

Queen >> canAttack: testRow x: testColumn
    | columnDifference |
    columnDifference := testColumn - column.
    ((row = testRow
        or: [row + columnDifference = testRow])
        or: [row - columnDifference = testRow])
            ifTrue: [^ true].
    ^ neighbor canAttack: testRow x: testColumn

🔍 4 דגשים קריטיים על מתודות Queen למבחן:

  • ב-canAttack:x:: לא בודקים column = testColumn כי לכל מלכה הוקצתה עמודה ייחודית באתחול! נבדקים רק 3 תנאים: אותה שורה (row = testRow), אלכסון עולה (row + columnDifference = testRow) ואלכסון יורד (row - columnDifference = testRow).
  • ב-advance: כאשר row = 8 המלכה מבקשת משכנתה neighbor nextSolution (Backtracking!), מאפסת row := 0 ואז מקדמת row := row + 1 כך שהיא מתמקמת בשורה 1 מול הסידור החדש של שכנותיה.
  • ב-result: חובה לסיים ב-; yourself כי addLast: row מחזירה את row ולא את ה-OrderedCollection!
  • ב-nextSolution: השימוש ב-and: [...] העצל מבטיח שאם self advance החזיר false (אין יותר פתרונות), החיפוש ייעצר מיד.

15.5 ★ הגדרת מלכה מנוונת: המחלקה QueenTerminator (תבנית Null Object)

במקום לסרבל את המימוש בתנאי עצירה (בדיקות neighbor isNil), מגדירים את QueenTerminator המשמשת כשכנה של המלכה הראשונה (בעמודה 1) ובעלת אותו פרוטוקול:

Object subclass: #QueenTerminator
    instanceVariableNames: '' classVariableNames: '' poolDictionaries: '' category: 'OOD-8Queens'

QueenTerminator >> canAttack: testRow x: testColumn
    ^ false                        "תמיד מחזיר false (אינה מאיימת על אף משבצת)"

QueenTerminator >> firstSolution
    ^ true                         "תמיד מחזיר true"

QueenTerminator >> nextSolution
    ^ false                        "תמיד מחזיר false (עוצר את החיפוש כשמלכה 1 מיצתה את שורה 8)"

QueenTerminator >> result
    ^ OrderedCollection new        "מחזיר רשימה ריקה חדשה"
// המתודות setColumn:neighbour:, advance ו-moveToNextNonAttackedRow אינן בשימוש במלכה המנוונת!

16.1–16.2 ★ הכללה ללוח N×N (מלכודת משתנה המחלקה N!) וללוח תלת-ממדי N×N×N

הכללה ל-$N$ מלכות על לוח $N \times N$: הצב $N$ מלכות על לוח שחמט $N \times N$ כך שאף מלכה אינה מאיימת על מלכה אחרת. בפתרון מונחה העצמים הקבוע 8 מופיע ב-2 מקומות בלבד: בלולאת 1 to: 8 ב-Board>>initialize ובבדיקה row = 8 ב-Queen>>advance.

🚨 שאלת הבחינה הקלאסית: מלכודת משתנה המחלקה N בהכללת הלוח ל-$N \times N$!

אם נגדיר ב-Board משתנה מחלקה משותף classVariableNames: 'N' ומתודת מחלקה Board class>>N ^ N, וב-Queen>>advance נבדוק row = Board N, מה יקרה בהרצת הקוד הבא?

board1 := Board new.
board2 := Board new.
board1 initialize: 5.   "קובע N := 5 ויוצר ב-board1 שרשרת של 5 מלכות"
board2 initialize: 1.   "דורס את משתנה המחלקה המשותף וקובע N := 1!"
board1 solve.           "ידפיס: No Solution. !"

הסבר: משתנה מחלקה N משותף לכל מופעי Board! הקריאה board2 initialize: 1 דרסה את N ל-1, ולכן כשמריצים board1 solve, 5 המלכות של board1 בודקות row = Board N מול 1, לא מתקדמות מעבר לשורה 1, ואי אפשר להציב 5 מלכות בשורה אחת בלבד!
הפתרון התקני: לשמור את גודל הלוח כמשתנה מופע פרטי (boardSize) ב-Board ובכל Queen!

🧊 חידת המלכות המרחבית – לוח שחמט תלת-ממדי $N \times N \times N$:

הגדרת הבעיה התלת-ממדית: הצב $N^2$ מלכות על לוח שחמט תלת-ממדי $N \times N \times N$ כך שבכל אחד מהמישורים אף מלכה אינה מאיימת על מלכה אחרת. עבור שתי מלכות $Q_1(i, j, k)$ ו-$Q_2(\text{row}, \text{col}, \text{lev})$, הקוד ב-Smalltalk המזהה איום מרחבי:

dx := (row - i) abs.
dy := (col - j) abs.
dz := (lev - k) abs.
((dx = 0) and: [ dy = 0 ]) ifTrue: [ ^ true ].   "1. איום לאורך ציר המפלסים (אותה שורה ואותה עמודה)"
((dx = 0) and: [ dy = dz ]) ifTrue: [ ^ true ].  "2. איום אלכסוני במישור המקביל לצירים y-z (אותה שורה)"
((dx = dz) and: [ dy = 0 ]) ifTrue: [ ^ true ].  "3. איום אלכסוני במישור המקביל לצירים x-z (אותה עמודה)"
((dx = dy) and: [ dx = dz ]) ifTrue: [ ^ true ]  "4. איום באלכסון מרחבי תלת-ממדי או במישור x-y (כאשר dz=0)"

* מימוש פתרון לחידה התלת-ממדית ב-Smalltalk: מכיוון שיש להציב $N^2$ מלכות על לוח $N \times N \times N$, מקצים לכל מלכה זוג קבוע של (row, column) (סה"כ $N^2$ מלכות בשרשרת מקושרת מהמלכה $(N,N)$ ועד QueenTerminator), והמלכות מנהלות משא ומתן על בחירת המפלס lev ($1 \dots N$) תוך בדיקת אי-איום באמצעות 4 תנאי האיום המרחביים שהוגדרו לעיל!

17.1–17.3 הסביבה הגרפית Morphic: ישירות, חיות, מערכת הצירים והרכבת צורות

  • שני עקרונות היסוד של סביבת Morphic:
    1. ישירוּת (`directness`): הצורות על המסך הן עצמים שאפשר לבחון ולשנותם בצורה ישירה. הדגמת ניתוק פריט מתפריט העולם: לוחצים בכפתור הכחול (האמצעי) מעל תפריט העולם לקבלת ה-morphic halo של התפריט $\to$ לוחצים שוב בכפתור הכחול מעל אחד מפריטי התפריט להקפתו ב-Halo משלו $\to$ גוררים אותו החוצה באמצעות צלמית ה-pickup (באמצע השורה העליונה), והפריט מתפקד ככפתור עצמאי!
    2. חיוּת (`liveness`): הסביבה מגיבה אל המשתמש והמידע על המסך מתעדכן בהתאם בזמן אמת.
  • הפיכת עצמים וצבעים ל-Morph וחלון ה-Viewer:
    • s := 'Hello' asMorph openInWorld. s openViewerForArgument. פותח בצד ימין את חלון ה-Viewer המאפשר להזיז ולסובב את העצם בלחיצה על סימני הקריאה ! (ולגרום לו "לצחוק" באמצעות שליחת הפקודה הקולית ב-Viewer!).
    • Color>>asMorph ממומשת עם Cascade ו-yourself: ^ (RectangleMorph new) fillStyle: self; borderWidth: 0; yourself.
  • מערכת הצירים, מתודות בסיסיות ובניית צורות מורכבות: ראשית הצירים (0@0) נמצאת בפינה השמאלית העליונה של המסך; ציר x מתקדם ימינה וציר y מתקדם כלפי מטה. מתודות: position / position: (פינה שמאלית-עליונה של המלבן החוסם), extent / extent: (רוחב@גובה), color:.
    // יצירת צורה צהובה וצורה ירוקה מסוג Morph:
    mYellow := Morph new color: Color yellow; openInWorld.
    mGreen := Morph new color: Color green; openInWorld.
    10 timesRepeat: [mYellow position: mYellow position + (10@0)].    "הזזה ימינה 10 פיקסלים 10 פעמים"
    10 timesRepeat: [mGreen position: mGreen position + (10@10)].     "הזזה באלכסון למטה וימינה 10 פעמים"
    mGreen extent: mYellow extent.                                    "השוואת גודל הירוקה לצהובה"
    mYellow color: Color blue.                                        "שינוי צבע הצהובה לכחול"
  • הרכבת צורות (`Composition`):
    • בקוד: rect addMorph: star מכניסה את הכוכב לתוך המלבן; הזזת המלבן (rect position: ...) מזיזה את שניהם יחדיו, והזזת הכוכב ישירות מזיזה אותו ביחס למלבן אך הוא ממשיך לזוז עם המלבן! star center: rect center ממרכזת אותו (בפרוטוקול geometry), ו-rect removeMorph: star מנתקת אותו (נעלם מהמסך אך קיים בזיכרון ומוצג שוב עם star openInWorld).
    • בממשק החזותי: מתפריט העולם objects (o) → Graphics גוררים שתי צורות, מניחים זו על זו, לוחצים בכפתור הצהוב (ימני) על העליונה ובוחרים embed into. לניתוק: שתי לחיצות בכפתור הכחול (הראשונה מעלה Halo למכילה, השנייה למוכלת) וגרירתה החוצה.

17.4 ★ יצירת צורות גרפיות: CrossMorph, drawOn:, containsPoint: ושני השכתובים

כדי ליצור את המחלקה CrossMorph (היורשת מ-Morph), משכתבים את drawOn: aCanvas (הנעזרת ב-self bounds insetBy: להסטת פינות המלבן החוסם פנימה) ואת containsPoint: aPoint (כדי שרק נקודות על הצלב עצמו יגיבו לעכבר ולא 4 הפינות הריקות של המלבן החוסם):

CrossMorph >> drawOn: aCanvas
    | crossHeight crossWidth horizontalBar verticalBar |
    crossHeight := self height / 3.0.
    crossWidth := self width / 3.0.
    horizontalBar := self bounds insetBy: 0 @ crossHeight.
    verticalBar := self bounds insetBy: crossWidth @ 0.
    aCanvas fillRectangle: horizontalBar color: self color.
    aCanvas fillRectangle: verticalBar color: self color

CrossMorph >> containsPoint: aPoint
    | crossHeight crossWidth horizontalBar verticalBar |
    crossHeight := self height / 3.0.
    crossWidth := self width / 3.0.
    horizontalBar := self bounds insetBy: 0 @ crossHeight.
    verticalBar := self bounds insetBy: crossWidth @ 0.
    ^ (horizontalBar containsPoint: aPoint) or: [verticalBar containsPoint: aPoint]

שני עקרונות תכנון וארגון קוד ב-CrossMorph: (1) ריכוז חישובי horizontalBar ו-verticalBar במתודות עזר פרטיות למניעת כפילות הקוד בין drawOn: ל-containsPoint:; (2) פיצול המלבן האנכי לחלק עליון וחלק תחתון (מעל ומתחת ל-horizontalBar) כך שהריבוע המרכזי המשותף יצויר פעם אחת בלבד (חשוב במיוחד כשמציירים בצבע שקוף למחצה!).

17.5 ★ אירועי עכבר (2 שלבים), מקלדת (3 שלבים) ואנימציית Stepping

🖱️ טיפול באירועי עכבר (2 שלבים) ודוגמאות תגובה: • שלב 1 (הצהרה): שכתוב handlesMouseDown: (ללחיצה/שחרור/תזוזה), handlesMouseOver: (לכניסה/יציאה) או handlesMouseOverDragging: כך שיחזירו ^ true.
• שלב 2 (תגובה + `self changed`):
// תגובה ללחיצה (הקטנה בלחיצה על הכפתור הצהוב):
CrossMorph >> mouseDown: anEvent
    anEvent redButtonPressed
        ifTrue: [self extent: (self extent * 1.1)].
    anEvent yellowButtonPressed
        ifTrue: [self extent: (self extent * 0.9)].
    self changed   "חובה כדי לתזמן קריאה ל-drawOn:!"

// תגובה לתנועת סמן (אדום בכניסת עכבר, ירוק ביציאה):
CrossMorph >> handlesMouseOver: anEvent  ^ true
CrossMorph >> mouseEnter: anEvent  self color: Color red
CrossMorph >> mouseLeave: anEvent  self color: Color green
⌨️ אירועי מקלדת (3 שלבים) ואנימציית Stepping: • שלב 1: ב-mouseEnter: anEvent מעבירים פוקוס: anEvent hand newKeyboardFocus: self (ובו בזמן יש לדרוס handlesMouseOver: ^ true).
• שלב 2: ב-handleKeystroke: anEvent בודקים את anEvent keyValue: 30 (חץ למעלה: -(0@1)), 31 (חץ למטה: +(0@1)), 29 (חץ ימינה: +(1@0)), 28 (חץ שמאלה: -(1@0)).
• שלב 3: ב-mouseLeave: anEvent משחררים פוקוס: anEvent hand newKeyboardFocus: nil.
• אנימציות (`stepping and presenter`): stepTime (מספר המילישניות בין צעדים), step (מבצעת את השינוי בצורה בכל פעימה), startStepping, stopStepping ו-isStepping.

17.6–17.10 ★ דו-שיח, Drag & Drop, DieMorph ו-Canvas/fork

  • תיבת דו-שיח (`FillInTheBlank`) ותפריט קופץ (`PopUpMenu`):
    • FillInTheBlank request: 'enter your name:' initialAnswer: '??????' $\implies$ מחזירה את המחרוזת שהוקלדה (או '??????' אם אושר ללא שינוי, או '' ב-Cancel).
    • menu := PopUpMenu labelArray: #('x' 'y' 'z') lines: #(1 2 3). menu startUpWithCaption: 'Choose an item' $\implies$ lines: מציין אחרי אילו אינדקסים יופיע קו מפריד, ו-startUpWithCaption: מחזירה את אינדקס הפריט שנבחר (1..3, או 0 אם לא נבחר דבר).
  • גרירה והשמטה (`Drag and Drop`) ומנגנון תיאום בין צורות:
    • במקבל (`ReceiverMorph`): wantsDroppedMorph: aMorph event: anEvent (האם לקבל את העצם הנגרר) ו-repelsMorph: aMorph event: ev (אם מחזירה true, עצם שנדחה חוזר למקומו המקורי במקום להישאר מונח בנקודת העזיבה!).
    • בנגרר (`DroppedMorph`): wantsToBeDroppedInto: aMorph ו-rejectDropMorphEvent: anEvent (מגדירה את התנהגות הנגרר במקרה של דחייה).
    // מנגנון Drag & Drop (ReceiverMorph מקבל רק צבע שונה משלו ומחזיר דחויים; DroppedMorph משנה צבע בדחייה):
    ReceiverMorph >> wantsDroppedMorph: aMorph event: anEvent
        ^ self color ~= aMorph color
    ReceiverMorph >> repelsMorph: aMorph event: ev
        ^ (self wantsDroppedMorph: aMorph event: ev) not
    DroppedMorph >> rejectDropMorphEvent: anEvent
        self color: Color random.   "משנה את צבעו כך שבגרירה הבאה יתקבל!"
        super rejectDropMorphEvent: anEvent

🎲 קוביית המשחק `DieMorph` (`perform:`) ומחלקת `Canvas`

BorderedMorph subclass: #DieMorph
    instanceVariableNames: 'faces dieValue isStopped'
    classVariableNames: '' poolDictionaries: '' category: 'SBE-Morphic'

DieMorph class >> faces: aNumber  ^ self new faces: aNumber
DieMorph >> initialize
    super initialize. self extent: 50@50. self color: Color green.
    dieValue := 1. faces := 6. isStopped := false
DieMorph >> faces: aNumber
    (aNumber isInteger and: [aNumber > 0] and: [aNumber <= 9]) ifTrue: [faces := aNumber]
DieMorph >> face1  ^ {0.5@0.5}
DieMorph >> face2  ^ {0.25@0.25 . 0.75@0.75}
DieMorph >> face3  ^ {0.25@0.25 . 0.75@0.75 . 0.5@0.5}  "... עד face9 (קואורדינטות יחסיות 0..1)"
DieMorph >> drawOn: aCanvas
    super drawOn: aCanvas.
    (self perform: ('face', dieValue asString) asSymbol)
        do: [:aPoint | self drawDotOn: aCanvas at: aPoint]
DieMorph >> drawDotOn: aCanvas at: aPoint
    aCanvas fillOval: (Rectangle center: self position + (self extent * aPoint) extent: self extent / 6) color: Color black
DieMorph >> dieValue: aNumber
    (aNumber isInteger and: [aNumber > 0] and: [aNumber <= faces])
        ifTrue: [dieValue := aNumber. self changed]
DieMorph >> stepTime  ^ 100
DieMorph >> step      isStopped ifFalse: [self dieValue: (1 to: faces) atRandom]
DieMorph >> handlesMouseDown: anEvent  ^ true
DieMorph >> mouseDown: anEvent         anEvent redButtonPressed ifTrue: [isStopped := isStopped not]

• שימוש ב-Canvas, תמונות Form ותהליכי fork: המחלקה Canvas מספקת מתודות לציור צורות, טקסט ותמונות (כולל שקיפות ב-AlphaBlendingCanvas, רוטציה ושינוי קנה-מידה). לטעינת תמונה מקובץ והקטנתה: f := (Form fromFileNamed: 'myPic.bmp') magnifyBy: 0.5. f asMorph openInWorld. להרצת אנימציה בתהליך רקע עצמאי (Thread): שולחים לבלוק את ההודעה fork (למשל [100 timesRepeat: [(Delay forMilliseconds: 20) wait. m position: m position + (2@1)]] fork).

חלק VII: מטה-מחלקות – המושג "מטה-מחלקה" (פרק 18)

שרשרת true class..., סיווג למטה-רמות, אבולוציית 5 הרמות, 10 חוקי המודל ב-Squeak, איתור מתודת new והמעגל הסגור של Metaclass.

18.1–18.7 ★ ייצוג מחלקה כעצם, שרשרת class, סיווג למטה-רמות ו-5 רמות עצמים

מכיוון שב-Smalltalk כל דבר הוא עצם וכל עצם הוא מופע של מחלקה, גם מחלקה היא עצם — והיא מופע של מטה-מחלקה (`Metaclass`): מחלקה שהמופעים שלה הם מחלקות!

🔄 מעקב שרשרת שליחת הודעת class (חוקי המודל ומטה-מחלקות):
  • • שלב 1: true class → True
  • • שלב 2: true class class → True class
  • • שלב 3: true class class class → Metaclass
  • • שלב 4: true class class class class → Metaclass class
  • • שלב 5 (5 פעמים class): → Metaclass
  • • שלב 6 (6 פעמים class): → Metaclass class
  • • שלב 7 (7 פעמים class): → Metaclass
🎭 סיווג לשלוש שכבות של עצמים: בדוגמת המחזות ("מקבת", "כטוב בעיניכם", "אותלו" $\in$ המחלקה Play) והרומנים ("1984", "אוליבר טוויסט", "מלחמה ושלום" $\in$ המחלקה Novel), מבחינים בשלוש שכבות מהותיות של ישויות במערכת:
1. מופעים סופיים (`Terminal Instances` / `Non-instantiable objects`): עצמים פשוטים שאינם יכולים לייצר עצמים נוספים (כגון "אוליבר טוויסט").
2. מחלקות (`Classes` / `Instantiable objects`): עצמים המייצרים מופעים פשוטים (כגון Novel ו-Play).
3. מטה-מחלקות (`Metaclasses` / `Instances are classes`): עצמים המייצרים מחלקות (כגון המחלקה Class).
מספר הרמות במערכת שפות מייצגות מאפייני הארכיטקטורה והמגבלה שהובילה לרמה הבאה
רמה 1: מערכת של עצמים בלבד Self מערכת "שטוחה" ללא מחלקות כלל. כל עצם מתאר את עצמו (טבלה המתאימה שמות לערכים) ומופעים חדשים נוצרים באמצעות שכפול (Cloning) של עצם קיים (אב-טיפוס `Novel Prototype`).
רמה 2: מערכת עצמים בת 2 רמות Eiffel יש עצמים ויש מחלקות המתארות אותם, אך אין מטה-מחלקות: המחלקות אינן מיוצגות בזמן ריצה, אינן עצמים ואינן מגיבות להודעות.
רמה 3: מערכת עצמים בת 3 רמות Smalltalk-76, Little Smalltalk הדרישה המינימלית למערכת סמולטוק: גם העצמים וגם המחלקות הם עצמים, ויש רק מטה-מחלקה אחת בלבד (היורשת מ"עצם" והיא מופע של עצמה). המגבלה: לכל המחלקות יש מבנה והתנהגות זהים (למשל מימוש משותף של new) ואי אפשר להגדיר מתודות-מחלקה או משתני-מחלקה פרטניים לכל מחלקה!
רמה 4: מערכת עצמים בת 4 רמות LOOPS, ObjVlisp (הרחבות Lisp) מאפשרת לכל מחלקה להגדיר מטה-מחלקה פרטית משלה (כגון Meta Novel עם n_novels, או לשתף מטה-מחלקה), וכל המטה-מחלקות הן מופעים של "המטה-מחלקה הגנרית" היחידה (שהיא מופע של עצמה).
רמה 5: מערכת בת 5 רמות – Explicit Metaclass Squeak, Smalltalk-80 לכל מחלקה (לרבות Metaclass עצמה!) יש מטה-מחלקה שמתארת רק אותה; כל המטה-מחלקות (לרבות Metaclass class) הן מופעים של Metaclass, ונוצר גרף ירושה של מטה-מחלקות החופף את גרף הירושה של המחלקות הרגילות!

18.8–18.11 ★ 10 חוקי המודל ב-Squeak, היררכיה מקבילה ואיתור blue ו-new

📜 10 החוקים של מודל העצמים והמטה-מחלקות ב-Squeak (סעיפים 18.8–18.14):

חוקים 1–5 (מודל העצמים הבסיסי): 1. כל דבר הוא עצם.
2. כל עצם הוא מופע של מחלקה.
3. לכל מחלקה יש מחלקת-על.
4. הכל קורה באמצעות שליחת הודעות.
5. איתור מתודות נעשה במעלה היררכיית הירושה (למשל שליחת class ל-translucentBlue מחפשת ב-TranslucentColor → Color → Object).
חוקים 6–10 (חוקי המטה-מחלקות – סעיפים 18.10–18.14): 6. כל מחלקה היא מופע יחיד של מטה-מחלקה אנונימית (Color class).
7. היררכיית מטה-מחלקות מקבילה להיררכיית המחלקות!
8. כל מטה-מחלקה יורשת מהמחלקה Class ומהמחלקה Behavior.
9. כל מטה-מחלקה היא מופע של המחלקה Metaclass.
10. המטה-מחלקה של Metaclass (Metaclass class) היא מופע של Metaclass!
  • הודעות תשאול למחלקה כעצם ובחינה ב-Inspector:
    • הודעות למחלקה: subclasses (מערך תתי-מחלקות), allSuperclasses (אוסף כל מחלקות-העל), instVarNames (מערך משתני המופע של המחלקה), allInstVarNames (מערך כל משתני המופע כולל אלו שנורשו ממחלקות-העל).
    • מעקב שרשרת superclass של מטה-מחלקה: מה מחזיר הביטוי TranslucentColor class superclass superclass? הוא מחזיר את Object class (המטה-מחלקה של Object).
    • שליחת מתודת מחלקה בירושה (TranslucentColor blue): כששולחים TranslucentColor blue, החיפוש מתחיל ב-TranslucentColor class, עולה ל-Color class, מוצא שם את blue ומחזיר צבע כחול אטום רגיל (Color blue). מה אפשר לעשות כדי ש-TranslucentColor blue תחזיר צבע כחול שקוף במקום כחול רגיל? להגדיר מחדש (Override) את המתודה blue במטה-מחלקה TranslucentColor class (למשל ^ super blue alpha: 0.5)!
    • כותרת חלון ה-Inspector: בכותרת ה-Inspector רשום תמיד שם המחלקה של העצם הנבחן (ולא שם העצם): כשבוחנים את Color blue הכותרת היא Color, וכשבוחנים את המחלקה Color הכותרת היא Color class!

🏆 שאלת הבחינה הקלאסית: מסלול איתור המתודה TranslucentColor new:

  1. המקבל (self): עצם המחלקה TranslucentColor.
  2. התחלת החיפוש במטה-מחלקה של המקבל: החיפוש מתחיל ב-TranslucentColor class (ולא במחלקת-העל Color!).
  3. טיפוס במעלה היררכיית המטה-מחלקות: TranslucentColor class → Color class → Object class → Class → ClassDescription → Behavior.
  4. מציאת new ב-Behavior: המתודה new ממומשת ב-Behavior, ומכיוון שהיא יוצרת ומחזירה מופע של self (שהוא המקבל המקורי TranslucentColor), העצם המוחזר הוא מופע של TranslucentColor (ולא של Behavior)!

18.12–18.15 ★ מחלקות הליבה (Behavior, ClassDescription, Class, Metaclass), נוסחאות ודבר סוף

1. Behavior (יורשת מ-Object) יוצרת את המינימום הנדרש להידור מתודות וליצירת מופעים ולהרצתם (שומרת מצביע ל-superclass, מילון מתודות, פורמט מופעים, ומתודות היצירה new, basicNew, new:, basicNew:).
2. ClassDescription (יורשת מ-Behavior) מחלקה מופשטת המוסיפה פעולות כגון הגדרת פרוטוקולים, שמות למשתני מופע, תיעוד ומעקב אחרי שינויים. ממנה יורשות שתי אחיות: Class ו-Metaclass!
3. Class (יורשת מ-ClassDescription) יוצרת את ההתנהגות המשותפת לכל המחלקות הרגילות: מטפלת בשם המחלקה, משתני מחלקה, מאגרים משותפים ויצירת תתי-מחלקות. ממנה יורשת Object class (וב-Squeak דרך ProtoObject class)!
4. Metaclass (יורשת מ-ClassDescription) מגדירה את התנהגותן המשותפת של מטה-מחלקות (יצירה ואתחול של מופע יחיד שהוא עצם המחלקה, אתחול משתני מחלקה וכו'). כל המטה-מחלקות (כולל Metaclass class!) הן מופעים של Metaclass!

🎓 נוסחאות זהב לפיצוח שאלות שרשור class ו-superclass במבחן (סעיפים 18.14–18.15):

  • • C superclass class == C class superclass → true  (עבור כל מחלקה C שאינה שורש העץ, לפי חוק 7 של ההיררכיה המקבילה!)
  • • Object superclass → ProtoObject (או nil בשורש)   |   Object class superclass → Class (דרך ProtoObject class)
  • • Metaclass superclass → ClassDescription   |   Class superclass → ClassDescription
  • • Metaclass class → Metaclass class   |   Metaclass class class → Metaclass  (חוק 10: קשר מופע מעגלי!)
  • • Metaclass class superclass → ClassDescription class  (לפי חוק 7, כי מחלקת-העל של Metaclass היא ClassDescription!)

* דבר סיום והשפעות בעולם התוכנה: שפת Objective-C (ששימשה את חברת Apple למערכות OS X ו-iOS) היא שפת תכנות מונחית עצמים המוסיפה לשפת C הודעות בסגנון שפת Smalltalk!

בהצלחה רבה במבחן בתכנות מונחה עצמים (20906)! 🎓🚀

הסיכום מקיף, מעמיק ונאמן לחומר הלימוד בכל 18 הפרקים של הקורס.