היום למדתי: הפלוס הקטן בסקאלה

29/10/2023

בואו נכתוב תוכנית סקאלה שמחזיקה "דברים" שיכולים להישמר לשרת מרוחק. אפשר לדמיין קלאס של משתמש שיראה כך:

case class User(name: String)

ואולי קלאס של מסמך:

class Document(title: String, content: String)

בשביל לשמור אותם לשרת מרוחק אני צריך להוסיף לכל אחד מהם url, ובשביל לא לשנות את הקלאסים אני יכול להשתמש ב Generic וליצור גירסה "מרוחקת" שלהם, כלומר:

case class Remote[S](value: S, url: String)

אז עכשיו משהו שהוא Remote[User] יחזיק שדה בשם value מסוג User וגם שדה בשם url מסוג String. ואולי נרצה לכתוב פונקציה כללית שיכולה לקבל משהו שהוא Remote ולשמור את הערך מ value לשרת המרוחק באמצעות HTTP Post. היינו עשויים לחשוב שהחתימה של פונקציה כזו תיראה כך:

def save(what: Remote[Any]): Unit =
  println(s"Saving ...")

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

@main def main()=
  val a = Remote[User](value = User("ynon"), url = "/admin")
  save(a)

לא מצליח להתקמפל בגלל השגיאה:

Found:    (a : Remote[User])
Required: Remote[Any]
  save(a)

מה קרה כאן? הפונקציה מצפה לקבל משהו מטיפוס Remote[Any]. נכון ש User הוא סוג של Any ואם היתה לנו פונקציה שמקבלת Any לא היתה בעיה להעביר לה משהו מסוג User, אבל באופן רגיל Remote[User] איננו משהו מסוג Remote[Any].

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

def save[S](what: Remote[S]): Unit =
  println(s"Saving ...${what.value} to ${what.url}")

והכל מתקמפל ועובד.

הפיתרון השני והוא הסיפור של הפוסט הזה נועד למקרים בהם לא הצלחתם להפוך את הפונקציה לגנרית מכל מיני סיבות או שיש לכם Use Case טיפה שונה, ואתם באמת רוצים להגיד משהו אחר על הטיפוס Remote[S] - הייתם רוצים להגיד שאם יש לכם פונקציה שמצפה לקבל Remote[S] לא משנה מה זה S, ויש לכם משהו אחר נקרא לו T שיורש מ S, אז Remote[T] יירש מ Remote[S]. ובשביל להגיד את זה בסקאלה אפשר להוסיף סימן + לפני ה S בהגדרת הטיפוס הגנרי. זה נראה כך:

case class Remote[+S](value: S, url: String)
case class User(name: String)
class Document(title: String, content: String)

def save(what: Remote[Any]): Unit =
  println(s"Saving ...${what.value} to ${what.url}")

@main def main()=
  val x: Any = User("ynon")
  val a = Remote[User](value = User("ynon"), url = "/admin")
  save(a)

וגם הגירסה הזו מתקמפלת ועובדת. ולינק אחרון לסקרנים שרוצים לקרוא עוד על המנגנון הזה בתיעוד של סקאלה, על המינוס שאפשר לרשום במקום הפלוס, ועל השמות באנגלית של שניהם יהיה תיעוד Variances מהסיור בסקאלה: https://docs.scala-lang.org/tour/variances.html