ברוכים הבאים ליחידה "פונקציות מסדר גבוה וסגורים" בקורס "שפות תכנות" (20905). יחידה זו היא אבן יסוד בהבנת פרדיגמות תכנות פונקציונליות וכיצד מפרשים (Interpreters) מטפלים בפונקציות. נחקור כיצד פונקציות הופכות ל"אזרח סוג א'", את הרעיון העוצמתי של סגורים, וכיצד מנגנונים אלו ממומשים בליבת מפרש שפה.
פונקציות כאזרח סוג א'
בשפות תכנות רבות, פונקציות הן ישויות מיוחדות. אך בשפות פונקציונליות ובשפות מודרניות רבות, פונקציות נחשבות ל"אזרח סוג א'". מה זה אומר?
פונקציות מסדר גבוה (Higher-Order Functions - HOFs)
היכולת להתייחס לפונקציות כאזרח סוג א' מאפשרת את קיומן של פונקציות מסדר גבוה.
- העברת פונקציות כארגומנטים: דוגמאות קלאסיות הן פונקציות כמו
map,filter,fold(אוreduce) בשפות כמו Scheme/Racket. פונקציות אלו מקבלות פונקציה אחרת ומיישמות אותה על איברים בקולקציה. לדוגמה,(map (lambda (x) (* x x)) '(1 2 3)). - החזרת פונקציות כערך: פונקציה יכולה לייצר ולהחזיר פונקציה חדשה. זהו מנגנון בסיסי ליצירת סגורים, כפי שנראה בהמשך.
סגורים (Closures)
סגורים הם אחד המושגים העוצמתיים ביותר בתכנות פונקציונלי, והם נובעים ישירות מהיכולת להתייחס לפונקציות כאזרח סוג א'.
לכידת סביבה (Environment Capture)
הליבה של סגור היא מנגנון לכידת הסביבה.
דוגמה קלאסית: פונקציה שמייצרת פונקציות אחרות:
(define (make-adder n)
(lambda (x)
(+ x n)))
(define add-5 (make-adder 5))
(define add-10 (make-adder 10))
(add-5 3) ; => 8
(add-10 3) ; => 13
כאן, הפונקציות add-5 ו-add-10 הן סגורים. כל אחת מהן לוכדת ערך שונה של n מהסביבה שבה נוצרה, ומשתמשת בו בעת הקריאה.
מימוש סגורים במפרש
הבנת הסגורים מתחזקת כאשר מבינים כיצד הם ממומשים ברמת המפרש (כפי שנלמד בקורס דרך קבצי interp.scm ו-environments.scm).
סביבת הגדרה
הסביבה (Environment) שבה פונקציה נוצרה (הוגדרה). כאשר המפרש נתקל בהגדרת פונקציה (למשל, ביטוי lambda), הוא יוצר אובייקט סגור שכולל מצביע לסביבה זו.
סביבת קריאה
הסביבה שבה פונקציה נקראת ומבוצעת. סביבה זו נוצרת עבור כל קריאה לפונקציה ומשמשת לפתרון שמות של פרמטרים ומשתנים מקומיים חדשים שמוגדרים בתוך הפונקציה.
אובייקט סגור (Closure Object)
מבנה נתונים פנימי במפרש המייצג סגור. הוא מכיל שני מרכיבים עיקריים: את קוד הפונקציה (לרוב, עץ תחבירי מופשט) ואת מצביע לסביבת ההגדרה שלה (הסביבה שנלכדה).
כאשר פונקציה המהווה סגור נקראת, המפרש מבצע את הפעולות הבאות:
- יוצר סביבת קריאה חדשה.
- מאכלס את סביבת הקריאה בקישורי הפרמטרים שהועברו לפונקציה.
- משרשר את סביבת הקריאה החדשה לסביבת ההגדרה הלכודה של הסגור. כלומר, סביבת ההגדרה הלכודה הופכת להיות "ההורה" של סביבת הקריאה.
- מבצע את גוף הפונקציה בתוך סביבת הקריאה המשורשרת.
בדרך זו, משתנים חופשיים (שאינם פרמטרים או מוגדרים מקומית) נפתרים על ידי חיפוש בסביבת ההגדרה הלכודה, בעוד פרמטרים ומשתנים מקומיים נפתרים בסביבת הקריאה הנוכחית.
שאלות לדיון
- הסבירו מדוע פונקציות מסדר גבוה וסגורים נחשבים לעמודי תווך בתכנות פונקציונלי. תנו דוגמאות לשימושים מעשיים.
- כיצד מפרש (כמו זה שנבנה בקורס) מטפל בהגדרת פונקציה שמחזירה פונקציה אחרת? מהו מבנה הנתונים הפנימי שנוצר?
- הסבירו את ההבדל בין משתנה גלובלי למשתנה חופשי בסגור. מתי מפרש יחפש ערך של משתנה בסביבה הגלובלית ומתי בסביבה הלכודה של סגור?
- מהם היתרונות והחסרונות של שימוש נרחב בסגורים מבחינת ניהול זיכרון וביצועים?
נקודות לתשובת מודל
- חשיבות: HOFs מאפשרות הפשטה ושימוש חוזר בקוד (דפוסי איטרציה, טרנספורמציה). סגורים מאפשרים יצירת פונקציות "ממוקדות מצב" (stateful functions) ללא שימוש באובייקטים מורכבים, בניית DSLs, קארינג, דקורטורים.
- טיפול במפרש: בעת הגדרת פונקציה פנימית (למשל,
lambdaבתוךmake-adder), המפרש יוצר אובייקט סגור. אובייקט זה מכיל את קוד ה-lambdaואת מצביע לסביבה הלקסיקלית הנוכחית (זו שבהmake-adderנקראה ו-nהוגדר). כאשר הסגור נקרא, סביבת הקריאה שלו מקושרת לסביבה הלכודה. - משתנה גלובלי מול חופשי: משתנה גלובלי מוגדר בסביבה העליונה ביותר של התוכנית. משתנה חופשי בסגור הוא משתנה שאינו פרמטר של הסגור ואינו מוגדר בתוכו, אך הוא היה קיים בסביבת ההגדרה של הסגור. המפרש יחפש תחילה בסביבת הקריאה, אחר כך בסביבה הלכודה של הסגור, ורק לבסוף בסביבות ה"הוריות" עד לסביבה הגלובלית.
- יתרונות/חסרונות: יתרונות: גמישות, מודולריות, קוד קצר וקריא, הימנעות מ-side effects לא רצויים. חסרונות: עלול להוביל לצריכת זיכרון גבוהה יותר אם הסביבות הלכודות גדולות ואינן משוחררות בזמן (garbage collection), פוטנציאל לבלבול למתכנתים לא מנוסים.