שימוש חוזר בקוד עם קלוז'ר ומפות
אני ממשיך לכתוב על Advent Of Code כי אני עדיין בהתלהבות של ההתחלה. לא יודע כמה זמן עוד אצליח להתמיד, אבל בכל מקרה מקווה שאם אתם קוראים את זה אולי זה ייתן גם לכם חשק לשחק עם האתגרים. היומיים הראשונים היו בנויים ממש טוב ואהבתי במיוחד את האתגר של אתמול שחיבר בין התרגיל לתיאוריה של מבנה המחשב ועזר להבין איך עובד זיכרון ואיך מחשבים מריצים תוכניות. בקיצור מומלץ. אה, וזה הקישור שיהיה לכם קל:
https://adventofcode.com/2019/day/2
אני פותר אותם השנה בקלוז'ר במטרה ללמוד את השפה, והתרגיל של אתמול נתן לי הזדמנות ללמוד איך להשתמש במפות (Hash Maps) כדי לחסוך בקוד כפול. הנה שתי פונקציות מתוך הגירסא הראשונה שכתבתי:
(defn process-add
[ip program]
(let [
[opcode idx1 idx2 idx3] (subvec program ip)
val1 (nth program idx1)
val2 (nth program idx2)
]
(assoc program idx3 (+ val1 val2))
))
(defn process-mul
[ip program]
(let [
[opcode idx1 idx2 idx3] (subvec program ip)
val1 (nth program idx1)
val2 (nth program idx2)
]
(assoc program idx3 (* val1 val2))
))
זה קלוז'ר אז אני אתרגם - כל אחת מהפונקציות מקבלת כתובת ותוכנית (התוכנית היא בסך הכל מערך) ואז מבצעת את העבודה שלה: פונקציית החיבור לוקחת אינדקסים מהתוכנית, שולפת ערכים מתאימים, מחברת אותם ורושמת את התוצאה לאינדקס השלישי, ופונקציית הכפל עושה אותו דבר בדיוק אבל עם כפל.
שפות תכנות רבות (וקלוז'ר ביניהן) מאפשרות לחסוך את כפל הקוד הנורא הזה באמצעות העברת האופרטור כפרמטר נוסף לפונקציה. במקרה שלנו אגב הפונקציה כבר מקבלת את קוד הפעולה בפרמטר opcode כך שאפילו לא צריך לשנות שום דבר בחתימה או בשורת הקריאה. בשביל לחסוך את כפל הקוד כל מה שאנחנו צריכים לעשות הוא לבנות פונקציה חדשה שמסתכלת על קוד הפעולה ב opcode ולפי זה מחליטה מה לעשות. משהו כמו זה:
(defn process
[ip program]
(let [
[opcode idx1 idx2 idx3] (subvec program ip)
val1 (nth program idx1)
val2 (nth program idx2)
op (get operators opcode)
]
(case opcode
1 (assoc program idx3 (+ val1 val2))
2 (assoc program idx3 (* val1 val2))
)))
וזה עובד ויכול בקלות להחליף את שתי הפונקציות, אבל עדיין משהו מרגיש צורם: שורת ה case בסוף משוכפלת, ונכון שזה שכפול קטן יותר מאשר שכפול של פונקציה שלמה, אבל בכל זאת שכפול זה שכפול ולאט לאט ככל שיוסיפו יותר פעולות למכונה זה רק יהפוך לקשה יותר לתחזוקה.
שפות תכנות רבות (וקלוז'ר ביניהן) מאפשרות להעביר את הפונקציה עצמה של הכפל או החיבור בתור פרמטר, או לשמור אותם בתור ערכים ב Hash Map. שמירה כזו אומרת שנוכל "לשלוף" את הפעולה מה Hash Map במקום לכתוב פקודת switch/case ארוכה שמכסה את כל המקרים.
שכתוב הקוד שישתמש ב Hash Map נותן את הפונקציה הבאה:
(def operators
{
1 +
2 *
})
(defn process
[ip program]
(let [
[opcode idx1 idx2 idx3] (subvec program ip)
val1 (nth program idx1)
val2 (nth program idx2)
op (get operators opcode)
]
(assoc program idx3 (op val1 val2))
))
אני אוהב את הגירסא הזו כי יש כאן הפרדה בין הפעולות לבין הפונקציה שמבצעת את הפעולות, ולכן יהיה קל בעתיד לשנות את מזהי הפעולות או להוסיף פעולות נוספות. אפשר היה לכתוב את הפונקציה בצורה אפילו יותר גנרית כך שתוכל לטפל בפעולות עם מספר משתנה של פרמטרים, אבל לא היה בזה צורך בשביל התרגיל של היום. יש לי תחושה שעוד נחזור לקוד הזה בהמשך החודש.