
אתר ללימוד מקיף> פרק 8 - Macros
פרק 8 - Macros
הנושאים בפרק זה:
8.1.
ההרחבות ל-template
8.2.
הימנעות מלכידת משתנים ב-macros
8.3.
fluid-let
משתמשים יכולים ליצור תבניות משלהם ע"י הגדרת macros. כאשר Scheme נתקלת בביטוי macro, היא פונה
לטרנספורמטור של ה-macro של התת תבניות ומעריכה את התוצאה של ההמרה.
ה-Macro מציין את קטע הקוד שלו. סוג זה של המרה שימושי לקיצור תבנית טקסטואלית מורכבת.
Macro - מוגדר ע"י שימוש בתבנית define-macro .
לדוגמא, אם ב-Scheme שלך חסרה הפרוצדורה when, ניתן להגדיר אותה כ-macro.
(define-macro when
(lambda (test . branch)
(list 'if test
(cons 'begin branch))))
|
ההגדרה הזו של when, תמיר ביטוי של when לביטוי if מקביל.
דוגמא לשימוש ב-when:
(when (< (pressure tube) 60)
(open-valve tube)
(attach floor-pump tube)
(depress floor-pump 5)
(detach floor-pump tube)
(close-valve tube))
|
בדוגמא זו ביטוי ה-when יומר לביטוי אחר, התוצאה של יישום הטרנספורמטור של when על ביטויי תת התבניות
של ה-when:
(apply
(lambda (test . branch)
(list 'if test
(cons 'begin branch)))
'((< (pressure tube) 60)
(open-valve tube)
(attach floor-pump tube)
(depress floor-pump 5)
(detach floor-pump tube)
(close-valve tube)))
|
ההמרה מפיקה את הרשימה:
(if (< (pressure tube) 60)
(begin
(open-valve tube)
(attach floor-pump tube)
(depress floor-pump 5)
(detach floor-pump tube)
(close-valve tube)))
|
Scheme תעריך את הביטוי כפי שהעריכה את שאר הביטויים.
הנה דוגמא נוספת להגדרת macro של unless:
(define-macro unless
(lambda (test . branch)
(list 'if (list 'not test)
(cons 'begin branch))))
|
ניתן להשתמש ב-when בהגדרה של unless:
(define-macro unless
(lambda (test . branch)
(cons 'when
(cons (list 'not test) branch))))
|
ההרחבות של ה-macro יכולות להתייחס ל-macros אחרים.
8.1 ההרחבות ל-template
הטרנספורמטור של ה-macro לוקח כמה s-expression ומפיק מהם s-expression יחיד שניתן יהיה להשתמש בו כתבנית.
בד"כ הפלט הוא רשימה.
בדוגמא שלנו הפלט הוא רשימה הנוצרת ע"י שימוש ב:
(list 'if test
(cons 'begin branch))
|
ה-test מקושר לתת תבנית הראשונה של ה-macro, כלומר:
ה-branch קשור ליתר התת תבניות של ה-macro, כלומר:
((open-valve tube)
(attach floor-pump tube)
(depress floor-pump 5)
(detach floor-pump tube)
(close-valve tube))
|
רשימות הפלט יכולות להיות מעט מסובכות. macro - יותר מסובך יכול להוביל למבנה יותר משוכלל של רשימת הפלט.
במקרים אלו יותר קל לייצג את פלט ה-macro כ-template, שיכיל את הארגומנטים של ה-macro במקומות הנכונים.
Scheme מספקת את התחביר לציין template (ע"י גרש).
לכן נוח יותר לייצג את הביטוי:
(list 'IF test
(cons 'BEGIN branch))
|
כך:
`(IF ,test
(BEGIN ,@branch))
|
כעת נכתוב מחדש את הגדרת ה-macro של when:
(define-macro when
(lambda (test . branch)
`(IF ,test
(BEGIN ,@branch))))
|
יש לשים לב שהפורמט של ה-template בניגוד למבני הרשימה הקודמים נותן אינדיקציה על התבנית של רשימת הפלט.
הגרש ( ' ) מציג מבנה של template לרשימה.
האלמנטים ב-template מופיעים אחד אחד מלבד אלו שלפניהם יש פסיק (',') או ('@,'). על מנת להמחיש זאת רשמנו
את האלמנטים שמופיעים באותיות גדולות.
הפסיק ו ה-('@,') משמשים להכנסת ארגומנטים ל-template. הפסיק מכניס את תוצאת הערכה לביטוי הבא. ה-('@,')
מכניס את התוצאה לביטוי הבא לאחר שאיחה אותה, כלומר, הוא מסיר לקבוצה הקיצונית ביותר את הסוגריים.
(מכאן נובע שהביטוי שמגיע לפני ('@,') חייב להיות רשימה).
בדוגמא שלנו לאחר נתינת הערכים ש-test ו-branch קשורים אליהם, קל לראות שה-template יורחב לביטוי הנ"ל:
(IF (< (pressure tube) 60)
(BEGIN
(open-valve tube)
(attach floor-pump tube)
(depress floor-pump 5)
(detach floor-pump tube)
(close-valve tube)))
|
8.2 הימנעות מלכידת משתנים ב-macro
הגדרת my-or, שהיא תבנית בעלת שני ארגומנטים:
(define-macro my-or
(lambda (x y)
`(if ,x ,x ,y)))
|
my-or מקבלת שני ארגומנטים ומחזירה את הערך של הארגומנט הראשון שהוא אמת. (אם אין כאלו היא תחזיר f#).
למעשה הביטוי השני יחושב רק אם הראשון הוא שקר.
לדוגמא:
(my-or 1 2)
=> 1
(my-or #f 2)
=> 2
|
אך יש בעיה ב-my-or כפי שהיא כתובה כעת. היא מעריכה את הארגומנט הראשון פעמיים אם הוא אמת: פעם אחת
בבדיקת ה-if ופעם נוספת ב-than. תופעה זו יכולה לגרום להתנהגות לא רצויה אם הארגומנט השני מכיל הדפסה
למסך לדוגמא.
(my-or
(begin
(display "doing first argument")
(newline)
#t)
2)
|
קטע קוד זה מדפיס פעמיים "doing first argument". ניתן להימנע מכך ע"י אחסון תוצאת בדיקת ה-if במשתנה
מקומי כלומר כך:
(define-macro my-or
(lambda (x y)
`(let ((temp ,x))
(if temp temp ,y))))
|
קוד זה כמעט טוב, רק שהשתמשנו באותו שם משתנה temp.
אם נריץ זאת נקבל:
(define temp 3)
(my-or #f temp)
=> #f
|
התוצאה צריכה הייתה להיות 3, אך מכיוון שה-macro השתמש במשתנה המקומי temp לאחסן את הערך של הארגומנט
הראשון וכן המשתנה temp בארגומנט השני נלכד ע"י ה-temp שהוצג ע"י ה-macro.
כדי להימנע מכך עלינו להיות זהירים בבחירת השמות למשתנים המקומיים בהגדרת ה-macro ניתן לבחור שמות מוזרים
כדי שלא יחשבו עליהם ותיגרמנה טעויות. לדוגמא,
(define-macro my-or
(lambda (x y)
`(let ((+temp ,x))
(if +temp +temp ,y))))
|
קטע קוד זה יעבוד מכיוון שלא ישתמשו במשתנה temp+ בקוד מחוץ ל-macro.
יש דרך יותר אמינה להשיג שמות משתנים ייחודים ע"י שימוש בפרוצדורה ב-gensym.
הגדרה יותר בטוחה ל-my-or ע"י שימוש gensym:
(define-macro my-or
(lambda (x y)
(let ((temp (gensym)))
`(let ((,temp ,x))
(if ,temp ,temp ,y)))))
|
בהגדרות ה-macro באתר זה לא נשתמש ב-gensym על מנת להיות יותר תמציתיים.
נשתמש ב-+ כקידומת למשתנים ונשאיר לקורא להפוך אותם ע"י שימוש ב-gensym.
8.3 fluid-let
הגדרה נוספת ל-macro היותר מסובך fluid-let (
פרק 5.2).
fluid-let מציין באופן זמני קישורים לקבוצה של משתנים לקסיקלים.
לדוגמא ביטוי זה של fluid-let:
(fluid-let ((x 9) (y (+ y 1)))
(+ x y))
|
ואנו רוצים שהוא יורחב ל:
(let ((OLD-X x) (OLD-Y y))
(set! x 9)
(set! y (+ y 1))
(let ((RESULT (begin (+ x y))))
(set! x OLD-X)
(set! y OLD-Y)
RESULT))
|
בדוגמא זו אנו רוצים ש-OLD-Y ,OLD-X ו-RESULT יהיו סמלים שלא ילכדו משתנים אחרים בתבנית ה-fluid-let.
בדוגמא הבאה נכתוב את הקטע מחדש בעזרת ה-macro fluid-let אשר מיישמת את מבוקשנו:
(define-macro fluid-let
(lambda (xexe . body)
(let ((xx (map car xexe))
(ee (map cadr xexe))
(old-xx (map (lambda (ig) (gensym)) xexe))
(result (gensym)))
`(let ,(map (lambda (old-x x) `(,old-x ,x))
old-xx xx)
,@(map (lambda (x e)
`(set! ,x ,e))
xx ee)
(let ((,result (begin ,@body)))
,@(map (lambda (x old-x)
`(set! ,x ,old-x))
xx old-xx)
,result)))))
|
הארגומנטים ב-macro הם: xexe - המייצג רשימה של זוגות משתנים או ביטויים המוצגים ע"י fluid-let
ו-body - המייצג רשימה של ביטויים בגוף הפרוצדורה fluid-let.
בדוגמא שלנו
((x 9) (y(+y 1 )))
מייצג את הארגומנט xexe , ו-(( x y +)) מייצג את הארגומנט body.
גוף ה-macro מציג קבוצת משתנים מקומיים :
xx - מייצג רשימת משתנים שחולצו מזוגות המשתנים או הביטויים.
ee - מייצג את הרשימה המתאימה של הביטויים.
old-xx - מייצג רשימה של משתנים מזהים חדשים עבור כל משתנה הנמצא ב-xx.
הם משמשים לאחסון הערכים הנכנסים למשתנים הנמצאים ב-xx, לכן ניתן לחזור לערכים המקוריים של המשתנים
לאחר שגוף ה-fluid-let הוערך.
result - הנו משתנה מזהה חדש המשמש לאחסון הערך של גוף הפרוצדורה fluid-let.
בדוגמא שלנו xx - הוא הרשימה ( x y ) ו-ee - הוא הרשימה (y 1 (+9)). בהתאם ליישום הפרוצדורות gensym
במערכת המשתמש, יתכן ו-old-xx יהיה הרשימה (GEN-63 GEN-64) ו-result יהיה GEN-65.
רשימת הפלט נוצרת ע"י ה-macro כפי שיתואר בדוגמא להלן ומתאים לדרישותינו:
(let ((GEN-63 x) (GEN-64 y))
(set! x 9)
(set! y (+ y 1))
(let ((GEN-65 (begin (+ x y))))
(set! x GEN-63)
(set! y GEN-64)
GEN-65))
|