שלום Clojure

02/11/2019

ללמוד קלוז'ר זה מהדברים שאני הרבה זמן רוצה ולא הצלחתי להגיע אליהם, ואז אתמול קלטתי שכבר נובמבר ועוד חודש מתחיל שוב Advent Of Code אז יהיה נחמד לנצל את המשחק השנה כדי ללמוד קלוז'ר. בכל מקרה בשביל שבדצמבר יהיה לי קצת מושג על איך כותבים קוד ישבתי להתקין ולהתחיל היום עם תוכנית קלוז'ר ראשונה שלי.

המטרה היתה לבנות משהו לא מסובך מדי (בכל זאת שעה ראשונה עם קלוז'ר) אבל גם לא סתם Hello World. בסוף הלכתי על המשימה הראשונה מ AoC של שנה שעברה. תיאור המשימה המלא בקישור הזה.

ניגש לקוד?

1. שלום עולם

אני מתנצל מראש שחלק מההסברים בפוסט הזה בטח יהיו רחוקים מהמציאות כי הוא נכתב אחרי שעה עם השפה, אבל בכל זאת יהיו פה 3 תוכניות שעובדות אז בטח נוכל ללמוד מהן משהו. נתחיל עם הראשונה שמדפיסה שלום עולם על המסך:

(println "Hello world.")

אז כן זו תוכנית קלוז'ר אמיתית, כל מה שצריך בשביל להריץ אותה זה להתקין clojure, לשמור את השורה בקובץ עם סיומת clj ולהפעיל:

clojure helloworld.clj

בהנחה ששם הקובץ שבחרתם היה helloworld.clj כמו שאני בחרתי.

2. הדפסת סכום המספרים בקובץ

המשימה השניה להיום, והיא כבר מ AoC, היא לקרוא את כל השורות מקובץ טקסט נתון, בכל שורה רשום מספר ולהדפיס את סכום המספרים שקראנו. לקלוז'ר כך גיליתי יש תיעוד מטורף וכמעט כל שאלה שאני כותב בגוגל על השפה מביאה אותי לאחד מדפי התיעוד שלה ובדיוק לפונקציה המתאימה. כך למדתי ש line-seq מחזיר לי את כל השורות מקובץ ו read-string הופך מחזורת למספר. את reduce ו map ניחשתי לבד ואת println פגשנו בתוכנית קודמת וכך הגעתי לקוד הבא:

(with-open [rdr (clojure.java.io/reader "./input.txt")]
    (println
      (reduce
        +
        (map
          read-string 
          (line-seq rdr)))))

תוכנית Clojure קוראים כמו lisp כלומר מבפנים החוצה: לוקחים את כל השורות בקובץ, שולחים כל אחת ל read-string באמצעות map, אחרי זה סוכמים עם reduce ופונקציית החיבור ומדפיסים את התוצאה עם println. עד לפה קוד פונקציונאלי סטנדרטי ודי נעים לעין.

3. מציאת הסכום הראשון שמופיע פעמיים

החלק השני של השאלה היה קצת יותר מסובך - המטרה עכשיו היא להוסיף מספרים בהדרגה וכל פעם להסתכל על הסכום שיצא, כשיוצא סכום שכבר ראינו לעצור ולספר מהו. אם נגמרו המספרים יש לחזור לתחילת הערימה ולהתחיל לקרוא את הקובץ מההתחלה.

בדוגמא קטנה שלהם אם הקלט הוא:

1
-2
3
1

אז במקום לחבר את כל ה-4 נצטרך לחבר אותם אחד אחד ונקבל את הסכומים:

1
-1
2
3
(At this point, the device continues from the start of the list.)
4
2 - Found it!

ולכן המספר 2 הוא הסכום הראשון שמופיע פעם שניה.

בשביל לפתור את זה בקלוז'ר שמתי לב שיש פונקציה בשם distinct שלוקחת רצף ומחזירה את אותו הרצף ללא חזרות, לכן ההיגיון שלי היה להסתכל על סידרת הסכומים (שתכף נמצא איך לחשב אותה) - פעם אחת עם הפעלה של distinct ופעם אחת בלי. האיבר הראשון ששונה בין הסדרות הוא האיבר הכפול הראשון, ואותו נדפיס.

הפקודה reductions של קלוז'ר מחשבת סידרה של סכומים חלקיים (קצת כמו reduce רק שמחזירה סידרה במקום תוצאה בודדת). זה נראה ככה בדוגמא פשוטה:

user=> (reductions + '(1 -2 3 1))
(1 -1 2 3)

חוץ ממנה יש את cycle שהופכת סידרת מספרים לאינסופית באמצעות חזרה על הסידרה שוב ושוב, ואת let איתה אנחנו יכולים לתת שמות למשתנים שלנו. לכן הקוד הבא מייצר בדיוק את שתי הסדרות שאני צריך:

(with-open [rdr (clojure.java.io/reader "./input.txt")]
    (let [sums (reductions
                 +
                 (map
                   read-string
                   (cycle lines)))

          distinct-sums (distinct sums)]))

והבעיה הראשונה שהיתה לי עם clojure היתה שלא מצאתי דרך מובנית בשפה להדפיס את האיבר הראשון ששונה בין שתי סדרות. מפה לשם למדתי איך לכתוב פונקציה והגעתי לקוד הבא:

(defn first-diff [c1 c2]
  (nth
    (first
      (drop-while
        #(== (nth % 0) (nth % 1))
        (map vector c1 c2)))
    0))

וזה סוגר לנו את השאלה מתחילת הפוסט:

(with-open [rdr (clojure.java.io/reader "./input.txt")]
    (let [
          lines (line-seq rdr)
          sums (reductions
                 +
                 (map
                   read-string
                   (cycle lines)))

          distinct-sums (distinct sums)]

      (println
        (first-diff sums distinct-sums))))

בואו ננסה לקרוא את זה:

  1. לוקחים את הסידרה שקיבלנו מ line-seq וקוראים לה בשם lines.
  2. מפעילים על lines את cycle כדי לקבל את כל השורות שוב ושוב. הופכים אותן למספרים ומייצרים מזה סידרה של כל הסכומים. לסידרה הזאת אנחנו קוראים sums.
  3. לוקחים רק את האלמנטים הייחודיים מסידרת הסכומים החלקיים לסידרה חדשה לה אנו קוראים distinct-sums.

אחרי שכל המשתנים במקום אני מפעיל את first-diff שכתבתי כדי למצוא את האלמנט הראשון ששונה בין הסדרות ולהדפיס אותו, וזה בדיוק הסכום הראשון שמופיע פעם שניה.

בשבועות הקרובים אני מתכנן ללמוד עוד קצת קלוז'ר. מוזמנים להישאר וללמוד איתי או לקחת השראה ולפתור את השאלות בשפה חדשה אותה אתם רוצים ללמוד.