טיפ סקאלה: הדפסת דיבג באמצע Pipeline

04/12/2023

בשפות פונקציונאליות דינמיות כמו קלוז'ר או אליקסיר, יש אופרטור Pipeline ששולח את התוצאה של פונקציה אחת בתור קלט לפונקציה הבאה בתור. לדוגמה הקוד הבא בקלוז'ר:

(->> 10
  (range)
  (map #(* %1 %1))
  (reduce +)
  (println))

לוקח את המספר 10, שולח אותו בתור הפרמטר לפונקציה range, אחרי זה לוקח את התוצאה של range בתור קלט לפונקציית ה map, את התוצאה של map הוא שולח ל reduce ואת זה ישלח ל println כדי להדפיס. סך הכל נקבל על המסך את סכום ריבועי המספרים מאפס עד תשע.

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

(defn debug [n]
  (println n)
  n)

(->> 10
  (debug)
  (range)
  (debug)
  (map #(* %1 %1))
  (debug)
  (reduce +)
  (println))

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

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

  println(
    (0 to 9)
      .map(_.pow(2))
      .sum
  )

אז זה היה פשוט בסקאלה אני לא יכול להשתמש פעמיים בפרמטר של פונקציה אנונימית אבל במקום זה יש לי פונקציה להעלות בריבוע, טווח כולל את המספר האחרון לכן מסיימים בתשע ויש פונקציית sum אז לא צריכים את reduce. ובכל זאת שני דברים פוגעים פה בחוויה - הראשון הוא ש println צריך להיכתב בהתחלה בתור פונקציה, והשני שגם אם אכתוב פונקציית debug כמו שהיתה לי בקלוז'ר היא לא חלק מהפייפליין.

לשמחתנו מנגנון ה Extensions של סקאלה מאפשר לפתור את שתי הבעיות יחסית בקלות. אני מגדיר Extension גנרי שיוסיף את הפונקציה debug לכל ערך בשפה. זה נראה ככה:

extension [A](value: A)
  def debug(): A =
    println(value)
    value
}

@main
def main(): Unit =
  println(
    (0 to 9)
      .debug()
      .map(_.pow(2))
      .debug()
      .sum
  )

והפלט:

NumericRange 0 to 9
Vector(0, 1, 4, 9, 16, 25, 36, 49, 64, 81)
285

יותר מזה, בגלל שחלק מהאוביקטים בשפה מחושבים בצורה עצלה (למשל Range), אפשר להוסיף עוד פונקציה שתהפוך איטרטורים לרשימות ואז תדפיס אותם, רק שימו לב לא להפעיל אותה על דברים אינסופיים:


extension [A](value: A)
  def debug(): A =
    println(value)
    value

  def eagerDebug(): A = value match {
    case it: Iterable[_] =>
      println(it.toList)
      value
    case it: Iterator[_] =>
      println(it.toList)
      value
    case _ =>
      println(value.getClass)
      value
  }

@main
def main(): Unit =
  println(
    (0 to 9)
      .eagerDebug()
      .map(_.pow(2))
      .debug()
      .sum
  )

ואם כבר הוספנו את debug נוכל להשתמש בה גם בסוף הפייפליין כדי להדפיס בסגנון קלוז'ר:

@main
def main(): Unit =
  (0 to 9)
    .eagerDebug()
    .map(_.pow(2))
    .debug()
    .sum
    .debug()