סוג שיעורים ב-Scala3: מדריך למתחילים | פִּנקָס

הקלד שיעורים ב-Scala3: מדריך למתחילים | פִּנקָס

סוג שיעורים ב-Scala3: מדריך למתחילים | Ledger PlatoBlockchain Data Intelligence. חיפוש אנכי. איי.

מסמך זה מיועד למפתח Scala3 המתחיל שכבר בקי בפרוזה של Scala, אך מתלבט לגבי כל ה`implicits` ותכונות פרמטריות בקוד.

מסמך זה מסביר את הסיבה, איך, היכן ומתי סוגים (TC).

לאחר קריאת מסמך זה, מפתח Scala3 המתחיל ירכוש ידע מוצק לשימוש ויצלול לתוך קוד המקור שלו הרבה של ספריות Scala ולהתחיל לכתוב קוד Scala אידיומטי.

בואו נתחיל עם הסיבה…

בעיית הביטוי

ב1998, אמר פיליפ ואדלר ש"בעיית הביטוי היא שם חדש לבעיה ישנה". זו הבעיה של הרחבה של תוכנה. על פי כתב מר ודלר, הפתרון לבעיית הביטוי חייב לעמוד בכללים הבאים:

  • כלל 1: אפשר את היישום של התנהגויות קיימות (חשבו על תכונת Scala) שיש ליישם עליה ייצוגים חדשים (חשבו על מחלקת מקרה)
  • כלל 2: אפשר את היישום של התנהגויות חדשות להחיל עליו ייצוגים קיימים
  • כלל 3: אסור לו לסכן את בטיחות סוג
  • כלל 4: אין צורך לבצע קומפילציה מחדש קוד קיים

פתרון בעיה זו יהיה חוט הכסף של מאמר זה.

כלל 1: יישום התנהגות קיימת על ייצוג חדש

לכל שפה מונחה עצמים יש פתרון אפוי עבור כלל 1 עם תת -סוג פולימורפיזם. אתה יכול ליישם בבטחה כל `trait` מוגדר בתלות ב`class` בקוד משלך, מבלי להדר מחדש את התלות. בוא נראה את זה בפעולה:

סולם

def todo = 42
type Height = Int
type Block = Int

object Lib1:
 trait Blockchain:
 def getBlock(height: Height): Block

 case class Ethereum() extends Blockchain:
 override def getBlock(height: Height) = todo

 case class Bitcoin() extends Blockchain:
 override def getBlock(height: Height) = todo


object Lib2:
 import Lib1.*

 case class Polkadot() extends Blockchain:
 override def getBlock(height: Height): Block = todo

val eth = Lib1.Ethereum()
val btc = Lib1.Bitcoin()
val dot = Lib2.Polkadot()

בדוגמה הפיקטיבית הזו, ספרייה `Lib1` (שורה 5) מגדירה תכונה `Blockchain` (שורה 6) עם 2 יישומים שלה (שורות 9 ו-12). `Lib1` יישאר זהה בכל המסמך הזה (אכיפה של כלל 4).

`Lib2` (שורה 15) מיישם את ההתנהגות הקיימת `Blockchain`בכיתה חדשה`Polkadot` (כלל 1) בצורה בטוחה (כלל 3), ללא הידור מחדש `Lib1` (כלל 4). 

כלל 2: יישום של התנהגויות חדשות שיושמו על ייצוגים קיימים

בואו נדמיין ב`Lib2`אנחנו רוצים התנהגות חדשה`lastBlock` ליישום ספציפי עבור כל אחד `Blockchain`.

הדבר הראשון שעולה על הדעת הוא יצירת מתג גדול המבוסס על סוג הפרמטר.

סולם

def todo = 42
type Height = Int
type Block = Int

object Lib1:
 trait Blockchain:
 def getBlock(height: Height): Block

 case class Ethereum() extends Blockchain:
 override def getBlock(height: Height) = todo

 case class Bitcoin() extends Blockchain:
 override def getBlock(height: Height) = todo

object Lib2:
 import Lib1.*

 case class Polkadot() extends Blockchain:
 override def getBlock(height: Height): Block = todo

 def lastBlock(blockchain: Blockchain): Block = blockchain match
 case _:Ethereum => todo
 case _:Bitcoin => todo
 case _:Polkadot => todo
 

object Lib3:
 import Lib1.*

 case class Polygon() extends Blockchain:
 override def getBlock(height: Height): Block = todo

import Lib1.*, Lib2.*, Lib3.*
println(lastBlock(Bitcoin()))
println(lastBlock(Ethereum()))
println(lastBlock(Polkadot()))
println(lastBlock(Polygon()))

פתרון זה הוא יישום מחדש חלש של פולימורפיזם מבוסס סוג שכבר אפוי בשפה!

`Lib1` נותר ללא נגיעה (זכור, כלל 4 נאכף בכל מסמך זה). 

הפתרון מיושם ב-`Lib2`הוא בסדר עד שיוצג עוד בלוקצ'יין ב-`Lib3`. זה מפר את כלל בטיחות הסוג (כלל 3) מכיוון שקוד זה נכשל בזמן ריצה בשורה 37. ומשנה `Lib2` יפר את כלל 4.

פתרון נוסף הוא שימוש ב- `extension`.

סולם

def todo = 42
type Height = Int
type Block = Int

object Lib1:
 trait Blockchain:
 def getBlock(height: Height): Block

 case class Ethereum() extends Blockchain:
 override def getBlock(height: Height) = todo

 case class Bitcoin() extends Blockchain:
 override def getBlock(height: Height) = todo

object Lib2:
 import Lib1.*

 case class Polkadot() extends Blockchain:
 override def getBlock(height: Height): Block = todo

 def lastBlock(): Block = todo

 extension (eth: Ethereum) def lastBlock(): Block = todo

 extension (btc: Bitcoin) def lastBlock(): Block = todo

import Lib1.*, Lib2.*
println(Bitcoin().lastBlock())
println(Ethereum().lastBlock())
println(Polkadot().lastBlock())

def polymorphic(blockchain: Blockchain) =
 // blockchain.lastBlock()
 ???

`Lib1` נותר ללא נגיעה (אכיפה של כלל 4 בכל המסמך). 

`Lib2` מגדיר התנהגות עבור הסוג שלו (שורה 21) ו-`הרחבה` עבור סוגים קיימים (שורות 23 ו-25).

שורות 28-30, ניתן להשתמש בהתנהגות החדשה בכל מחלקה. 

אבל אין דרך לקרוא להתנהגות החדשה הזו בצורה פולימורפית (שורה 32). כל ניסיון לעשות זאת מוביל לשגיאות קומפילציה (שורה 33) או למתגים מבוססי הקלדה. 

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

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

דפוס ה-Type Class

למתכון הדגם Type Class (TC בקיצור) יש 3 שלבים. 

  1. הגדירו התנהגות חדשה
  2. יישם את ההתנהגות
  3. השתמש בהתנהגות

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

1. הגדירו התנהגות חדשה
סולם

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

`Lib1` שוב נותר ללא פגע.

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

הפרמטר `A` מייצג את הסוג שאנו רוצים להחיל עליו התנהגות, שהם תת-סוגים של `Blockchain`במקרה שלנו.

כמה הערות:

  • במידת הצורך, הסוג עם הפרמטר `A` ניתן להגביל עוד יותר על ידי המערכת מסוג Scala. למשל, נוכל לאכוף `A`להיות `Blockchain`. 
  • כמו כן, ל-TC יכולות להיות הרבה יותר פונקציות המוצהרות בו.
  • לבסוף, לכל פונקציה עשויים להיות הרבה יותר פרמטרים שרירותיים.

אבל בואו נשאיר דברים פשוטים למען הקריאה.

2. ליישם את ההתנהגות
סולם

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 val ethereumLastBlock = new LastBlock[Ethereum]:
 def lastBlock(eth: Ethereum) = eth.lastBlock

 val bitcoinLastBlock = new LastBlock[Bitcoin]:
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

עבור כל סוג החדש `LastBlock` צפויה התנהגות, יש מופע ספציפי של התנהגות זו. 

ה `Ethereum`שורת יישום 22 מחושבת מתוך `eth` מופע הועבר כפרמטר. 

היישום של `LastBlock`עבור `Bitcoin`שורה 25 מיושמת עם IO לא מנוהל ואינה משתמשת בפרמטר שלו.

אז, `Lib2`מיישמת התנהגות חדשה`LastBlock`עבור `Lib1`שיעורים.

3. השתמש בהתנהגות
סולם

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 val ethereumLastBlock = new LastBlock[Ethereum]:
 def lastBlock(eth: Ethereum) = eth.lastBlock

 val bitcoinLastBlock = new LastBlock[Bitcoin]:
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

import Lib1.*, Lib2.*

def useLastBlock[A](instance: A, behavior: LastBlock[A]) =
 behavior.lastBlock(instance)

println(useLastBlock(Ethereum(lastBlock = 2), ethereumLastBlock))
println(useLastBlock(Bitcoin(), bitcoinLastBlock))

שורה 30 `useLastBlock` משתמש במופע של `A` וה`LastBlock` התנהגות שהוגדרה עבור המקרה הזה.

שורה 33 `useLastBlock` נקרא עם מופע של `Ethereum` ויישום של `LastBlock` מוגדר ב`Lib2`. שימו לב שאפשר לעבור כל יישום חלופי של `LastBlock[A]` (תחשוב על הזרקת תלות).

`useLastBlock` הוא הדבק בין הייצוג (ה-A בפועל) לבין ההתנהגות שלו. נתונים והתנהגות מופרדים, וזה מה שתכנות פונקציונלי דוגל בו.

דיון

בואו נסכם את הכללים של בעיית הביטוי:

  • כלל 1: אפשר את היישום של התנהגויות קיימות  להחיל עליו כיתות חדשות
  • כלל 2: אפשר את היישום של התנהגויות חדשות להחיל עליו כיתות קיימות
  • כלל 3: אסור לו לסכן את בטיחות סוג
  • כלל 4: אין צורך לבצע קומפילציה מחדש קוד קיים

חוק 1 יכול להיפתר מהקופסה עם תת-סוג פולימורפיזם.

דפוס ה-TC שהוצג זה עתה (ראה צילום מסך קודם) פותר את כלל 2. זה בטוח מסוג (כלל 3) ומעולם לא נגענו `Lib1` (כלל 4). 

עם זאת, זה לא מעשי לשימוש מכמה סיבות:

  • שורות 33-34 עלינו להעביר במפורש את ההתנהגות לאורך המופע שלה. זוהי תקורה נוספת. אנחנו צריכים פשוט לכתוב `useLastBlock(Bitcoin())`.
  • בשורה 31 התחביר אינו שכיח. נעדיף לכתוב תמציתית ומונחה יותר עצמים  `instance.lastBlock()`הצהרה.

בואו נדגיש כמה תכונות של Scala לשימוש מעשי ב-TC. 

חווית מפתח משופרת

ל- Scala יש סט ייחודי של תכונות וסוכרים תחביריים שהופכים את TC לחוויה מהנה באמת עבור מפתחים.

משתמע

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

תוכנית מציבה מופע בטווח המרומז עם ה-`given` מילת מפתח. לחלופין, תוכנית יכולה לאחזר מופע מהטווח המרומז עם מילת המפתח `using`.

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

זה שונה מהיקף גלובלי כי: 

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

חוזרים לשיעור טיפוס! בואו ניקח את אותה דוגמה בדיוק.

סולם

def todo = 42
type Height = Int
type Block = Int
def http(uri: String): Block = todo

object Lib1:
 trait Blockchain:
 def getBlock(height: Height): Block

 case class Ethereum() extends Blockchain:
 override def getBlock(height: Height) = todo

 case class Bitcoin() extends Blockchain:
 override def getBlock(height: Height) = todo

`Lib1` הוא אותו קוד ללא שינוי שהגדרנו בעבר. 

סולם

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 given ethereumLastBlock:LastBlock[Ethereum] = new LastBlock[Ethereum]:
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given bitcoinLastBlock:LastBlock[Bitcoin] = new LastBlock[Bitcoin]:
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

import Lib1.*, Lib2.*

def useLastBlock[A](instance: A)(using behavior: LastBlock[A]) =
 behavior.lastBlock(instance)

println(useLastBlock(Ethereum(lastBlock = 2)))
println(useLastBlock(Bitcoin()))

שורה 19 התנהגות חדשה `LastBlock` מוגדר, בדיוק כמו שעשינו בעבר.

קו 22 וקו 25, `val` מוחלף ב`given`. שני ההטמעות של `LastBlock` מוכנסים לטווח המרומז.

שורה 31 `useLastBlock` מצהיר על ההתנהגות `LastBlock` כפרמטר מרומז. המהדר פותר את המופע המתאים של `LastBlock` מתוך היקף מרומז שהוגדר בהקשר ממיקומי מתקשרים (שורות 33 ו-34). קו 28 מייבא הכל מ`Lib2`, כולל ההיקף המשתמע. אז, המהדר מעביר מופעים שהוגדרו שורות 22 ו-25 כפרמטר האחרון של `useLastBlock`. 

כמשתמש בספרייה, השימוש במחלקת סוג קל יותר מבעבר. בשורות 34 ו-35 מפתח צריך רק לוודא שמופע של ההתנהגות מוזרק בהיקף המרומז (וזה יכול להיות רק `import`). אם משתמע אינו `given` איפה הקוד `using`זה, אומר לו המהדר.

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

סוכרים מרומזים

ניתן לשפר עוד יותר את שורות 22 ו-25 של הקוד הקודם! בואו נחזור על יישומי TC.

סולם

given LastBlock[Ethereum] = new LastBlock[Ethereum]:
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given LastBlock[Bitcoin] = new LastBlock[Bitcoin]:
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

שורות 22 ו-25, אם שם המופע אינו בשימוש, ניתן להשמיט אותו.

סולם


 given LastBlock[Ethereum] with
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given LastBlock[Bitcoin] with
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

שורות 22 ו-25, ניתן להחליף את החזרה על הסוג ב`with` מילת מפתח.

סולם

given LastBlock[Ethereum] = _.lastBlock

 given LastBlock[Bitcoin] = _ => http("http://bitcoin/last")

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

Scala מציעה סוכרים תחביריים כדי לייעל את התחביר, להסיר שמות מיותרים, הצהרות ויתירות סוג.

הארכה

בשימוש מושכל, את `extensionמנגנון ` יכול לפשט את התחביר לשימוש במחלקת סוג.

סולם

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 given LastBlock[Ethereum] with
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given LastBlock[Bitcoin] with
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

 extension[A](instance: A)
 def lastBlock(using tc: LastBlock[A]) = tc.lastBlock(instance)

import Lib1.*, Lib2.*

println(Ethereum(lastBlock = 2).lastBlock)
println(Bitcoin().lastBlock)

שורות 28-29 שיטת הרחבה גנרית `lastBlock` מוגדר עבור כל `A` עם `LastBlock` פרמטר TC בהיקף משתמע.

שורות 33-34 ההרחבה ממנפת תחביר מונחה עצמים כדי להשתמש ב-TC.

סולם

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 given LastBlock[Ethereum] with
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given LastBlock[Bitcoin] with
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

 extension[A](instance: A)(using tc: LastBlock[A])
 def lastBlock = tc.lastBlock(instance)
 def penultimateBlock = tc.lastBlock(instance) - 1

import Lib1.*, Lib2.*

val eth = Ethereum(lastBlock = 2)
println(eth.lastBlock)
println(eth.penultimateBlock)

val btc = Bitcoin()
println(btc.lastBlock)
println(btc.penultimateBlock)

שורה 28, ניתן להגדיר את הפרמטר TC גם עבור כל ההרחבה כדי למנוע חזרה. בשורה 30 אנו עושים שימוש חוזר ב-TC בהרחבה כדי להגדיר `penultimateBlock` (למרות שניתן ליישם אותו ב-`LastBlock` תכונה ישירות)

הקסם מתרחש כאשר משתמשים ב-TC. הביטוי מרגיש הרבה יותר טבעי, נותן אשליה שהתנהגות `lastBlock` מתבלבל עם המופע.

סוג גנרי עם TC
סולם

import Lib1.*, Lib2.*

def useLastBlock1[A](instance: A)(using LastBlock[A]) = instance.lastBlock

def useLastBlock2[A: LastBlock](instance: A) = instance.lastBlock

val eth = Ethereum(lastBlock = 2)
assert(useLastBlock1(eth) == useLastBlock2(eth))

בשורה 34 הפונקציה משתמשת ב-TC מרומז. שימו לב שאין צורך לתת שם ל-TC אם השם הזה מיותר.

דפוס ה-TC נמצא בשימוש כה נרחב שיש תחביר סוג גנרי לבטא "טיפוס עם התנהגות מרומזת". שורה 36 התחביר הוא חלופה תמציתית יותר לקודמת (שורה 34). הוא נמנע מהכרזה ספציפית על פרמטר TC המרומז ללא שם.

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

גזירה אוטומטית

הרבה ספריות Scala משתמשות ב-TC, מה שמשאיר למתכנת ליישם אותן בבסיס הקוד שלהן.

לדוגמה Circe (ספריית דה-סריאליזציה של json) משתמשת ב-TC `Encoder[T]` ו`Decoder[T]` עבור מתכנתים ליישם בבסיס הקוד שלהם. לאחר היישום ניתן להשתמש בכל היקף הספרייה. 

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

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

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

גזירת TC באופן כללי היא נפוצה מאוד, אז Scala הציגה ארגז כלים שלם למטרה זו. שיטה זו לא תמיד מפורסמת על ידי תיעוד הספרייה למרות שזו שיטת Scala 3 להשתמש בגזירה.

סולם

object GenericLib:

 trait Named[A]:
 def blockchainName(instance: A): String

 object Named:
 import scala.deriving.*

 inline final def derived[A](using inline m: Mirror.Of[A]): Named[A] =
 val nameOfType: String = inline m match
 case p: Mirror.ProductOf[A] => compiletime.constValue[p.MirroredLabel]
 case _ => compiletime.error("Not a product")
 new Named[A]:
 override def blockchainName(instance: A):String = nameOfType.toLowerCase

 extension[A] (instance: A)(using tc: Named[A])
 def blockchainName = tc.blockchainName(instance)

import Lib1.*, GenericLib.*

case class Polkadot() derives Named
given Named[Bitcoin] = Named.derived
given Named[Ethereum] = Named.derived

println(Ethereum(lastBlock = 2).blockchainName)
println(Bitcoin().blockchainName)
println(Polkadot().blockchainName)

קו 18 TC חדש `Named` מוצג. TC זה לא קשור לעסקי הבלוקצ'יין, למהדרין. מטרתו לתת שם לבלוקצ'יין על סמך שם מחלקת המקרים.

תחילה התמקדו בהגדרות שורות 36-38. ישנם 2 תחבירים לגזירת TC:

  1. בשורה 36 ניתן להגדיר את מופע ה-TC ישירות על מחלקת המקרה עם ה- `derives` מילת מפתח. מתחת למכסה המנוע המהדר יוצר `Named` מופע ב`Polkadot` חפץ נלווה.
  2. שורה 37 ו-38, מופעי מחלקות מסוג ניתנים במחלקות קיימות מראש עם `TC.derived

שורה 31 מוגדרת הרחבה גנרית (ראה סעיפים קודמים) ו`blockchainName` משמש באופן טבעי.  

ה `derives` מילת מפתח מצפה לשיטה עם הטופס `inline def derived[T](using Mirror.Of[T]): TC[T] = ???` שמוגדר שורה 24. לא אסביר לעומק מה הקוד עושה. בקווים כלליים:

  • `inline def` מגדיר מאקרו
  • `Mirror` הוא חלק מארגז הכלים להסתכלות פנימית על טיפוסים. ישנם סוגים שונים של מראות, ובשורה 26 הקוד מתמקד `Product` מראות (מחלקת מקרה היא מוצר). שורה 27, אם מתכנתים מנסים לגזור משהו שהוא לא `Product`, הקוד לא יקמפל.
  • את `Mirror` מכיל סוגים אחרים. אחד מהם, `MirrorLabel`, היא מחרוזת המכילה את שם הסוג. ערך זה משמש ביישום, שורה 29, של ה-`Named` TC.

מחברי TC יכולים להשתמש בתכנות מטא כדי לספק פונקציות שמייצרות באופן כללי מופעים של TC בהינתן סוג. מתכנתים יכולים להשתמש ב-API ייעודי של ספרייה או בכלים להפקת Scala כדי ליצור מופעים עבור הקוד שלהם.

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

סיכום כל ההטבות

  • זה פותר את בעיית הביטוי
    • טיפוסים חדשים יכולים ליישם התנהגות קיימת באמצעות הורשת תכונה מסורתית
    • ניתן ליישם התנהגויות חדשות על סוגים קיימים
  • הפרדת דאגה
    • הקוד אינו מעוות וניתן למחיקה בקלות. TC מפריד בין נתונים והתנהגות, שהוא מוטו תכנות פונקציונלי.
  • זה בטוח
    • זה בטוח כי זה לא מסתמך על התבוננות פנימית. זה נמנע מהתאמת דפוסים גדולה הכוללת סוגים. אם אתה נתקל בעצמך כותב קוד כזה, אתה עשוי לזהות מקרה שבו דפוס TC יתאים בצורה מושלמת.
    • המנגנון המרומז בטוח להידור! אם חסר מופע בזמן ההידור, הקוד לא יקמפל. אין הפתעה בזמן ריצה.
  • זה מביא לפולימורפיזם אד-הוק
    • פולימורפיזם אד-הוק חסר בדרך כלל בתכנות מסורתי מונחה עצמים.
    • עם פולימורפיזם אד-הוק, מפתחים יכולים ליישם את אותה התנהגות עבור סוגים שונים שאינם קשורים מבלי להשתמש בהקלדת משנה מסורתית (המצמדת את הקוד)
  • הזרקת תלות קלה
    • ניתן לשנות מופע TC ביחס לעקרון ההחלפה של Liskov. 
    • כאשר לרכיב יש תלות ב-TC, ניתן להזריק בקלות TC מדומה למטרות בדיקה. 

אינדיקציות נגד

כל פטיש מיועד למגוון בעיות.

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

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

לדוגמה, בליבת Scala, יש `Numericסוג מחלקה:

סולם

trait Numeric[T] extends Ordering[T] {
 def plus(x: T, y: T): T
 def minus(x: T, y: T): T
 def times(x: T, y: T): T

זה באמת הגיוני להשתמש במחלקת טיפוס כזו מכיוון שהיא לא רק מאפשרת שימוש חוזר באלגוריתמים אלגבריים על טיפוסים המוטבעים ב-Scala (Int, BigInt, ...), אלא גם על טיפוסים המוגדרים על ידי המשתמש (a `ComplexNumber` למשל).

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

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

סיכום

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

מקווה שרכשת ידע בקריאת מסמך זה. 

הקוד זמין ב https://github.com/jprudent/type-class-article. אנא פנה אליי אם יש לך סוג של שאלות או הערות. אתה יכול להשתמש בבעיות או בהערות קוד במאגר אם תרצה.


ג'רום פרודנט

מהנדס תוכנה

בול זמן:

עוד מ פנקס