Smart-World Surf

יחידה 4: סביבות וטווח הכרה (Lexical Scope)

ניהול קישורים בין שמות לערכים והבנת מנגנון ה-Lexical Scope.
סביבת הערכה (Environment)טווח הכרה סטטיקישור שמותסגורים (Closures)

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

ניהול קישורים וטווח הכרה: הליבה

בכל שפת תכנות, אנו משתמשים בשמות כדי להתייחס לנתונים ולפעולות. השאלה המרכזית היא: איזה ערך מקושר לשם מסוים בנקודה נתונה בתוכנית? התשובה לשאלה זו טמונה במושג טווח ההכרה (Scope) ובאופן שבו שפות מנהלות סביבות הערכה (Environments).

סביבת הערכה (Environment): מבנה נתונים (לרוב רשימה של מפות או טבלאות) הממפה שמות לערכים בנקודה מסוימת בזמן ריצת התוכנית. כל סביבה יכולה לכלול הפניה לסביבת האב שלה, וליצור שרשרת סביבות.
קישור שמות (Name Binding): התהליך שבו שם (כמו שם משתנה או פונקציה) מקושר לערך ספציפי בסביבת הערכה. קישור זה קובע איזה ערך יוחזר כאשר השם יופיע בביטוי.
טווח הכרה (Scope): האזור בתוכנית שבו שם מסוים "מוכר" וניתן לגשת לערך המקושר אליו.

טווח הכרה סטטי (Lexical Scope) לעומת טווח הכרה דינמי

קיימות שתי גישות עיקריות לקביעת טווח ההכרה של שמות:

טווח הכרה סטטי (Lexical Scope)

טווח ההכרה נקבע בזמן כתיבת הקוד (זמן קומפילציה/ניתוח). שם מקושר לערך בסביבה שבה הפונקציה הוגדרה. זוהי הגישה הנפוצה ביותר בשפות מודרניות (Python, JavaScript, Java, C#, Scheme).

טווח הכרה דינמי (Dynamic Scope)

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

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

סגורים (Closures): עוצמת ה-Lexical Scope

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

סגור (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).

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

שאלות לדיון

  • הסבירו מדוע טווח הכרה סטטי (Lexical Scope) נחשב בדרך כלל לעדיף על פני טווח הכרה דינמי ברוב שפות התכנות המודרניות.
  • תארו את תהליך יצירת סביבת הערכה חדשה ואת הקשר שלה לסביבת האב שלה, בהקשר של קריאה לפונקציה.
  • כיצד סגורים מאפשרים ליישם את דפוס ה-Currying או פונקציות "מפעל" (factory functions) בשפות תכנות? תנו דוגמה קצרה.
  • מה יקרה אם ננסה לגשת למשתנה שאינו מקושר באף אחת מהסביבות בשרשרת ה-Lexical Scope?

נקודות לתשובת מודל

  • טווח הכרה סטטי מול דינמי: סטטי קל יותר לניתוח והבנה (ניתן לקבוע את הקישורים בזמן קומפילציה), הוא צפוי יותר (התנהגות הפונקציה אינה תלויה בהקשר הקריאה), ומאפשר אופטימיזציות קומפילציה. דינמי עלול להוביל לבאגים קשים לאיתור עקב תלות בהקשר הריצה.
  • יצירת סביבה: בעת קריאה לפונקציה, נוצרת סביבת הערכה חדשה. סביבה זו מקבלת את סביבת ההגדרה של הפונקציה כסביבת האב שלה. ארגומנטים הפונקציה מקושרים לשמות הפרמטרים בסביבה החדשה. משתנים מקומיים מוגדרים גם הם בסביבה זו. חיפוש שמות מתבצע בסביבה הנוכחית, ואם לא נמצא, ממשיך במעלה שרשרת סביבות האב.
  • סגורים ויישומים: סגורים מאפשרים ליצור פונקציות מותאמות אישית על ידי "הקפאת" חלק מהארגומנטים (Currying) או ליצור פונקציות שמחזיקות מצב פנימי (factory functions). הדוגמה עם make-adder היא דוגמה טובה לפונקציית מפעל.
  • גישה למשתנה לא מקושר: ניסיון לגשת למשתנה שאינו מקושר בשרשרת ה-Lexical Scope יוביל לשגיאת זמן ריצה (לדוגמה, "unbound variable" או "name not defined"), מכיוון שהמפרש/קומפיילר לא ימצא קישור מתאים לשם זה.
מצאתם טעות או שחסר משהו?
→ הקודמת
בניית מפרש בסיסי
הבאה ←
פונקציות מסדר גבוה וסגורים