תוכן עניינים הקדמה
הכרות עם schemes
תבניות נתונים
תבניות
משפטי בקרה
משתנים לקסיקלים
רקורסיה
קלט/פלט
macros
מבנים
alises and tables ממשק המערכת
מחלקות ואובייקטים
jumps
אי-דטרמיניסטיות
מנועים
shell scripts
אתר ללימוד מקיף> פרק 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, כלומר:

(< (pressure tube) 60)

ה-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))