מאגר קטעי קוד ממדריך הלמידה
פונקציה זו מחשבת את אורכה של רשימה נתונה ב-Scheme. היא מדגימה את המבנה הקלאסי של פונקציה רקורסיבית על רשימות: תנאי העצירה (null? lst) המטפל ברשימה ריקה, והקריאה הרקורסיבית (list-length (cdr lst)) שמתקדמת לאיבר הבא תוך הוספת 1 למנייה.
למבחן חשוב להבין את הבסיס של רקורסיה: איך בודקים מקרה קצה (תנאי עצירה) ואיך מקטינים את הבעיה בכל קריאה. במטלות רבות תתבקשו לרוץ על רשימות או עצים בדיוק בגישה הזו.
;; list-length : List -> Int
;; usage: (list-length l) = the length of l
;; Page: 14
(define list-length
(lambda (lst)
(if (null? lst)
0
(+ 1 (list-length (cdr lst))))))
פונקציה זו בודקת האם משתנה מסוים 'חופשי' (אינו קשור) בביטוי תחשיב למדא (Lc-exp) בייצוג של רשימות. היא מטפלת בשלושה מקרים: משתנה בודד (symbol), ביטוי למדא (שיוצר קשירה), וביטוי אפליקציה (קריאה לפונקציה).
מושג המשתנה החופשי מופיע לעיתים קרובות במבחנים תחת נושא התחימה הלקסיקלית (Lexical Scoping). חשוב להבין שמשתנה הופך לקשור רק תחת ביטוי 'lambda' שמכריז עליו, אחרת הוא נשאר חופשי. פונקציה זו ממחישה ניתוח עץ תחבירי באמצעות פעולות רשימה (car, cadr, caddr).
;; occurs-free? : Sym * Lcexp -> Bool
;; usage:
;; returns #t if the symbol var occurs free in exp,
;; otherwise returns #f.
;; Page: 19
(define occurs-free?
(lambda (var exp)
(cond
((symbol? exp) (eqv? var exp))
((eqv? (car exp) ’lambda)
(and
(not (eqv? var (car (cadr exp))))
(occurs-free? var (caddr exp))))
(else
(or
(occurs-free? var (car exp))
(occurs-free? var (cadr exp)))))))
פונקציה לחיבור שני מספרים (x ו-y) על ידי פעולות של יורש (successor) וקודם (predecessor). היא לא משתמשת בפעולת ה-+ המובנית של סקים אלא מסתמכת על הפשטת נתונים.
הקוד ממחיש את הרעיון שחישובים יכולים להתבצע רק בעזרת פעולות יסוד אבסטרקטיות. למבחן, חשוב להבין איך בונים פונקציות מתמטיות שלמות מאבני בניין בסיסיות ללא תלות באופן הייצוג שלהן בזיכרון.
;; plus : N * N -> N
;; usage: (plus x y) = the sum of x and y
(define (plus x y)
(if (is-zero? x)
y
(successor (plus (predecessor x) y))))
זהו מימוש ספציפי של הפשטת המספרים - ייצוג אונרי (Unary Representation). כל מספר מיוצג כרשימה של ערכי #t. ה'אפס' הוא רשימה ריקה, 'יורש' מוסיף איבר לרשימה, ו'קודם' מסיר איבר.
קריטי למבחן בנושאי Data Abstraction. ייתכן שתתבקשו להחליף ייצוג אונרי בייצוג אחר (כמו בינארי או BigNum) ולהראות שהפונקציה 'plus' מהקטע הקודם עדיין תעבוד מבלי לשנות בה תו אחד.
;; plus : N * N -> N
;; Unary Representation
;; page 33 10.
(define zero (lambda () ’()))
(define is-zero? (lambda (n) (null? n)))
(define successor (lambda (n) (cons #t n)))
(define predecessor (lambda (n) (cdr n)))
זוהי גרסה חלופית (וקצרה יותר) לאותו ייצוג אונרי, בה הפונקציות הבסיסיות של Scheme פשוט מושמות לשמות הפעולות ללא עטיפת lambda נוספת.
חשוב להכיר ב-Scheme שאפשר להעביר פונקציה כמו cdr למשתנה אחר. זה עוזר לקצר קוד במבחן כשצריך להגדיר מנבאים או מחלצים זהים לפעולות מובנות.
(define zero ’())
(define is-zero? null?)
(define successor (lambda (n) (cons #t n)))
(define predecessor cdr)
ייצוג בינארי פשוט (Scheme number representation) למספרים טבעיים, בו אנו סומכים לחלוטין על המספרים של Scheme ועל פעולות ה- (+ 1) ו- (- 1).
דוגמה מובהקת לאופן שבו הפשטת נתונים (Data Abstraction) מאפשרת לנו לשנות מימוש פנימי. אם נחליף בין הקוד האונרי לקוד הבינארי הזה - שאר התוכנית המשתמשת ב-is-zero?, successor, ו-predecessor תמשיך לתפקד באופן זהה.
;; plus : N * N -> N
;; Scheme number representation
;; page 33
(define zero (lambda () 0))
(define is-zero? (lambda (n) (zero? n)))
(define successor (lambda (n) (+ n 1)))
(define predecessor (lambda (n) (- n 1)))
זהו קטע חלקי המציג את הבסיס לקידוד צ'רץ' (Church Encoding). בקידוד זה לא קיימים מספרים אלא רק פונקציות. 'אפס' מקבל פונקציה ומחזיר את ארגומנט ה-x כפי שהוא ללא הפעלת הפונקציה עליו.
קידוד צ'רץ' הוא נושא מתקדם ולעתים מופיע בשאלות בונוס. הרעיון הוא שאפשר לייצג הכל (כולל מספרים טבעיים) באמצעות פונקציות בלבד (lambda calculus).
;; Could not extract snippet starting with: ;; (zero) =
;; from page 56
קטע בדיקות למספרי Church, שמראה איך מעבירים פונקציית בדיקה (כמו +2 או בריבוע) על המספר המקודד כדי להמיר אותו חזרה למספר רגיל ב-Scheme שנוכל לקרוא.
ממחיש איך לבדוק נכונות של קוד מסובך. במטלות או מבחנים עם ייצוגים לא טריוויאליים, כתיבת פונקציות עזר קטנות הממירות ערכים בחזרה לסביבה המוכרת היא קריטית לדיבוג.
(define one (successor (zero)))
(define two (successor one))
(is-zero? (zero)) ;returns #t
(is-zero? one) ;returns #f
(is-zero? two) ;returns #f
;; testing
(define (f n) (+ n 2)) ;f(n)=n+2
(= 2 (((zero) f) 2)) ;returns #t
(= 4 ((one f) 2)) ;returns #t
(= 6 ((two f) 2)) ;returns #t
(define (g n) (* n n)) ;g(n)=n*n
(= 2 (((zero) g) 2)) ;returns #t
(= 4 ((one g) 2)) ;returns #t
(= 16 ((two g) 2)) ;returns #t
עוד סדרת בדיקות המוודאות שהפונקציות predecessor ו-plus עובדות כראוי במספרי Church.
מעבר לתרגול קריאת קוד, מראה את החשיבות של יצירת Test-Suite מקיף לכל קצוות הפעולה (מספרים חיוביים, שליליים ואפס) במפרש שלכם.
;; expressions that return #t
(is-zero? (predecessor one))
(is-zero? (predecessor (predecessor two)))
(is-zero? (predecessor (plus (zero) one)))
(is-zero? (predecessor (predecessor (plus one one))))
;; expressions that return #f
(is-zero? (predecessor (plus one one)))
בניית סביבה (Environment) באופן ידני על ידי שרשור פעולות extend-env. הסביבה נבנית החל מסביבה ריקה, כשכל הרחבה מוסיפה משתנה וערך חדשים (x, y, d).
סביבות הן לב ליבו של המפרש. חשוב להבין שסביבות פועלות כ'מחסנית' (Stack). כשנחפש את הערך של 'y', המפרש יעצור ב-'y' הראשון שימצא בשרשרת (כלומר y=8 יסתיר את y=14). זהו מנגנון ה-Shadowing.
(define e
(extend-env ’d 6
(extend-env ’y 8
(extend-env ’x 7
(extend-env ’y 14
(empty-env))))))
זהו המימוש מבוסס-רשימות של מנגנון הסביבה (Procedural/List Representation). סביבה היא פשוט רשימה מתויגת ('extend-env) הכוללת את שם המשתנה, הערך והסביבה הקודמת (הרקורסיבית). apply-env מחפשת ברשימה זו.
חובה לדעת למבחן! תצטרכו פעמים רבות לשנות את מבנה הסביבה (למשל, כדי לשמור גם טיפוסים ולא רק ערכים, או מערכים). המבנה של apply-env המשתמש ב-cond ובקריאה רקורסיבית מופיע כמעט בכל מבחן.
;; data definition:
;; Env ::= (empty-env) | (extend-env Var Schemeval Env)
;; empty-env : () -> Env
(define empty-env
(lambda () (list ’empty-env)))
;; extend-env : Var * Schemeval * Env -> Env
(define extend-env
(lambda (var val env)
(list ’extend-env var val env)))
;; apply-env : Env * Var -> Schemeval
(define apply-env
(lambda (env search-var)
(cond
((equal? (car env) ’empty-env)
(report-no-binding-found search-var))
((equal? (car env) ’extend-env)
(let ((saved-var (cadr env))
(saved-val (caddr env))
(saved-env (cadddr env)))
(if (eqv? search-var saved-var)
saved-val
(apply-env saved-env search-var))))
(else
(report-invalid-env env)))))
פונקציות עזר לדיווח על שגיאות כאשר apply-env לא מוצאת משתנה בסביבה, או מקבלת סביבה בפורמט לא תקין.
שימוש בפונקציית השגיאה eopl:error הוא סטנדרט בקורס לדיווח שגיאות זמן-ריצה של השפה המפורשת.
(define report-no-binding-found
(lambda (search-var)
(eopl:error ’apply-env "No binding for ~s" search-var)))
(define report-invalid-env
(lambda (env)
(eopl:error ’apply-env "Bad environment: ~s" env)))
מימוש סביבה פרוצדורלי (Procedural Representation). במקום לייצג סביבה כרשימה, סביבה מיוצגת כאן כ*פונקציה* שמקבלת שם משתנה ומחזירה את הערך שלו. extend-env מחזירה למדא שבודקת את המשתנה ומפנה הלאה בעת הצורך.
ממחיש את העוצמה של Scheme בכך שפונקציות הן אזרחים מסוג ראשון (First-Class). ייתכן שתישאלו בבחינה כיצד לשנות ייצוג מבוסס מבנה נתונים לייצוג פונקציונלי. זהו דפוס חשיבה שונה לחלוטין שמבוסס על סגורים (Closures).
;; data definition:
;; Env = Var -> Schemeval
;; empty-env : () -> Env
(define empty-env
(lambda ()
(lambda (search-var)
(report-no-binding-found search-var))))
;; extend-env : Var * Schemeval * Env -> Env
(define extend-env
(lambda (saved-var saved-val saved-env)
(lambda (search-var)
(if (eqv? search-var saved-var)
saved-val
(apply-env saved-env search-var)))))
;; apply-env : Env * Var -> Schemeval
(define apply-env
(lambda (env search-var)
(env search-var)))
גרסה כמעט זהה למימוש הפרוצדורלי הקודם, אשר מפשטת את קריאת ההמשך (saved-env search-var) במקום קריאה מפורשת ל-apply-env.
ממחיש שההבנה ש-Env היא בעצם הפונקציה בעצמה חוסכת לנו יצירת ממשק עוטף נוסף. זה עוזר לכתוב קוד קומפקטי ואלגנטי יותר.
;; data definition:
;; Env = Var -> Schemeval
;; empty-env : () -> Env
(define empty-env
(lambda ()
(lambda (search-var)
(report-no-binding-found search-var))))
;; extend-env : Var * Schemeval * Env -> Env
(define extend-env
(lambda (saved-var saved-val saved-env)
(lambda (search-var)
(if (eqv? search-var saved-var)
saved-val
(saved-env search-var)))))
יצירת בנאים (Constructors) בגישה ידנית עבור עץ תחביר מופשט (AST) של שפת תחשיב למדא (var-exp, lambda-exp, app-exp). כל בנאי מייצר רשימה מתויגת.
למרות שבקורס משתמשים לרוב ב-define-datatype, הבנה של בנאים רגילים חשובה כדי להבין איך באמת מיוצגים העצים בזיכרון כרשימות וסמלים, ומה ה-macro של ה-datatype בעצם חוסך לנו מאחורי הקלעים.
;; var-exp : Var -> Lc-exp
(define var-exp
(lambda (var)
‘(var-exp ,var)))
;; lambda-exp : Var * Lc-exp -> Lc-exp
(define lambda-exp
(lambda (var lc-exp)
‘(lambda-exp ,var ,lc-exp)))
;; app-exp : Lc-exp * Lc-exp -> Lc-exp
(define app-exp
(lambda (lc-exp1 lc-exp2)
‘(app-exp ,lc-exp1 ,lc-exp2)))
מנבאים (Predicates) בגישה ידנית עבור ה-AST. לכל סוג של צומת בעץ יש פונקציה שבודקת האם הקלט הוא רשימה והתגית (car) תואמת לסוג.
עוזר להבין את מנגנון ה-cases. ה-cases משתמש בפונקציות מנבאים כאלו בדיוק כדי לדעת לאיזה ענף לנתב את הביטוי שאנו מפענחים.
;; var-exp? : Lc-exp -> Bool
(define var-exp?
(lambda (x)
(and (pair? x) (eq? (car x) ’var-exp))))
;; lambda-exp? : Lc-exp -> Bool
(define lambda-exp?
(lambda (x)
(and (pair? x) (eq? (car x) ’lambda-exp))))
;; app-exp? : Lc-exp -> Bool
(define app-exp?
(lambda (x)
(and (pair? x) (eq? (car x) ’app-exp))))
מחלצים (Extractors) בגישה ידנית עבור ה-AST. פונקציות פשוטות המשתמשות ב-cadr ו-caddr כדי להוציא את שדות המידע מצומת בעץ התחביר.
כתיבה ישירה כזו גורמת להרבה באגים. זו בדיוק הסיבה שהספר EOPL עובר מהר מאוד להשתמש ב-cases וב-define-datatype שמייצרים את המחלצים האלה אוטומטית ובצורה בטוחה בהרבה.
;; var-exp->var : Lc-exp -> Var
(define var-exp->var
(lambda (x)
(cadr x)))
;; lambda-exp->bound-var : Lc-exp -> Var
(define lambda-exp->bound-var
(lambda (x)
(cadr x)))
;; lambda-exp->body : Lc-exp -> Lc-exp
(define lambda-exp->body
(lambda (x)
(caddr x)))
;; app-exp->rator : Lc-exp -> Lc-exp
(define app-exp->rator
(lambda (x)
(cadr x)))
;; app-exp->rand : Lc-exp -> Lc-exp
(define app-exp->rand
(lambda (x)
(caddr x)))
מימוש חוזר של הפונקציה occurs-free? אבל הפעם מעל מבנה נתונים מסודר של AST עם בנאים ומחלצים, במקום שימוש גולמי בפעולות רשימה כמו car ו-cadr.
זהו שלב מעבר חשוב: הקוד קריא יותר ויציב יותר לשינויים בייצוג של העץ, אך הוא עדיין ידני למדי. מראה את היתרון של Data Abstraction על פני טיפול ישיר במבנה.
;; occurs-free? : Sym * Lcexp -> Bool
(define occurs-free?
(lambda (search-var exp)
(cond
((var-exp? exp) (eqv? search-var (var-exp->var exp)))
((lambda-exp? exp)
(and
(not (eqv? search-var (lambda-exp->bound-var exp)))
(occurs-free? search-var (lambda-exp->body exp))))
(else
(or
(occurs-free? search-var (app-exp->rator exp))
(occurs-free? search-var (app-exp->rand exp)))))))
הגדרת ה- AST בצורה נקייה וסופית באמצעות `define-datatype` של EOPL. כאן אנו מגדירים טיפוס `lc-exp` עם שלוש וריאציות (Variants), וכל וריאציה דורשת שדות עם טיפוסים ספציפיים.
זהו אחד מקטעי הקוד הכי חשובים בקורס! במבחן (ובמטלות) תצטרכו כמעט תמיד לשנות או להרחיב את מבנה ה-datatype כדי להוסיף יכולות חדשות לשפה (למשל, הוספת לולאת while או משתנים מרובים).
(define-datatype lc-exp lc-exp?
(var-exp
(var symbol?))
(lambda-exp
(bound-var symbol?)
(body lc-exp?))
(app-exp
(rator lc-exp?)
(rand lc-exp?)))
המימוש האולטימטיבי של occurs-free? שמשתמש במנגנון ה-`cases` (תואם ל-datatype מוקדם יותר). הוא מפרק את ה-AST למרכיביו בצורה אלגנטית על פי סוג הצומת.
הטמפלייט הזה הוא הבסיס לרוב הפונקציות שתכתבו בקורס! כל פונקציה שסורקת עץ תחביר משתמשת ב-cases. חשוב לשנן את התחביר הזה לקראת המבחן - הוא יופיע בכל מקום (במיוחד ב-value-of).
;; occurs-free? : Sym * Lcexp -> Bool
(define occurs-free?
(lambda (search-var exp)
(cases lc-exp exp
(var-exp (var)
(eqv? var search-var))
(lambda-exp (bound-var body)
(and
(not (eqv? search-var bound-var))
(occurs-free? search-var body)))
(app-exp (rator rand)
(or
(occurs-free? search-var rator)
(occurs-free? search-var rand))))))
פונקציית parse-expression שלוקחת קלט גולמי כ-S-expression מסקים, וממירה אותו לעץ התחביר המופשט (AST) שלנו שבנוי עם בנאי ה-datatype.
השלב הראשון בהפיכת טקסט לתכנית שהמחשב מבין (Parsing). במבחן ייתכן שתידרשו לתקן או להוסיף רכיבים למנתח התחבירי. שימו לב לאופן בו הוא מפרק רשימות בעזרת caddr.
;; parse-expression: SchemeVal -> LcExp
(define parse-expression
(lambda (datum)
(cond
((symbol? datum) (var-exp datum))
((pair? datum)
(if (eqv? (car datum) ’lambda)
(lambda-exp (caadr datum)
(parse-expression (caddr datum)))
(app-exp
(parse-expression (car datum))
(parse-expression (cadr datum)))))
(else (eopl:error ’parse-expression
"Invalid concrete syntax ~s" datum)))))
הפונקציה ההפוכה: unparse-expression. מקבלת עץ תחביר מופשט (AST) וממירה אותו בחזרה לייצוג של רשימות כפי שהמשתמש הזין. שימושי מאוד לדיבוג והצגת שגיאות למשתמש.
יעיל במיוחד בתרגילי מטלות כשצריך להדפיס למסך מה השתבש. ממחיש כיצד להרכיב מחדש רשימות (list) תוך סיור רקורסיבי על כל ענפי העץ בעזרת cases.
;; unparse-expression: LcExp -> SchemeVal
(define unparse-expression
(lambda (exp)
(cases lc-exp exp
(var-exp (id) id)
(lambda-exp (id body)
(list ’lambda (list id)
(unparse-expression body)))
(app-exp (rator rand)
(list (unparse-expression rator)
(unparse-expression rand))))))
(eopl:error ’apply-env "No binding for ~s" search-var)))
יצירה ידנית של עץ תחביר עבור תכנית קטנה, המציגה איך בנאים מה-datatype מורכבים זה על גבי זה (למשל ביטוי חיסור בתוך let).
זה מה שהפרסר SLLgen עושה עבורכם אוטומטית מאחורי הקלעים! הבנה של העץ הידני עוזרת מאוד בדיבוג כשפונקציית ה-value-of קורסת עם קלט לא צפוי.
(a-program
(let-exp ’x
(const-exp 4)
(diff-exp (var-exp ’x)
(diff-exp (const-exp 1)
(var-exp ’x)))))
הגדרת הטיפוס 'ערך משוערך' (ExpVal). בניגוד ל-AST שמייצג את קוד המקור, expval מייצג את הערכים שנוצרים *בזמן הריצה*. כאן השפה תומכת רק במספרים וערכים בוליאניים.
טיפוס קריטי בכל מפרש. ככל שהשפה מתרחבת (PROC, EXPLICIT-REFS), אנו נוסיף לכאן עוד סוגי ערכים כמו פרוצדורות והפניות. כמעט כל פונקציה במפרש תחזיר ExpVal.
(define-datatype expval expval?
(num-val
(calue number?))
(bool-val
(boolean boolean?)))
מחלץ (Extractor) שמוציא את המספר (num) מתוך עטיפת ה-expval של מספר. אם הקלט אינו מסוג num-val, הוא זורק שגיאה.
פונקציות החילוץ הן השומרות הדינמיות (Type Checking) של זמן הריצה. בכל פעם שנעשה חיבור, נחייב חילוץ למספר באמצעות פונקציה זו. במבחן תצטרכו להשתמש בהן כדי למנוע חיבור של בוליאני למספר.
(define expval->num
(lambda (v)
(cases expval v
(num-val (num) num)
(else (expval-extractor-error ’num v)))))
מחלץ עבור bool-val. דומה לזה של המספרים אך בודק ומוציא ערך בוליאני מסקים.
משמש בדרך כלל לפני ביצוע תנאים (if). ה-value-of משתמש בפונקציה זו כדי להחליט איזה ענף לבצע בהמשך.
(define expval->bool
(lambda (v)
(cases expval v
(bool-val (bool) bool)
(else (expval-extractor-error ’bool v)))))
פונקציית הדיווח על שגיאות בעת כישלון חילוץ expval (לדוגמה: ניסיון לחלץ מספר מתוך bool-val).
סטנדרטיזציה של שגיאות מערכת. חשוב במטלות בהן תתבקשו לממש מנגנון Strict Type Enforcement בזמן ריצה.
(define expval-extractor-error
(lambda (variant value)
(eopl:error ’expval-extractors "Looking for a ~s, found ~s "
variant value)))
רצף פורמלי של צעדי שערוך ביטוי חיסור המדגים כיצד ביטוי אחד תלוי בתוצאות השערוך של תת-הביטויים שמרכיבים אותו תחת סביבה מסוימת (rho).
תרגיל תיאורטי קלאסי במבחנים להצגת הבנה של 'סמנטיקה מבנית' (Operational Semantics). נדרש להראות צעד-אחר-צעד את המעבר מהתחביר המופשט לתוצאה הסופית.
(value-of-expression <<-(-(x,3),-(v,i)>> ρ) =
= (-
(value-of-expression <<-(x,3)>> ρ)
(value-of-expression <<-(v,i)>> ρ)) =
הרחבת ה-expval של השפה כך שיתמוך גם בפרוצדורות (proc-val). זהו השלב בו שפת התכנות שלנו הופכת לשפה עם פונקציות מסדר ראשון (First-Class Functions).
במבחן תמיד חשוב לבדוק האם השפה המבוקשת תומכת בפונקציות כערכים שעוברים בין משתנים. אם כן - חובה להוסיף proc-val ל-datatype של ExpVal ולכתוב מנבאים מתאימים.
;;; an expressed value is either a number, a boolean or a procv al.
(define-datatype expval expval?
(num-val
(value number?))
(bool-val
(boolean boolean?)))
;;; extractors:
(define expval->num
(lambda (v)
(cases expval v
(num-val (num) num)
(else (expval-extractor-error ’num v)))))
(define expval->bool
(lambda (v)
(cases expval v
(bool-val (bool) bool)
(else (expval-extractor-error ’bool v)))))
(define expval-extractor-error
(lambda (variant value)
(eopl:error ’expval-extractors "Looking for a ~s, found ~s "
variant value)))
הגדרת סביבת ההתחלה של המפרש (init-env). בדרך כלל היא כוללת משתני בסיס וקבועים שהמשתמש יכול לעבוד איתם מיד, כגון i=1, v=5, x=10.
הסביבה הזו מאפשרת להריץ תוכניות בדיקה מהר ללא צורך להגדיר המון משתנים ב-let התחלתי. במבחן לעיתים תתבקשו להרחיב סביבה זו עם פעולות בסיסיות (Primitives) נוספות.
;; init-env : -> Env
;; (init-env) builds an environment in which i is bound to the
;; expressed value 1, v is bound to the expressed value 5, and x is
;; bound to the expressed value 10.
(define init-env
(lambda ()
(extend-env
’i (num-val 1)
(extend-env
’v (num-val 5)
(extend-env
’x (num-val 10)
(empty-env))))))
פונקציית 'run' שמריצה תוכנית שלמה: היא מקבלת מחרוזת, מעבירה אותה דרך הפרסר (scan&parse) שיוצר AST, ואז מעבירה אותה לשערוך (value-of-program).
זוהי דלת הכניסה למפרש שלכם! כל תכנית שתיבדק תעבור דרך הפונקציה הזו. הבנת הזרימה (String -> AST -> ExpVal) חיונית להבנת תהליך הקומפילציה/אינטרפרטציה השלם.
;; run : String -> ExpVal
(define run
(lambda (string)
(value-of-program (scan>parse string))))
פונקציית המעטפת (value-of-program). מקבלת AST של תכנית שלמה, משחררת את הביטוי הפנימי ומריצה עליו את value-of יחד עם סביבת ההתחלה (init-env).
נקודת חיבור בין רמת ה-Program (שעשויה לכלול הצהרות יבוא או משתנים גלובליים בשפות מתקדמות) לבין רמת ה-Expression (הביטויים עצמם). חשוב במיוחד בשפת MODULES בה יש הרבה עבודת הכנה לפני שמגיעים לביטוי המרכזי.
;; value-of-program : Program -> ExpVal
(define value-of-program
(lambda (pgm)
(cases program pgm
(a-program (body)
(value-of body (init-env))))))
לב המפרש של שפת LET! פונקציית `value-of` מקבלת ביטוי וסביבה, ומבצעת Pattern Matching באמצעות `cases`. עבור let-exp למשל, היא מפענחת את הערך הימני (rhs) ומרחיבה את הסביבה לקראת שערוך ה-body.
מנוע המפרש. אם אתם צריכים לזכור רק פונקציה אחת בעל פה למבחן - זו הפונקציה. כמעט כל שאלה בבחינה של הקורס מבקשת להוסיף פיצ'ר חדש לשפה, וזה אומר שצריך להוסיף clause חדש בתוך ה-cases של ה-value-of שידע להתמודד איתו בצורה נכונה עם הסביבות.
;; value-of : Exp * Env -> ExpVal
(define value-of
(lambda (exp env)
(cases expression exp
(const-exp (num) (num-val num))
(var-exp (id) (apply-env env id))
(diff-exp (exp1 exp2)
(let ((val1
(expval->num
(value-of exp1 env)))
(val2
(expval->num
(value-of exp2 env))))
(num-val
(- val1 val2))))
(zero?-exp (exp1)
(let ((val1 (expval->num (value-of exp1 env))))
(if (zero? val1)
(bool-val #t)
(bool-val #f))))
(if-exp (exp0 exp1 exp2)
(if (expval->bool (value-of exp0 env))
(value-of exp1 env)
(value-of exp2 env)))
(let-exp (id rhs body)
(let ((val (value-of rhs env)))
(value-of body
(extend-env id val env))))
)))
רשימת בדיקות (test-list) אוטומטיות הבודקות פעולות אריתמטיות פשוטות ומקוננות, וכן בדיקת קשירת משתנים פשוטים דרך הסביבה.
מספק דוגמה לאופן שבו כותבים תכניות קטנות כדי לוודא שכל תוספת לשפה (כגון 'minus' או 'var') עובדת כראוי ללא רגרסיות.
(define test-list
’(
;; simple arithmetic
(positive-const "11" 11)
(negative-const "-33" -33)
(simple-arith-1 "-(44,33)" 11)
;; nested arithmetic
(nested-arith-left "-(-(44,33),22)" -11)
(nested-arith-right "-(55, -(22,11))" 44)
;; simple variables
(test-var-1 "x" 10)
(test-var-2 "-(x,1)" 9)
(test-var-3 "-(1,x)" -9)
המשך רשימת הבדיקות - מתמקד בבדיקת שגיאות (כמו משתנה לא מוגדר foo), ובדיקת תנאים (if) ו-let מרובה (nested) והסתרה (shadowing).
חשוב למבחן: להבין איך עובד המנגנון של Shadowing ב-let (מתי משתנה פנימי מסתיר משתנה חיצוני בעל אותו שם) ואיך התחימה הלקסיקלית נאכפת. בנוסף, חשוב לוודא ש-if אכן מדלג על שערוך הענף שלא נבחר (Short-Circuit Evaluation).
;; simple unbound variables
(test-unbound-var-1 "foo" error)
(test-unbound-var-2 "-(x,foo)" error)
;; simple conditionals
(if-true "if zero?(0) then 3 else 4" 3)
(if-false "if zero?(1) then 3 else 4" 4)
;; test dynamic typechecking
(no-bool-to-diff-1 "-(zero?(0),1)" error)
(no-bool-to-diff-2 "-(1,zero?(0))" error)
(no-int-to-if "if 1 then 2 else 3" error)
;; make sure that the test and both arms get evaluated
;; properly.
(if-eval-test-true "if zero?(-(11,11)) then 3 else 4" 3)
(if-eval-test-false "if zero?(-(11, 12)) then 3 else 4" 4)
;; and make sure the other arm doesn’t get evaluated.
(if-eval-test-true-2 "if zero?(-(11, 11)) then 3 else foo" 3)
(if-eval-test-false-2 "if zero?(-(11,12)) then foo else 4 " 4)
;; simple let
(simple-let-1 "let x = 3 in x" 3)
;; make sure the body and rhs get evaluated
(eval-let-body "let x = 3 in -(x,1)" 2)
(eval-let-rhs "let x = -(4,1) in -(x,1)" 2)
;; check nested let and shadowing
(simple-nested-let "let x = 3 in let y = 4 in -(x,y)" -1)
(check-shadowing-in-body "let x = 3 in let x = 4 in x" 4)
(check-shadowing-in-rhs "let x = 3 in let x = -(x,1) in x" 2)
))
קטע ה-cases מתוך value-of אשר מטפל בפרוצדורות (proc-exp) וקריאה להן (call-exp). עבור proc-exp נוצר 'סגור' (closure) באמצעות הפונקציה procedure השומרת את הסביבה. עבור קריאה, משערכים את האופרנדים וקוראים ל-apply-procedure.
זהו השלב החשוב ביותר בהבנת פונקציות מסדר ראשון! המושג Closure הוא קריטי. חייבים לדעת שכאשר אנו בונים פונקציה, אנו "מקפיאים" ושומרים את הסביבה בה היא נוצרה (env), ולא את הסביבה בה היא נקראת.
(define value-of
(lambda (exp env)
(cases expression exp
...
(proc-exp (bvar body)
(proc-val
(procedure bvar body env)))
(call-exp (rator rand)
(let ((proc (expval->proc (value-of rator env)))
(arg (value-of rand env)))
(apply-procedure proc arg)
)))))
הדגמה של רצף שערוך תיאורטי של שימוש ב-let שבו מוגדרות פונקציות (f ו-g). ממחיש בצורה מתמטית איך הסביבה משתנה בכל שלב ואיך הסגורים נבנים עם הסביבות המתאימות.
שאלות מהסוג הזה נפוצות בחלק התיאורטי של המבחן. הן באות לבדוק האם הסטודנט מבין שתחימה לקסיקלית מחייבת לשמור את ערכי הסביבה בזמן יצירת ה-proc.
(value-of <<let x = 200
in let f = proc (z) -(z,x)
in let x = 100
in let g = proc (z) -(z,x)
in -((f 1), (g 1))>>
ρ) =
= (value-of <<let f = proc (z) -(z,x)
in let x = 100
in let g = proc (z) -(z,x)
in -((f 1), (g 1))>>
[x=(num-val 200)] ρ) =
= (value-of <<let x = 100
in let g = proc (z) -(z,x)
in -((f 1), (g 1))>>
[f=(proc-val (procedure z <<-(z,x)>> [x=(num-val 200)] ρ))]
[x=(num-val 200)] ρ) =
= (value-of <<let g = proc (z) -(z,x)
in -((f 1), (g 1))>>
[x=(num-val 100)]
[f=(proc-val (procedure z <<-(z,x)>> [x=(num-val 200)] ρ))]
[x=(num-val 200)] ρ) =
= (value-of <<-((f 1), (g 1))>>
[g=(proc-val (procedure z
<<-(z,x)>>
[x=(num-val 100)]
[f=(proc-val (procedure z <<-(z,x)>> [x=(num-val 200)] ρ))]
[x=(num-val 200)] ρ))]
[x=(num-val 100)]
[f=(proc-val (procedure z <<-(z,x)>> [x=(num-val 200)] ρ))]
[x=(num-val 200)] ρ)
מימוש פונקציונלי-טהור של 'סגור' (Closure). הפונקציה procedure מחזירה פונקציית lambda של Scheme ששומרת את הסביבה (env), ואילו apply-procedure פשוט מפעילה את אותה למדא עם הארגומנט שסופק.
ממחיש איך אפשר לנצל את הסגורים המובנים של השפה המארחת (Scheme) כדי לממש סגורים בשפה המפורשת שלנו. במבחן לעיתים תתבקשו לממש מנגנון דומה לשפות אחרות (כגון שפות תיאורטיות).
;; procedure : Symbol * Exp * Env -> Proc
(define procedure
(lambda (bvar body env)
(lambda (arg)
(value-of body (extend-env bvar arg env)))))
;; apply-procedure : Proc * ExpVal -> ExpVal
(define apply-procedure
(lambda (proc arg)
(proc arg)))
המרת המימוש הפונקציונלי של ה'סגור' למימוש מבוסס מבני נתונים (datatype). procedure שומרת מפורשות את המשתנה, גוף הפונקציה והסביבה. apply-procedure מפרקת אותם עם cases ומבצעת value-of עם הסביבה המורחבת.
מימוש זה הוא המימוש הסטנדרטי להמשך הקורס. הוא קל יותר לדיבוג והדפסה, והכרחי כדי לאפשר למפרש לתמוך בהרחבות מורכבות יותר. יש לזכור את המבנה הזה בעל פה.
(define-datatype proc proc?
(procedure
(bvar symbol?)
(body expression?)
(env environment?)))
(define apply-procedure
(lambda (proc1 arg)
(cases proc proc1
(procedure (bvar body saved-env)
(value-of body (extend-env bvar arg saved-env))))))
עדכון ה-expval של השפה שלנו כדי שיוכל להחזיק בתוכו פרוצדורות (proc-val). כך פונקציות הופכות לערכים (First-Class Values) שאפשר להעביר למשתנים ולהחזיר מפונקציות אחרות.
הכרחי ברגע שהוספנו את שפת PROC. אל תשכחו להוסיף את המחלץ (expval->proc) כדי לאפשר לקרוא לפונקציות ב-call-exp.
;;; an expressed value is either a number, a boolean or a procv al.
(define-datatype expval expval?
(num-val
(value number?))
(bool-val
(boolean boolean?))
(proc-val
(proc proc?)))
קטע מתוך ה-value-of אשר מציג את השערוך של קריאה לפונקציה. תחילה משערכים את הפונקציה (rator), מחלצים את ה-proc-val, לאחר מכן משערכים את הארגומנט, ומפעילים.
ממחיש Call-by-Value. הארגומנט משוערך לחלוטין *לפני* שהוא מועבר לפונקציה. אם תתבקשו במבחן לממש Call-by-Reference או Call-by-Name, קוד זה ישתנה באופן דרסטי!
;; value-of : Exp * Env -> ExpVal
(define value-of
(lambda (exp env)
(cases expression exp
...
(proc-exp (var body)
(proc-val (procedure var body env)))
(call-exp (rator rand)
; (let ((proc (expval->proc (value-of rator env)))
; (arg (value-of rand env)))
(let ((val (value-of rator env))
(arg (value-of rand env)))
(let ((proc (expval->proc val)))
(apply-procedure proc arg)))
)))
דוגמה לתוכנית המשתמשת בפקודת `let*`. להבדיל מ-let רגיל בו כל הביטויים משוערכים ביחס לסביבה הקודמת, ב-let* כל משתנה משוערך בסביבה הכוללת את המשתנים שהוגדרו לפניו באותו בלוק.
let* הוא פיצ'ר קלאסי שמתבקשים לממש במבחנים (בדרך כלל כ-Macro או על ידי פירוק ל-let מקוננים). חשוב להבין את סדר השערוך הליניארי שלו.
let
x=30
in
let*
x= -(x,1)
y= -(x,2)
in
-(x,y)
Expression ::= cond{Expression ==> Expression}∗end
cond-exp (exps1 exps2)
דוגמה לתוכנית המשתמשת בפקודת `cond`. מבנה הבקרה הזה מאפשר ריבוי תנאים, כשהתנאי הראשון שמחזיר אמת יגרום לשערוך הביטוי המשויך אליו.
כדי לממש cond במפרש, תצטרכו להוסיף צומת מיוחד ל-AST שמכיל שתי רשימות: רשימת התנאים ורשימת התוצאות, ולרוץ עליהן ברקורסיה ב-value-of.
let
x=5
in
cond
zero? (x) ==> 7
zero? (-(x,5)) ==> 12
zero? (-(x,2)) ==>70
end
Expression ::= foldExpressionExpression [{Expression}∗(,)]
fold-exp (proc1
דוגמה לתכנית בשפה שלנו שמשתמשת במבנה `compose` שמפעיל פונקציות בשרשרת זו על גבי תוצאתה של זו.
תרגיל נפוץ במבחן מבקש לממש הרכבת פונקציות (Function Composition). יש להבין כיצד לחלץ את סדרת הפרוצדורות ולהעביר את התוצאה מאחת לשנייה (למשל בעזרת רשימות).
let
p1 = proc (x) -(x, -(0,3))
in
let
p2 = proc (x) -(x, 7)
in
compose [p1 ; p2; proc (x) {(10,x)] 5
דוגמה לשימוש בהפניות מפורשות (Explicit References) בשפת IMPLICIT/EXPLICIT REFS. התכנית משתמשת ב-newref, deref ו-setref! כדי לממש פונקציות odd ו-even שקוראות אחת לשנייה ומעדכנות את אותו מצביע.
הדוגמה הראשונה הממחישה הוספת State (מצב וזיכרון) לשפה שלנו. התוכנית כבר אינה פונקציונלית טהורה. חשוב להבין במבחן את ההבדל בין השמה ל-Environment (שאינה אפשרית לרוב) לעומת השמה ל-Store (זיכרון) בעזרת setref!.
let x = newref(0)
in letrec even(dummy)
= if zero?(deref(x))
then zero?(0)
else begin
setref(x, -(deref(x),1));
(odd 888)
end
odd(dummy)
= if zero?(deref(x))
then zero?(1)
else begin
setref(x, -(deref(x),1));
(even 888)
end
in begin setref(x,13); (odd 888) end
דוגמה לפונקציה היוצרת 'מצב פנימי' (Internal State) חבוי בתוך סגור (Closure). המשתנה counter מוקצה בזיכרון, והפונקציה g שניצור תוכל לעדכן אותו בכל קריאה.
זהו דפוס של Object-Oriented זעיר בתוך שפה פונקציונלית. המשתנה counter זמין רק ל-g. במבחן, ייתכן שתידרשו לייצר מונים (Counters) או אובייקטים בעלי מצב הנשמרים לאורך זמן בעזרת newref.
let g = let counter = newref(0)
in proc (dummy)
begin
setref(counter, -(deref(counter), -1)) ;
deref(counter)
end
in let a = (g 11)
in let b = (g 11)
in let c = (g 11)
in c
let x = newref(newref(0))
in begin
setref(deref(x), 321) ;
deref(deref(x))
end
הדגמה של הפניה המצביעה להפניה אחרת (pointer to pointer). אנחנו יוצרים תא זיכרון שמכיל תא זיכרון המכיל 0, ואז מעדכנים את תוכנו של התא הפנימי.
ממחיש את הגמישות של הטיפוס expval: הוא יכול להכיל רפרנס שמצביע בתורו לרפרנס אחר. במטלות תתבקשו לשרטט במדויק איך יראה הזיכרון (Store) לאחר הרצת תוכנית כזו.
let x = newref(newref(0))
in begin
setref(deref(x), 321) ;
deref(deref(x))
end
פונקציית value-of-program מעודכנת לשפות עם State. השינוי הקריטי כאן הוא הקריאה ל-(initialize-store!) שמנקה את הזיכרון לפני שהתכנית רצה.
כל שפה שמשתמשת בהפניות חייבת לנקות את הזיכרון בתחילת הריצה. אם לא תוסיפו קריאה זו במבחן בעת מימוש שפת EXPLICIT-REFS, המפרש יצבור מידע ישן ויכשל בטסטים. תמיד זכרו לאתחל מצב גלובלי!
;; value-of-program : Program -> ExpVal
(define value-of-program
(lambda (pgm)
(initialize-store!) ; new for explicit refs.
(cases program pgm
(a-program (exp1)
(value-of exp1 (init-env))))))
מימוש הזיכרון (Store) כרשימה פשוטה של Scheme. הפניה (ref) היא למעשה רק המספר המייצג את האינדקס ברשימה. פונקציות הניהול הבסיסיות (the-store, empty-store).
מימוש לא יעיל אך קל להבנה. במבחן, לרוב לא תידרשו לכתוב אותו מחדש, אך חובה להבין שה-Store הגלובלי מאפשר תופעות לוואי (Side Effects) שמפרות את השקיפות הרפרנציאלית (Referential Transparency).
;;; World’s dumbest implementation of stores:
;;; a store is a list and
;;; a reference is an integer index into the list.
;;; the-store : Store
;;; the-store is the current store
(define the-store (empty-store))
;;; reference? : SchemeVal -> Boolean
;;; (reference? x) is true iff x represents a location
(define reference? integer?)
;;; initialize-store! : -> Unspecified
;;; (initialize-store!) sets the store to the empty store
(define initialize-store!
(lambda ()
(set! the-store (empty-store))))
;;; empty-store : -> Store
;;; (empty-store) returns an empty store
(define empty-store
(lambda () ’()))
מימוש של הפונקציה newref המקצה תא זיכרון חדש. היא משרשרת את הערך לסוף הרשימה (append) ומחזירה את האינדקס החדש (אורך הרשימה הקודם).
פעולת ההקצאה היא לב ליבו של ניהול הזיכרון. במערכת כזו הזיכרון רק הולך וגדל (אין שחרור / Garbage Collection). שימו לב ש-newref משנה מפורשות את המשתנה הגלובלי the-store באמצעות set!.
;;; newref : ExpVal -> Ref
;;; (newref v) returns a newly allocated reference
;;; initialized to v
(define newref
(lambda (val)
(let ((next-ref (length the-store)))
(set! the-store
(append the-store (list val)))
(if (instrument-newref)
(eopl:printf
"newref: allocating location ~s~%"
next-ref))
next-ref)))
מימוש של הפונקציה deref הקוראת תוכן של תא זיכרון. היא משתמשת בפונקציית list-ref המובנית של Scheme כדי לשלוף את האיבר באינדקס הנתון מתוך the-store.
פעולה או-של-אחד במימוש אופטימלי, אך כאן (בגלל רשימה מקושרת) היא O(n). אם תתבקשו במבחן לשנות את ה-Store למערך רציף (vector), תצטרכו לעדכן את הפונקציה הזו ל-vector-ref.
;;; deref : Ref -> ExpVal
;;; (deref ref) returns the value currently contained in ref
(define deref
(lambda (ref) (list-ref the-store ref)))
מימוש הפונקציה setref! הדורסת ערך קיים בתא הזיכרון. מכיוון שה-Store מיוצג כרשימה, היא בונה בעזרת setref-inner רשימה חדשה לגמרי שזהה לקודמת למעט האיבר הספציפי שהוחלף.
זו אחת הפעולות הכי יקרות מבחינת ביצועים במימוש שלנו. חשוב להבין את הרקורסיה של setref-inner שעוברת על הרשימה. במטלות שדרוג ביצועים, כדאי להחליף את הרשימה במבנה נתונים שיודע להתעדכן במקום (In-Place Mutation).
;;; setref! : Ref * ExpVal -> Unspecified
;;; (setref! ref val) changes the current store
;;; by storing val into ref
(define setref!
(lambda (ref0 val)
(set! the-store
;; setref-inner : Store * Ref -> Store
;; (setref-inner sigma l) returns a store
;; that is like sigma except that,
;; in the store returned by setref-inner,
;; the location l contains val.
(letrec ((setref-inner (lambda (store ref)
(cond
((null? store)
(eopl:error ’setref
"illegal reference"))
((zero? ref)
(cons val (cdr store)))
(else
(cons
(car store)
(setref-inner (cdr store)
(- ref 1)))))))))
(setref-inner the-store ref0)))))
שערוך ביטויי הפניות בתוך value-of. עבור newref יוצרים תא חדש, עבור deref שולפים, ועבור setref משנים. חשוב: setref מחזירה ערך סרק (למשל num-val 23) כי בשפתנו פקודה חייבת להחזיר ExpVal.
תזכרו תמיד: setref מחזירה ערך 'מיותר'. באג נפוץ במבחן הוא לשכוח להחזיר expval אחרי ביצוע פעולת לוואי (כמו שינוי זיכרון או הדפסה) מה שיגרום למפרש לקרוס.
(newref-exp (exp1)
(let ((v1 (value-of exp1 env)))
(ref-val (newref v1))))
(deref-exp (exp1)
(let ((v1 (value-of exp1 env)))
(let ((ref1 (expval->ref v1)))
(deref ref1))))
(setref-exp (exp1 exp2)
(let ((ref (expval->ref (value-of exp1 env))))
(let ((v2 (value-of exp2 env)))
(begin
(setref! ref v2)
(num-val 23)))))
הוספת הטיפוס ref-val ל-datatype של expval, מה שמאפשר לערכים בזמן ריצה להיות 'מצביעים' לזיכרון (Store).
השינוי הקטן הזה הוא שמאפשר למערכת שלנו לדמות שפות מודרניות כמו C בהן פוינטרים הם ערכים לגיטימיים שאפשר לשמור במשתנים.
;;; an expressed value is either a number, a boolean, a procva l, or a
;;; reference.
(define-datatype expval expval?
(num-val
(value number?))
(bool-val
(boolean boolean?))
(proc-val
(proc proc?))
(ref-val
(ref reference?))
)
מחלץ עבור ref-val בדומה למחלצי המספרים והבוליאנים. מוודא שהערך הנתון הוא אכן מצביע חוקי לזיכרון.
נחוץ לפני כל קריאה ל-deref או setref ב-value-of כדי למנוע שגיאות.
(define expval->ref
(lambda (v)
(cases expval v
(ref-val (ref) ref)
(else (expval-extractor-error ’reference v)))))
הגדרת datatype של סביבה (Environment) עם פיצ'ר של letrec (רקורסיה) מובנה (extend-env-rec*).
דרך מתקדמת ויעילה יותר לאפשר פונקציות רקורסיביות, על ידי כך שהסביבה מכילה מפורשות הצהרה על מבנים רקורסיביים מרובים שמשפיעים זה על זה (Mutual Recursion).
(define-datatype environment environment?
(empty-env)
(extend-env
(bvar symbol?)
(bval reference?) ; new for implicit-refs
(saved-env environment?))
(extend-env-rec*
(proc-names (list-of symbol?))
(b-vars (list-of symbol?))
(proc-bodies (list-of expression?))
(saved-env environment?)))
פונקציית env->list שלוקחת סביבה מופשטת וממירה אותה לרשימת סקים מסודרת וקריאה, לשם הדפסה (debugging).
כלי מעולה לדיבוג. כשמפרש נופל בגלל משתנה שלא נמצא, הדפסת הסביבה עם הפונקציה הזו תעזור לכם לראות מיד אילו קשירות חסרות. מומלץ להשתמש בזה בעת פתרון המטלות!
;; env->list : Env -> List
;; used for pretty-printing and debugging
(define env->list
(lambda (env)
(cases environment env
(empty-env () ’())
(extend-env (sym val saved-env)
(cons
(list sym val) ; val is a denoted value-- a
; reference.
(env->list saved-env)))
(extend-env-rec* (p-names b-vars p-bodies saved-env)
(cons
(list ’letrec p-names ’...)
(env->list saved-env))))))
פונקציה שלוקחת ערך ExpVal ומכינה אותו להדפסה, במיוחד מנקה פרוצדורות שלוקחות איתן את כל הסביבה (מה שעלול לגרום להדפסות ענק או אינסופיות בגלל רקורסיה).
בלי ניקוי של הסביבות בהדפסת פרוצדורות, סביבה המכילה letrec יכולה להכניס את פונקציית ההדפסה המובנית של Scheme ללולאה אינסופית בגלל הפניה מעגלית.
;; expval->printable : ExpVal -> List
;; returns a value like its argument, except procedures get c leaned
;; up with env->list
(define expval->printable
(lambda (val)
(cases expval val
(proc-val (p)
(cases proc p
(procedure (var body saved-env)
(list ’procedure var ’... (env->list saved-env)))))
(else val))))
הגדרת המשתנה הגלובלי the-store כלא-מאתחל, ופונקציות האתחול שמקצות עבורו רשימה ריקה בתחילת ריצת כל תוכנית.
מדגיש שמפרש שומר מצב מחוץ לסביבת השערוך הנקייה. כל פעולת value-of עשויה כעת להיות בעלת תוצאות שונות בהתאם למצבו של the-store.
;; the-store: a Scheme variable containing the current stat e of the
;; store. Initially set to a dummy variable.
(define the-store ’uninitialized)
;; empty-store : () -> Sto
;; Page: 111
(define empty-store
(lambda () ’()))
;; initialize-store! : () -> Sto
;; usage: (initialize-store!) sets the-store to the empty- store
;; Page 111
(define initialize-store!
(lambda ()
(set! the-store (empty-store))))
פונקציה פשוטה המחזירה את ה-Store הנוכחי. כתוב שהיא מיושנת מכיוון שבהמשך החלפנו אותה בפורמט הדפסה יפה יותר.
חלק ממנגנון הדיבוג הפנימי. תזכורת טובה לנקות קוד לא בשימוש.
;; get-store : () -> Sto
;; Page: 111
;; This is obsolete. Replaced by get-store-as-list below
(define get-store
(lambda () the-store))
פונקציה הבודקת האם ערך (ב-Scheme) הוא מצביע זיכרון חוקי (במימוש שלנו, פשוט מוודאת שהוא מספר שלם המייצג אינדקס).
במבחן תתבקשו לעיתים לשנות את ה-Store למילון (Hash Table) ואז תצטרכו לעדכן פונקציה זו לבדוק שזהו מפתח חוקי במילון.
;; reference? : SchemeVal -> Bool
;; Page: 111
(define reference?
(lambda (v)
(integer? v)))
גרסה משופרת של הפונקציה newref שכוללת גם הדפסת דיבוג (Tracing) בעת הקצאת זיכרון, אם מתג הדיבוג (instrument-newref) פעיל.
מנגנון הדפסה מותנה הוא כלי קריטי להבנת זרימת התוכנית (Control Flow). במבחנים מסוימים תתבקשו להוסיף יכולות Profiling כאלו כדי לספור למשל כמה הקצאות הזיכרון בוצעו.
;; newref : ExpVal -> Ref
;; Page: 111
(define newref
(lambda (val)
(let ((next-ref (length the-store)))
(set! the-store
(append the-store (list val)))
(if (instrument-newref)
(eopl:printf
"newref: allocating location ~s with initial contents ~s~% "
next-ref val))
next-ref)))
קריאה זהה של תא מהזיכרון. אין שום הבדל לוגי מקטעי הקוד הקודמים.
נראה כאן בעיקר לצורך שלמות המימוש יחד עם הגרסאות המשופרות של הפונקציות האחרות.
;; deref : Ref -> ExpVal
;; Page 111
(define deref
(lambda (ref)
(list-ref the-store ref)))
גרסה משופרת ל-setref! שכוללת שגרת שגיאה אינפורמטיבית יותר (report-invalid-reference) למקרה שמנסים לכתוב לכתובת זיכרון שלא קיימת.
טיפול בשגיאות בצורה קפדנית עוזר מאוד בשלב הבדיקות. אם במבחן מבקשים 'להוסיף טיפול בשגיאות' למבנה הנתונים - זו הדוגמה לכך.
;; setref! : Ref * ExpVal -> Unspecified
;; Page: 112
(define setref!
(lambda (ref val)
(set! the-store
(letrec
((setref-inner
;; returns a list like store1, except that position ref1
;; contains val.
(lambda (store1 ref1)
(cond
((null? store1)
(report-invalid-reference ref the-store))
((zero? ref1)
(cons val (cdr store1)))
(else
(cons
(car store1)
(setref-inner
(cdr store1) (- ref1 1))))))))
(setref-inner the-store ref)))))
הדפסת ה-Store באופן יפה כרשימה של תתי-רשימות (כתובת וערך), כך שקל לקרוא ולהבין את מצב הזיכרון ברגע נתון.
כלי הדיבוג החזק ביותר במטלות של שפת IMPLICIT/EXPLICIT REFS. הדפסת הזיכרון לפני ואחרי כל פקודת השמה מאפשרת לוודא שניהול הזיכרון תקין לחלוטין.
;; get-store-as-list : () -> Listof(List(Ref,Expval))
;; Exports the current state of the store as a scheme list.
;; (get-store-as-list ’(foo bar baz)) = ((0 foo)(1 bar) (2 ba z))
;; where foo, bar, and baz are expvals.
;; If the store were represented in a different way, this woul d be
;; replaced by something cleverer.
;; Replaces get-store (p. 111)
(define get-store-as-list
(lambda ()
(letrec
((inner-loop
;; convert sto to list as if its car was location n
(lambda (sto n)
(if (null? sto)
’()
(cons
(list n (car sto))
(inner-loop (cdr sto) (+ n 1)))))))
(inner-loop the-store 0))))
)
דוגמה בשפת C להמחשת טיפול ישיר במצביעים ובזיכרון. הקוד מבצע פעולות על Pointer (כולל אריתמטיקת מצביעים p+1).
נועד להשוואה מול שפות המנהלות את הזיכרון באופן אוטומטי. מדגים סכנות של גישה לזיכרון לא חוקי שאין בשפה שלנו. ייתכן שיופיע כדוגמה לשאלת הבנה בנושאי בטיחות-טיפוסים (Type Safety).
#include <stdio.h>
#include <stdlib.h>
doublef (double* p) {
*p = 3.14159;
return*p;
}
intmain (intargc,char* argv[]) {
intn = 12345;
double* p = (void*) &n;
doublex = f(p);
doubley = f(p+1);
doublez = f(p+2);
printf ( "n = %d\n" , n);
printf ( "x = %lf\n" , x);
}
דוגמה למבנה תוכנית בשפת CHECKED - שפה סטטית שמכריחה את המתכנת לכתוב במפורש את הטיפוסים (כמו int או bool) בהגדרת פרמטרים ופונקציות.
ממחיש איך המפרש שלנו עכשיו לא רק מריץ (run) אלא קודם מוודא נכונות טיפוסים (Type Checking) לפני ההרצה. במבחן לעיתים תתבקשו להוסיף סוג טיפוס חדש ל-checker.
proc (x : int) -(x,1)
letrec
int double (x : int) = if zero?(x)
דוגמה מורכבת יותר של שפת CHECKED בה פונקציות מוחזרות מתוך פונקציות אחרות (Currying), ויש להקפיד לכתוב את חתימת הטיפוס המלאה (למשל int -> int) לכל שלב.
מספק דוגמה נהדרת לשערוך טיפוס של פונקציות מסדר גבוה. חשוב לדעת לבדוק האם חתימת טיפוס מסוימת תואמת לאופן בו הפונקציה נקראת בקוד.
let a = proc (i) proc (j) -(i, -(0,j))
in letrec m (x) = proc (y) if zero?(x)
then 0
else ((a ((m -(x,1) y)) y)
in ((m 11) 12)
.(7.2 הנהמשתניהטיפוסשנשייךלכלמשתנהקשור)
הדגמת שימוש בסיסי בסביבת ההרצה של Racket (חלון ה-REPL). המשתמש מזין פקודות והמפרש מחזיר את התוצאה מיד.
שימושי מאוד לשלב התרגול בבית - לא יופיע בדרך כלל במבחן תיאורטי.
> (+ 2 3)
5
> +
#<Procedure>
> (define x 3)
> x
3
> (+ (* x x) (* 9 9)) 90)
180
הפונקציה המוכרת list-length מסקים. מחשבת אורך של רשימה בצורה רקורסיבית.
הדגמה נוספת לשימוש ברקורסיה זנבית / רגילה על מבנה נתונים רקורסיבי (רשימה מורכבת מ-cons של איבר ועוד רשימה).
(define list-length
(lambda (lst)
(if (null? lst)
0
(+ 1 (list-length (cdr lst))))))
((> lambda (x) 0) ’a)
0
תחביר ההוראה `cond` בסקים. מאפשר ריבוי משפטי תנאי אחד אחרי השני, בדומה ל-switch/case בשפות מודרניות, ומסתיים ב-else.
הוראה זו נמצאת כמעט בכל פונקציית parse או ערך במפרש. חובה להכיר ולזכור שה-else מתבצע רק אם כל התנאים הקודמים כשלו.
(cond (<test> <expression>)
(<test> <expression>)
...
(else <expression>) )
פונקציית member? מובנית לבדיקת השתייכות איבר לרשימה. מסיירת על איברי הרשימה ומשווה עם equal?.
מבנה זה הוא בסיס לפונקציות חיפוש בסביבה (כמו חיפוש ב-Env הפרוצדורלי או מבוסס הרשימות). תמיד יש צורך לבדוק 'האם המפתח שווה לאיבר הנוכחי?'.
(define member?
(lambda (a lst)
(cond ((null? lst) #f)
((equal? (car lst) a) #t)
(else (member? a (cdr lst))))))
מימוש פונקציית כפל (times) באמצעות פעולות אבסטרקטיות של הפשטת מספרים (plus, predecessor).
תרגיל מבחן קלאסי: כתיבת פונקציות רקורסיביות בלי להשתמש בפקודות המובנות של סקים, אלא רק בבנאים והמחלצים האבסטרקטיים שהוגדרו מראש.
(define times
(lambda (x y)
(if (is-zero? x) (zero)
(plus y (times (predecessor x) y)))))
פתרון פונקציית הניתוח parse-expression הממירה S-expression לעץ תחביר AST עם הבנאים (var-exp, lambda-exp, app-exp).
כדי לתרגל למבחן - חובה לכתוב את הפונקציה הזו לפחות פעם אחת בעצמכם כדי להפנים כיצד עובדים עם רשימות מקוננות.
;; parse-expression: SchemeVal -> LcExp
(define parse-expression
(lambda (datum)
(cond
((symbol? datum) (var-exp datum))
((pair? datum)
(if (eqv? (car datum) ’lambda)
(lambda-exp (caadr datum)
(parse-expression (caddr datum)))
(app-exp
(parse-expression (car datum))
(parse-expression (cadr datum)))))
(else (eopl:error ’parse-expression
"Invalid concrete syntax ~s" datum)))))
הפונקציה ההפוכה: unparse-expression. לוקחת AST ומחזירה את הקוד לצורת רשימות קריאות להדפסה (S-expressions).
כלי מעולה לדיבוג - השתמשו בו כשאתם מסתבכים במטלות עם הדפסת העצים והשגיאות כדי לראות מה באמת קיבלתם.
;; unparse-expression: LcExp -> SchemeVal
(define unparse-expression
(lambda (exp)
(cases lc-exp exp
(var-exp (id) id)
(lambda-exp (id body)
(list ’lambda (list id)
(unparse-expression body)))
(app-exp (rator rand)
(list (unparse-expression rator)
(unparse-expression rand))))))
(eopl:error ’apply-env "No binding for ~s" search-var)))
פתרון לתרגיל מעקב - הדגמת רצף שערוך תיאורטי של פונקציית let וקריאה ל-closure, צעד-אחר-צעד, עד לקבלת התוצאה 11.
סעיפי חובה במבחנים רבים! דורש הבנה עמוקה של האופן בו פונקציות נשמרות (יחד עם הסביבה שבה *הוגדרו*) ואז משוערכות (יחד עם הסביבה שבה *נקראו*).
(value-of <<let f = proc(z) -(z,x) in (f 21)>> ρ) =
= (value-of <<(f 21)>>
[f=(value-of <<proc(z) -(z,x)>> ρ)]ρ) =
= (value-of <<(f 21)>>
[f=(value-of (procedure z <<-(z,x)>> ρ)]ρ) =
= (apply-procedure
(value-of <<f>> [f=(procedure z <<-(z,x)>> ρ)]ρ)
(value-of <<21>> [f=(procedure z <<-(z,x)>> ρ)]ρ))=
= (apply-procedure
(procedure z <<-(z,x)>> ρ)
21)=
= (value-of <<-(z,x)>> [z=21] ρ)
= (- (value-of <<z>> [z=21] ρ)
(value-of <<x>> [z=21] ρ))
= (- 21 10)
= 11
/square
הדגמת רצף שערוך נוספת, הפעם עם משתנה x שמסתיר (Shadowing) קשירה חיצונית של פונקציה. מספק הדגמה נוספת לחשיבות התחימה הלקסיקלית.
ממחיש איך טיפול לא נכון בשמות משתנים היה גורם לבאג קריטי, ומדוע הסביבות נשמרות כמחסנית מסודרת לפתרון בעיית ההסתרה.
(value-of <<let x = proc(z) -(z,x) in (f 21)>> ρ) =
= (value-of <<(x 21)>>
[f=(value-of <<proc(z) -(z,x)>> ρ)]ρ) =
= (value-of <<(x 21)>>
[x=(value-of (procedure z <<-(z,x)>> ρ)]ρ) =
= (apply-procedure
(value-of <<x>> [x=(procedure z <<-(z,x)>> ρ)]ρ)
(value-of <<21>> [x=(procedure z <<-(z,x)>> ρ)]ρ))=
= (apply-procedure
(procedure z <<-(z,x)>> ρ)
21)=
= (value-of <<-(z,x)>> [z=21] ρ)
= (- (value-of <<z>> [z=21] ρ)
(value-of <<x>> [z=21] ρ))
= (- 21 10)
= 11
/square
חלק א' בשאלות מבחן קלאסיות - עדכון התחביר! כאן נוסיף לפרסר את ההוראה let* המאפשרת הגדרת משתנים הדרגתית, באמצעות שפת הגדרת הדקדוק (SLLGEN).
כל שאלת מבחן שמבקשת להרחיב את השפה תדרוש מכם קודם כל לתאר כיצד ייראה ה-AST של הפקודה החדשה. כאן נדרש להשתמש ברשימת מזהים (arbno). השלב הראשון לכל פתרון.
("let*" (arbno identifier "=" expression) "in" expression )
let*-exp)
חלק ב' של הרחבת let* - עדכון פונקציית `value-of`. שימו לב לפריסה ההדרגתית (רקורסיבית) של הסביבה עבור כל קשירת משתנה, בניגוד ל-let רגיל שמחשב את הכל באותה סביבה מקורית.
זוהי לוגיקת הליבה! רוב הניקוד במבחן יינתן על השורות הללו. הבנה ברורה של ההבדל הלוגי בין הרחבה מצטברת להרחבה מקבילה היא קריטית.
(let*-exp (vars exps body)
(if (null? vars)
(value-of body env)
(let* ((v1 (value-of (car exps) env))
(e1 (extend-env (car vars) v1 env)))
(value-of (let*-exp (cdr vars) (cdr exps) body) e1))))
עדכון התחביר עבור הפקודה cond - המאפשרת רשימה פתוחה של תנאים (arbno) הממופים לתוצאות באמצעות החץ ==>.
עוד דוגמה קלאסית להרחבת שפה בעזרת רשימות של ביטויים (arbno).
("cond" (arbno expression "==>" expression) "end")
cond-exp)
מימוש הלוגיקה (value-of) של cond. הפונקציה עוברת ברקורסיה על רשימת התנאים עד שהיא מוצאת אחד שהוא #t ואז משערכת ומחזירה את הביטוי התואם לו.
אם שום תנאי לא אמת (הרשימה מתרוקנת) הפונקציה מחזירה שגיאה - חשוב לאתר מקרי קצה במבחן. זוהי קלאסיקה של שימוש בפונקציות רשימה כמו car ו-cdr מתוך ה-value-of.
(cond-exp (exps1 exps2)
(if (null? exps1)
(display "error: cond error")
(if (expval->bool (value-of (car exps1) env))
(value-of (car exps2) env)
(value-of (cond-exp (cdr exps1) (cdr exps2)) env))))
תוספת לשפה של פונקציית הקיפול fold. מאפשר להעביר פונקציה, ערך התחלתי, ורשימה של ביטויים לקיפול.
הדגמת שימוש בתחביר מתקדם עם פסיקים כמפרידים ב-SLLGEN בעזרת separated-list.
("fold" expression
expression
"[" (separated-list expression ",") "]" )
fold-exp)
מימוש מורכב מאוד של פונקציית הקיפול fold ב-value-of. עושה שימוש משולב בהרצת הפרוצדורה על איברי הרשימה וחישוב צבירה (sum-all) פנימי.
ההבנה של מיזוג יכולות Scheme (כמו פונקציות עזר רקורסיביות ו-map) לתוך ה-value-of כדרוש בכדי לטפל במשימה היא יכולת מפתח למבחן.
(fold-exp (exp1 exp2 exps )
(letrec
((proc (expval->proc (value-of exp1 env)))
(acc (expval->num (value-of exp2 env)))
(lst (map (lambda (exp)
(expval->num (apply-procedure proc (value-of exp env))))
exps))
(sum-all (lambda (l)
(if (null? l) 0 (+ (car l) (sum-all (cdr l)))))))
(num-val (sum-all lst))))
)))
חוק תחבירי עבור פקודת check שמוודאת סדרת ביטויים בעזרת and.
דוגמה למבנה בקרה לוגי. מאפשר לקצר תנאים מורכבים.
("check" expression (arbno "and" expression) "end" )
check-exp)
מימוש פקודת check. הקוד משערך ביטוי אחר ביטוי ועוצר ברגע שאחד מהם שקר, או מחזיר אמת אם הגיע לסוף (Lazy Evaluation / Short-Circuit).
במבחנים מסוימים נדרש לממש לוגיקה 'עצלה' (כמו and/or) שבה לא משערכים את כל הביטויים מראש, אלא עוצרים ברגע שהתשובה הוכרעה (כאן ברגע שמצאנו #f).
(check-exp (exp1 exps )
(if (expval->bool (value-of exp1 env))
(if (null? exps)
(bool-val #t)
(value-of (check-exp (car exps) (cdr exps)) env))
(bool-val #f)))
תחביר פקודת count לספירת האלמנטים ברשימה שמקיימים תנאי מסוים של פונקציה.
שימוש ברשימות בתוך שפת היעד מחייב בניית רשימה בעזרת AST.
("count" expression
"[" (separated-list expression ",") "]")
count-exp)
מימוש ה-count. לוגיקה רקורסיבית מפעילה את הפרוצדורה על כל איבר ברשימה, וסופרת כמה החזירו #t.
דוגמה נוספת לאופן בו אנו מטמיעים לולאת עיבוד (פונקציית p) לתוך cases של שפה. צריך לזכור שהחזרת התשובה תמיד דורשת עטיפה של (num-val ...).
(count-exp (prc exps)
(letrec ((p (lambda (prc exps cnt)
(if (null? exps)
cnt
(if (expval->bool (value-of (call-exp prc (car exps)) env) )
(p prc (cdr exps) (+ 1 cnt))
(p prc (cdr exps) cnt))))))
(num-val (p prc exps 0))))
תחביר מרשים לפעולת compose של פונקציות מרובות (הפעלת פונקציות ברצף זו על גבי תוצאת זו).
הרחבה זו דורשת להבין כיצד ניתן לקחת תוצאה דינמית אחת ולהזין אותה כקלט לביטוי השני.
("compose" "[" expression (arbno " ;" expression) "]" expression)
compose-exp)
לוגיקת השערוך של compose. משערכת פונקציה ראשונה על הערך, ומשתמשת בתוצאה על ידי השמתה ל-'res' כדי להמשיך להפעלת הפונקציות הנותרות ברקורסיה.
יצירתיות! בקוד זה יצרנו ביטוי AST מזויף מסוג (var-exp 'res) כדי להעביר את הערך להמשך הקיפול. שיטה זו נחשבת אלגנטית אך קשה, ויכולה לחסוך עבודה במבחן כשצריך להרכיב ביטויים בצורה רקורסיבית.
(compose-exp (firstexp exps val)
(let* ((res (value-of (call-exp firstexp val) env))
(resexp (var-exp ’res)))
(if (null? exps)
res
(value-of (compose-exp (car exps) (cdr exps) resexp)
תחביר עבור setdynamic - עדכון מצב זמני (הפניה) של משתנה, הרצת בלוק קוד, והחזרתו למצבו הקודם לאחר סיום הבלוק (Dynamic Scoping מעשי).
פיצ'ר ייחודי המדמה התנהגות של שפות דינמיות. דורש תכנון זהיר.
("setdynamic" identifier "=" expression "during" express ion)
setdynamic-exp)
המימוש של setdynamic: משתמש ב-begin כדי לדרוס את הזיכרון (setref!), לשערך את גוף התכנית עם הערך החדש, ולבסוף לעדכן אותו בחזרה (restore) לערך הישן.
תרגיל חשוב מאוד! חובה לזכור שב-setdynamic גם אם בלוק הקוד קורס - הזיכרון צריך להישאר נקי. בקוד זה אין הגנה על כך (למשל try-catch), אך העיקרון הבסיסי של שמירת oldval, שינוי ושחזור הוא כלי מרכזי בפתרון בעיות.
(setdynamic-exp (var exp1 body)
(let*
((varref (apply-env env var))
(oldval (deref varref))
(tempval (value-of exp1 env)))
(begin
(setref! varref tempval)
(let ((result (value-of body env)))
(begin
(setref! varref oldval)
result)))))
תחביר ליצירת פונקציה עם 'משתנים סטטיים' פנימיים הנשמרים מריצה לריצה (כמו static ב-C), על ידי הצהרת static x = val.
זהו המבוא לתכנות מונחה עצמים בקורס - הסתרת מידע וזיכרון שנצמד לאובייקט/לסגור עצמו במקום להיות גלובלי.
("proc" "(" identifier ")"
(arbno "static" identifier "=" expression)
expression)
proc-exp)
המימוש של proc עם משתנים סטטיים: יוצרים הקצאות זיכרון (newref) עבור המשתנים, מרחיבים את הסביבה עם ההפניות האלה, ואז שומרים את הסביבה המורחבת בתוך ה-Closure.
יצירת הפניות בזמן הגדרת הפונקציה (ולא בזמן קריאתה) אומרת שההפניות 'ייצמדו' לפונקציה הזו לנצח ויאפשרו לה לזכור מצב פנימי (Internal State). שאלת חובה כמעט בכל מבחן.
(proc-exp (var statvars statvals body)
(letrec
((vals (map (lambda (exp)
(value-of exp env))
statvals))
(expand-env (lambda (vars vals env1)
(if (null? vars)
env1
(expand-env
(cdr vars)
(cdr vals)
(extend-env
(car vars)
(newref (car vals))
env1)))))
(env-with-static (expand-env statvars vals env)))
(proc-val (procedure var body env-with-static))))
תחביר העברת פרמטר לפי הפניה (Call-by-Reference). סימון במילה 'ref' שהמתכנת רוצה להעביר למעשה את המצביע ולא את הערך.
הנושא המתקדם ביותר בפרק על הפניות. שובר את ההפרדה הרגילה בין ערכים לפוינטרים.
("callref" expression "(" "ref" identifier ")")
callref-exp)
ב-callref משתמשים בפונקציה מיוחדת (apply-procedure2) ומעבירים אליה ישירות את הכתובת שבסביבה (באמצעות apply-env) במקום לשערך ולייצר תא חדש.
הבדל משמעותי: ב-call-by-value אנו משערכים את הארגומנט לערך ויוצרים לו newref. כאן אנו מדלגים על השערוך וישר מעבירים את ההפניה! אם ישאלו אתכם על הבדלי ביצועים, כדאי לזכור שזו גישה מהירה יותר אך בעלת השלכות בטיחות (הפונקציה עלולה לדרוס את המשתנה המקורי שלנו).
(callref-exp (rator rand)
(let
((proc (expval->proc (value-of rator env)))
(arg (apply-env env rand)))
(apply-procedure2 proc arg)))
הפונקציה המותאמת ל-callref (apply-procedure2). בניגוד לפונקציה הרגילה שדורשת ExpVal, היא פשוט מרחיבה את הסביבה הישנה ישירות עם ההפניה שהתקבלה.
הוכחה לכך שניתן לעדכן סביבות עם רפרנסים ישירים. מראה איך שפת תכנות תומכת במצביעים מאחורי הקלעים מבלי שהמתכנת מרגיש.
(define apply-procedure2
(lambda (proc1 arg)
(cases proc proc1
(procedure (var body saved-env)
(let ((new-env (extend-env var arg saved-env)))
(value-of body new-env))))))
תחביר לולאת while קלאסית: while <תנאי> do <גוף>.
מבנה הבקרה החשוב ביותר בשפות עם השמות (State). בלולאה פונקציונלית הערך משתנה בקריאה רקורסיבית. כאן אנו רצים באותה סביבה בדיוק עד שמשתנה זיכרון (Store) כלשהו גורם לתנאי להתאפס.
("while" expression "do" expression)
while-exp)
מימוש ה-while ב-value-of תוך שימוש בלולאה רקורסיבית פנימית (do-body) בסקים שעושה פקודת begin עד שהתנאי מחזיר שקר.
דוגמה מובהקת לאופן בו מרכיבים לולאת ריצה. כיוון שפקודת while בשפה שלנו חייבת להחזיר ערך כלשהו לפי החוקים, המימוש כאן בוחר להחזיר את מספר הפעמים שהלולאה רצה (כערך חזרה שימושי, או סתם 0 אם לא צריכים).
(while-exp (bl body)
(letrec
((do-body (lambda (c)
(if (expval->bool (value-of bl env))
(begin
(value-of body env)
(do-body (+ c 1)))
c))))
(num-val (do-body 0))))
סקירה שיטתית ומסודרת של כל פרקי מדריך הלמידה. לכל פרק צוין טווח העמודים, תיאור קצר של התוכן, ולאחר מכן הסבר מעמיק שמטרתו לחבר את המושגים ולעשות סדר ברעיונות שמאחורי הקלעים.
פרק זה משמש כ"שער כניסה" לקורס ולספר EOPL. הוא מסביר את הרציונל שמאחורי לימוד שפות תכנות דרך "גישת המפרשים" (בניית מפרשים לשפות במקום למידה השוואתית עלית-מטה). בנוסף, מפורטות דרישות הקדם לקורס, אופן הקריאה המומלץ של החומר (שילוב בין המדריך לספר), והנחיות לגבי המטלות שדורשות שינוי והרצת קוד ב-Scheme.
בפרק זה אנו מתוודעים לשאלה מדוע כדאי להנדס ולעצב שפות תכנות. נסקרות הסיבות שבגינן אנו מפתחים שפות חדשות או שפות תיאור. מוסבר ההבדל העקרוני בין שפה טבעית לשפת תכנות, ומובאים מושגי יסוד חשובים כמו ערכים, טיפוסים, חוקי תחימה (Scoping), אפקטים ותוצאי-לוואי (Side Effects), ומאפיינים סטטיים ודינמיים.
פרק יסודי זה מלמד כיצד לאפיין קבוצות נתונים בצורה מובנית. אנו לומדים שלוש דרכים עיקריות להגדרת נתונים אינדוקטיבית: מהמסד מעלה (bottom-up), מהטפחות מטה (top-down), ובאמצעות כללי היסק ודקדוק (BNF). החלק החשוב ביותר בפרק הוא ההכוונה לכתיבת קוד נכונה: המבנה של פונקציות רקורסיביות חייב תמיד לשקף את ההגדרה האינדוקטיבית של הנתונים עליהם הן פועלות.
פרק זה עוסק במושג הפשטת הנתונים (Data Abstraction) ובחציצה שבין ממשק (Interface) לבין מימוש. אנו לומדים לאפיין טיפוסי נתונים באמצעות בנאים, מנבאים ומחלצים, ללא תלות בייצוג הספציפי שלהם בזיכרון המחשב. כדוגמאות, מפותח ייצוג למספרים טבעיים (ייצוג אונרי, בינארי וקידוד צ'רץ'), ולסביבת משתנים (Environment) באמצעות רשימות או באמצעות פרוצדורות. כמו כן, נלמד הייצוג של עצים ורצפים (Sequence).
בפרק זה מתחילים לממש שפות תכנות הלכה למעשה. בתחילה לומדים להבדיל בין תחביר מוחשי (מחרוזות) לתחביר מופשט (AST), ונעזרים בכלי ה-SLLgen לפירוק הטקסט למבנה עץ. משם מתקדמים לבניית מפרשים (evaluators): מתחילים משפת "ויהי" (LET) שמוסיפה קשירת משתנים, מתקדמים לשפת "וישגור" (PROC) שמוסיפה פרוצדורות (Closures), ומסיימים בשפת "וייסוג" (LETREC) שמאפשרת הגדרת פרוצדורות רקורסיביות. נדונים בהרחבה מושגי התחימה הלקסיקלית מול התחימה הדינמית.
עד כה עסקנו בשפות פונקציונליות "טהורות". פרק זה מכניס את מימד הזמן והמצב (State) באמצעות הוספת יכולות השמה (Assignment) שמייצרות תוצאי לוואי (Side Effects). אנו לומדים כיצד למדל את הזיכרון כ"החסן" (Store), וממשים את שפת "ויפנה" שמוסיפה הפניות מפורשות (Explicit References), ואת שפת "וירמוז" המממשת הפניות מרומזות (Implicit References) בדומה לשפות התכנות המודרניות שבהן כל משתנה מצביע לזיכרון. לבסוף, הפרק דן באסטרטגיות שונות של העברת פרמטרים.
בפרק זה מתווספת לשפות שלנו שכבה של בטיחות סטטית שמטרתה למנוע שגיאות זמן-ריצה. בתחילה נלמדת שפת "ויבדוק" אשר דורשת מן המתכנת להצהיר מראש על הטיפוסים (כמו בשפת Java או C), ומערכת הטיפוסים מוודאת את נכונות התכנית לפני הרצתה. לאחר מכן נלמדת שפת "ויקיש", המציגה מנגנון מתקדם להסקה אוטומטית של טיפוסים (Type Inference) באמצעות יצירת משוואות ופתרונן על ידי אלגוריתם Unification (בדומה לנהוג בשפת Haskell או ML).
אחרי שלמדנו לפתח מפרשים שונים, מובאת בפרק זה סקירה השוואתית בין משפחות שפות התכנות השונות. נסקרת הפרדיגמה האימפרטיבית שמבוססת על פקודות לשינוי זיכרון (C, Pascal), הפרדיגמה הפונקציונלית המבוססת על שערוך ביטויים ותכונת ה-Referential Transparency (כגון Scheme, Haskell, ML), הפרדיגמה הלוגית המבוססת על יחסים וכללי היסק ומנוע הסקה (Prolog), ותכנות מונחה-עצמים שמספק מנגנוני הפשטה, מחלקות והורשה (Java, C++, Smalltalk).
הספר ומדריך הלימוד בחרו ב-Scheme מסיבות פדגוגיות ופרקטיות. פשטות התחביר של Scheme (שמורכב בעיקר מרשימות וסוגריים) מאפשרת למתכנתים לעקוף את מורכבויות הניתוח התחבירי (Parsing) ולעבוד כמעט ישירות עם מבנה העץ הדינמי (AST). כמו כן, סגנונה הפונקציונלי ויכולות התחימה הלקסיקלית והאשפת הזיכרון (Garbage Collection) מאפשרים כתיבת קוד נקי וקצר במיוחד ליישום מפרשים מורכבים.
פרק פרקטי שנועד להכניס את הסטודנט לסביבת העבודה Racket (שנקראה בעבר Dr. Scheme). נלמד בו תפעול בסיסי של חלון ההגדרות (Definitions) לכתיבת קוד וחלקו של חלון האינטראקציות (Interactions) המאפשר תקשורת ישירה עם סביבת ה-REPL לצורך הרצת שאילתות. בפרק מוסבר כיצד להריץ תוכניות פשוטות ולשמור אותן.
היכרות עם אבני הבניין של השפה Scheme עבור מתכנתים שלא נחשפו אליה בעבר. הפרק מסביר את עקרונות כתיבת הביטויים (הכתיב התחילי המאפיין את השפה), וכן את ההבדלים בין קריאות רגילות לפונקציות לבין "צורות מיוחדות" (Special Forms) כגון define ו-let. מוסבר גם ההבדל בין שגיאה תחבירית לשגיאה סמנטית וכיצד משתמשים בכלי פיתוח לאיתורן.
בפרק זה נלמדות היכולות המתקדמות והחיוניות של שפת Scheme הדרושות לבניית המפרשים בקורס. אנו לומדים לעבוד באינטנסיביות עם רשימות בעזרת פעולות השליפה (car, cdr, caddr וכו') ופעולות הבנייה (cons). כמו כן, הפרק עוסק בהגדרת פונקציות פנימיות (מקומיות) ובשימוש בתנאי מרובה אפשרויות מסוג cond. עיקר הפרק מתרכז בכתיבת שגרות עזר פונקציונליות רקורסיביות לעיבוד עצי נתונים.
עבור מי שסיים את חומר החובה, פרק זה מציע כיווני המשך להרחבת הידע בתחום שפות התכנות והמפרשים. מובאת רשימת קריאה מומלצת, הכוללת קישורים לספרות קלאסית בעולם מדעי המחשב (למשל הספר SICP) ומאמרים ידועים המסבירים מדוע כדאי לתכנת בצורה פונקציונלית, וכן הפניות למקורות מידע אקדמיים נוספים.
פתרונות מלאים לתרגילים מובחרים שמפוזרים לאורך המדריך, מפתח קוד מסודר לאיתור מהיר של מנגנונים ומפתח טעויות שנמצאו בספר המקורי באנגלית.