ברוכים הבאים ליחידת הלימוד "סביבות וטווח הכרה (Lexical Scope)" בקורס "שפות תכנות". יחידה זו חיונית להבנת האופן שבו שפות תכנות מודרניות מנהלות את הקישור בין שמות (משתנים, פונקציות) לערכים שהם מייצגים. נצלול לעומק מנגנון ה-Lexical Scope, נבין את תפקידן של סביבות הערכה, ונגלה כיצד מושגים אלו מאפשרים תכונות עוצמתיות כמו סגורים (Closures).
ניהול קישורים וטווח הכרה: הליבה
בכל שפת תכנות, אנו משתמשים בשמות כדי להתייחס לנתונים ולפעולות. השאלה המרכזית היא: איזה ערך מקושר לשם מסוים בנקודה נתונה בתוכנית? התשובה לשאלה זו טמונה במושג טווח ההכרה (Scope) ובאופן שבו שפות מנהלות סביבות הערכה (Environments).
טווח הכרה סטטי (Lexical Scope) לעומת טווח הכרה דינמי
קיימות שתי גישות עיקריות לקביעת טווח ההכרה של שמות:
טווח הכרה סטטי (Lexical Scope)
טווח ההכרה נקבע בזמן כתיבת הקוד (זמן קומפילציה/ניתוח). שם מקושר לערך בסביבה שבה הפונקציה הוגדרה. זוהי הגישה הנפוצה ביותר בשפות מודרניות (Python, JavaScript, Java, C#, Scheme).
טווח הכרה דינמי (Dynamic Scope)
טווח ההכרה נקבע בזמן ריצת הקוד. שם מקושר לערך בסביבה שבה הפונקציה נקראה. גישה זו פחות נפוצה כיום (למשל, בשפות כמו Emacs Lisp, Bash).
סגורים (Closures): עוצמת ה-Lexical Scope
אחד היישומים החזקים והחשובים ביותר של טווח הכרה סטטי הוא מושג הסגור (Closure). סגור הוא פונקציה ש"זוכרת" את סביבת ההערכה שבה היא נוצרה, גם לאחר שסביבה זו כבר לא פעילה באופן ישיר.
דוגמה (בסגנון Scheme):
(define (make-adder x)
(lambda (y)
(+ x y)))
(define add5 (make-adder 5))
(define add10 (make-adder 10))
(add5 3) ; -> 8
(add10 7) ; -> 17
בדוגמה זו, הפונקציה make-adder מחזירה פונקציה אנונימית (lambda). כאשר make-adder נקראת עם x=5, היא יוצרת סביבה שבה x מקושר ל-5. הפונקציה האנונימית "סוגרת" (captures) את המשתנה x מאותה סביבה. לכן, add5 היא פונקציה ש"זוכרת" ש-x שלה הוא 5, גם לאחר ש-make-adder סיימה את ריצתה. זהו סגור. חשוב לציין שסביבות הערכה אלו מנוהלות על ידי מפרש השפה (כפי שניתן לראות בקבצים כמו interp.scm ו-environments.scm).
שאלות לדיון
- הסבירו מדוע טווח הכרה סטטי (Lexical Scope) נחשב בדרך כלל לעדיף על פני טווח הכרה דינמי ברוב שפות התכנות המודרניות.
- תארו את תהליך יצירת סביבת הערכה חדשה ואת הקשר שלה לסביבת האב שלה, בהקשר של קריאה לפונקציה.
- כיצד סגורים מאפשרים ליישם את דפוס ה-Currying או פונקציות "מפעל" (factory functions) בשפות תכנות? תנו דוגמה קצרה.
- מה יקרה אם ננסה לגשת למשתנה שאינו מקושר באף אחת מהסביבות בשרשרת ה-Lexical Scope?
נקודות לתשובת מודל
- טווח הכרה סטטי מול דינמי: סטטי קל יותר לניתוח והבנה (ניתן לקבוע את הקישורים בזמן קומפילציה), הוא צפוי יותר (התנהגות הפונקציה אינה תלויה בהקשר הקריאה), ומאפשר אופטימיזציות קומפילציה. דינמי עלול להוביל לבאגים קשים לאיתור עקב תלות בהקשר הריצה.
- יצירת סביבה: בעת קריאה לפונקציה, נוצרת סביבת הערכה חדשה. סביבה זו מקבלת את סביבת ההגדרה של הפונקציה כסביבת האב שלה. ארגומנטים הפונקציה מקושרים לשמות הפרמטרים בסביבה החדשה. משתנים מקומיים מוגדרים גם הם בסביבה זו. חיפוש שמות מתבצע בסביבה הנוכחית, ואם לא נמצא, ממשיך במעלה שרשרת סביבות האב.
- סגורים ויישומים: סגורים מאפשרים ליצור פונקציות מותאמות אישית על ידי "הקפאת" חלק מהארגומנטים (Currying) או ליצור פונקציות שמחזיקות מצב פנימי (factory functions). הדוגמה עם
make-adderהיא דוגמה טובה לפונקציית מפעל. - גישה למשתנה לא מקושר: ניסיון לגשת למשתנה שאינו מקושר בשרשרת ה-Lexical Scope יוביל לשגיאת זמן ריצה (לדוגמה, "unbound variable" או "name not defined"), מכיוון שהמפרש/קומפיילר לא ימצא קישור מתאים לשם זה.