משחקים עם סקאלה - סריקת פיסקאות בקובץ

האוביקט Source של סקאלה מאפשר לקרוא טקסט מהמון מקורות אבל הוא בדרך כלל נותן לנו את הטקסט בשורות, לדוגמה בשביל להדפיס קובץ שורה אחרי שורה אני יכול לכתוב-

Source.fromFile("demo.txt").getLines().foreach(println)

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

Scala (/ˈskɑːlə/ SKAH-lah)[8] is a strong statically typed
high-level general-purpose programming language that supports
both object-oriented programming and functional programming.

Designed to be concise,[9] many of Scala's design decisions are
intended to address criticisms of Java.[7]

Scala source code can be compiled to Java bytecode and run on
a Java virtual machine (JVM).
Scala can also be compiled to JavaScript to run in a browser,
or directly to a native executable.

On the JVM Scala provides language interoperability with Java so that libraries written in either language may be referenced directly in Scala or Java code.[10] Like Java, Scala is object-oriented, and uses a syntax termed curly-brace which is similar to the language C. Since Scala 3, there is also an option to use the off-side rule (indenting) to structure blocks, and its use is advised. Martin Odersky has said that this turned out to be the most productive change introduced in Scala 3.[11]

אני יודע שאני יכול להפעיל את getLines כדי לקבל רשימה של כל השורות, אבל אני רוצה לבנות פונקציה שתהפוך את זה לרשימה של רשימות של שורות, כאשר בכל תת רשימה יהיו השורות הצמודות. סך הכל יהיו לי 4 רשימות שיתאימו לארבעת הפיסקאות בקובץ.

אחרי שתהיה לנו פונקציה כזאת (נקרא לה בשביל המשחק toParagraphs, אפשר יהיה לכתוב קוד כזה כדי למצוא את הפיסקה עם הכי הרבה שורות:

    Source
      .fromResource("demo.txt")
      .getLines()
      .toParagraphs
      .maxBy(_.size)
      .foreach(println)

או כזה כדי למצוא את הפיסקה הארוכה ביותר (עם הכי הרבה תווים):

    Source
      .fromResource("demo.txt")
      .getLines()
      .toParagraphs
      .maxBy(_.mkString.length)
      .foreach(println)

המימוש של toParagraphs היה די פשוט אחרי שהבנתי את המנגנון של איטרטורים בסקאלה. בגדול איטרטור הוא משהו שמאפשר לרוץ על אוסף של פריטים, ולכן בשביל לשנות את איך שרצים על הפריטים צריך לבנות איטרטור חדש. האיטרטור החדש מקבל כקלט איטרטור שרץ על השורות (התוצאה של getLines) והקסם שלו קורה בפונקציה next שצריכה להחזיר את הפריט הבא. במקום להחזיר שורה הפונקציה באיטרטור החדש שלי מחזירה רשימה של שורות. קוד האיטרטור החדש הוא לכן:

class ChunkedIterator[T](iterator: Iterator[T])(p: (T => Boolean)) extends Iterator[List[T]] {
  override def hasNext: Boolean = iterator.hasNext

  override def next(): List[T] = {
    if (!hasNext) throw new NoSuchElementException("next on empty iterator")
    iterator.takeWhile(p).toList
  }
}

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

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

extension (i: Iterator[String]) {
  def toParagraphs: ChunkedIterator[String] = {
    ChunkedIterator[String](i) { f => f.nonEmpty }
  }
}

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