Scala3 tüübiklassid: juhend algajatele | Pearaamat

Scala3 tüübiklassid: juhend algajatele | Pearaamat

Allikasõlm: 3028113

See dokument on mõeldud algajale Scala3 arendajale, kes on juba Scala proosaga kursis, kuid on hämmingus kõigi `implicits` ja koodis parameetristatud tunnused.

See dokument selgitab, miks, kuidas, kus ja millal Tüübiklassid (TC).

Pärast selle dokumendi lugemist omandab algaja Scala3 arendaja põhjalikud teadmised kasutamiseks ja sukeldub lähtekoodi palju Scala raamatukogudest ja hakata kirjutama idiomaatilist Scala koodi.

Alustame sellest, miks…

Väljendusprobleem

Aastal 1998, Philip Wadler teatas et "väljendusprobleem on vana probleemi uus nimi". See on tarkvara laiendatavuse probleem. Härra Wadleri kirjutise järgi peab väljendusprobleemi lahendus vastama järgmistele reeglitele:

  • Reegel 1: lubage rakendada olemasolevad käitumised (mõelge Scala tunnusele), mida rakendada uued esindused (mõelge juhtumiklassile)
  • 2. reegel: lubage rakenduse rakendamine uusi käitumisviise millele rakendada olemasolevad esindused
  • Reegel 3: see ei tohi ohustada tüübi ohutus
  • Reegel 4: see ei tohi nõuda uuesti kompileerimist olemasolev kood

Selle probleemi lahendamine on selle artikli hõbedane niit.

Reegel 1: olemasoleva käitumise rakendamine uue esituse korral

Igal objektorienteeritud keelel on reegli 1 jaoks sissetöötatud lahendus alatüübi polümorfism. Võite ohutult rakendada mis tahes `trait` määratletud sõltuvuses `class` oma koodis, ilma sõltuvust uuesti kompileerimata. Vaatame seda tegevuses:

Scala

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()

Selles fiktiivses näites raamatukogu `Lib1` (rida 5) määratleb tunnuse `Blockchain` (rida 6) selle kahe teostusega (read 2 ja 9). `Lib1` jääb KÕIGES selles dokumendis samaks (reegli 4 jõustamine).

`Lib2` (rida 15) rakendab olemasolevat käitumist `Blockchain`uues klassis`Polkadot` (reegel 1) tüüpi turvalisel (reegel 3) viisil, ilma ` uuesti kompileerimataLib1` (reegel 4). 

Reegel 2: uute käitumisviiside rakendamine olemasolevatele esitustele

Kujutame ette `Lib2"Me tahame uut käitumist".lastBlock` rakendatakse konkreetselt iga ` jaoksBlockchain`.

Esimene asi, mis meelde tuleb, on suure lüliti loomine parameetri tüübi alusel.

Scala

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()))

See lahendus on juba sissetöötatud tüübipõhise polümorfismi nõrk taasteostus!

`Lib1` jäetakse puutumata (pidage meeles, et reegel 4 kehtib kogu selles dokumendis). 

Aastal `Lib2` on okei kuni veel üks plokiahel võetakse kasutusele `Lib3`. See rikub tüübiohutuse reeglit (reegel 3), kuna see kood ebaõnnestub käitusajal real 37.Lib2rikuks reeglit 4.

Teine lahendus on `extension`.

Scala

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` jäetakse puutumata (4. reegli jõustamine kogu dokumendis). 

`Lib2` määrab oma tüübi käitumise (rida 21) ja laiendust olemasolevate tüüpide jaoks (read 23 ja 25).

Ridadel 28–30 saab uut käitumist kasutada igas klassis. 

Kuid seda uut käitumist ei saa kuidagi nimetada polümorfseks (rida 32). Iga katse seda teha põhjustab kompileerimisvigu (rida 33) või tüübipõhiseid lüliteid. 

See reegel nr 2 on keeruline. Püüdsime seda rakendada oma polümorfismi definitsiooni ja "pikendustriki" abil. Ja see oli imelik.

Puudu on tükk nn ad hoc polümorfism: võime käitumist ohutult edastada vastavalt tüübile, olenemata sellest, kus käitumine ja tüüp on määratletud. Sisestage Tüüp Klass muster.

Tüübiklassi muster

Tüübiklassi (lühidalt TC) mustri retseptil on 3 etappi. 

  1. Määratlege uus käitumine
  2. Rakenda käitumist
  3. Kasutage käitumist

Järgmises jaotises rakendan TC mustrit kõige arusaadavamal viisil. See on paljusõnaline, kohmakas ja ebapraktiline. Kuid oodake, need hoiatused fikseeritakse dokumendis samm-sammult edasi.

1. Määratlege uus käitumine
Scala

object Lib2:
  import Lib1.*

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

`Lib1` on taaskord puutumata jäetud.

Uus käitumine is TC materialiseerus selle tunnusega. Tunnuses määratletud funktsioonid on viis selle käitumise mõningate aspektide rakendamiseks.

Parameeter `A` tähistab tüüpi, millele tahame käitumist rakendada, mis on ` alamtüübidBlockchain` meie puhul.

Mõned märkused:

  • Vajadusel parameetritega tüüp `A` saab veelgi piirata Scala tüüpi süsteemiga. Näiteks võime jõustada `A`olla `Blockchain`. 
  • Samuti võiks TC-l olla palju rohkem funktsioone.
  • Lõpuks võib igal funktsioonil olla palju rohkem suvalisi parameetreid.

Kuid olgem loetavuse huvides asjad lihtsad.

2. Rakenda käitumine
Scala

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")

Iga tüübi jaoks uus `LastBlock` käitumist oodatakse, on selle käitumise konkreetne juhtum. 

`Ethereum` rakendusrida 22 arvutatakse `eth` eksemplar edastati parameetrina. 

Rakendamine `LastBlock` jaoks `Bitcoin` rida 25 on rakendatud haldamata IO-ga ja ei kasuta selle parameetrit.

Niisiis, `Lib2`rakendab uut käitumist`LastBlock` jaoks `Lib1` klassid.

3. Kasutage käitumist
Scala

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))

Rida 30 `useLastBlock` kasutab eksemplari `A` ja `LastBlock` selle juhtumi jaoks määratletud käitumine.

Rida 33 `useLastBlock` kutsutakse koos eksemplariga `Ethereum` ja rakenduse ` rakendamineLastBlock`määratletud `Lib2`. Pange tähele, et on võimalik edastada mis tahes alternatiivset `rakendustLastBlock[A]` (mõtle sõltuvussüst).

`useLastBlock` on liim esituse (tegelik A) ja selle käitumise vahel. Andmed ja käitumine on eraldatud, mida funktsionaalne programmeerimine toetab.

Arutelu

Kordame väljendusprobleemi reegleid:

  • Reegel 1: lubage rakendada olemasolevad käitumised  millele rakendada uued klassid
  • 2. reegel: lubage rakenduse rakendamine uusi käitumisviise millele rakendada olemasolevad klassid
  • Reegel 3: see ei tohi ohustada tüübi ohutus
  • Reegel 4: see ei tohi nõuda uuesti kompileerimist olemasolev kood

Reegli 1 saab lahendada alamtüübi polümorfismiga.

Äsja esitatud TC muster (vt eelmist ekraanipilti) lahendab 2. reegli. See on tüüpiline (reegel 3) ja me ei puudutanud kunagi 'Lib1` (reegel 4). 

Siiski on selle kasutamine ebapraktiline mitmel põhjusel:

  • Ridadel 33–34 peame käitumise selgesõnaliselt edastama selle eksemplari järgi. See on lisakulu. Peaksime lihtsalt kirjutama `useLastBlock(Bitcoin())`.
  • 31. rea süntaks on haruldane. Eelistaksime kirjutada lühidalt ja rohkem objektile orienteeritud  `instance.lastBlock()`avaldus.

Toome välja mõned Scala funktsioonid TC praktiliseks kasutamiseks. 

Täiustatud arendajakogemus

Scalal on ainulaadne komplekt funktsioone ja süntaktilisi suhkruid, mis muudavad TC arendajatele tõeliselt nauditavaks kogemuseks.

Implitsiitne

Kaudne ulatus on spetsiaalne ulatus, mis lahendatakse kompileerimise ajal ja kus saab eksisteerida ainult üks antud tüüpi eksemplar. 

Programm paneb eksemplari kaudsesse ulatusse koos `given` märksõna. Alternatiivina saab programm tuua eksemplari kaudsest ulatusest märksõnaga `using`.

Kaudne ulatus lahendatakse kompileerimise ajal, on teada, kuidas seda käitusajal dünaamiliselt muuta. Kui programm kompileerib, lahendatakse kaudne ulatus. Käitusajal ei ole võimalik, et kaudsed eksemplarid puuduvad, kus neid kasutatakse. Ainus võimalik segadus võib tuleneda vale kaudse eksemplari kasutamisest, kuid see probleem jääb tooli ja klaviatuuri vahele jäävale olendile.

See erineb globaalsest ulatusest, kuna: 

  1. See lahendatakse kontekstuaalselt. Programmi kaks asukohta võivad kaudses ulatuses kasutada sama tüüpi eksemplari, kuid need kaks eksemplari võivad olla erinevad.
  2. Stseeni taga edastab kood kaudsete argumentide funktsiooni, et see toimiks kuni kaudse kasutuse saavutamiseni. See ei kasuta globaalset mäluruumi.

Tagasi tüübiklassi! Võtame täpselt sama näite.

Scala

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` on sama muutmata kood, mille me varem määratlesime. 

Scala

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()))

Rida 19 uus käitumineLastBlock` on defineeritud täpselt nagu varem.

Rida 22 ja rida 25, `val` asendatakse `gagiven`. Mõlemad rakenduse `LastBlock` paigutatakse kaudsesse ulatusse.

Rida 31 `useLastBlock`deklareerib käitumist`LastBlock` kaudse parameetrina. Kompilaator lahendab ` sobiva eksemplariLastBlock` kaudsest ulatusest, kontekstualiseerituna helistaja asukohtadest (read 33 ja 34). Rida 28 impordib kõik alates `Lib2”, sealhulgas kaudne ulatus. Seega edastab kompilaator eksemplaridele defineeritud read 22 ja 25 viimase parameetrinauseLastBlock`. 

Raamatukogu kasutajana on tüübiklassi kasutamine varasemast lihtsam. 34. ja 35. rida peab arendaja ainult tagama, et käitumise eksemplar sisestatakse kaudsesse ulatusse (ja see võib olla lihtsalt "import`). Kui kaudne ei ole `given` kus on kood `using` seda, ütleb koostaja talle.

Scala kaudne hõlbustab klassi eksemplaride edastamist koos nende käitumisjuhtumitega.

Kaudsed suhkrud

Eelmise koodi ridu 22 ja 25 saab veelgi täiustada! Kordame TC rakendusi.

Scala

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")

Read 22 ja 25, kui eksemplari nime ei kasutata, võib selle ära jätta.

Scala


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

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

Ridadel 22 ja 25 võib tüübi kordumise asendada `gawith` märksõna.

Scala

given LastBlock[Ethereum] = _.lastBlock

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

Kuna me kasutame ühe funktsiooniga degenereerunud tunnust, võib IDE soovitada koodi lihtsustada SAM-avaldise abil. Kuigi see on õige, ei usu ma, et see on SAM-i õige kasutamine, välja arvatud juhul, kui mängite juhuslikult golfi.

Scala pakub süntaksi sujuvamaks muutmiseks süntaktilisi suhkruid, eemaldades tarbetu nimetamise, deklareerimise ja tüübi liiasuse.

Laiendamine

Targalt kasutades on `extension` mehhanism võib tüübiklassi kasutamise süntaksit lihtsustada.

Scala

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)

Read 28–29 on üldine laiendusmeetod `lastBlock` on määratletud mis tahes ` jaoksA` koos `-gaLastBlock` TC parameeter kaudses ulatuses.

Read 33–34 kasutab laiendus TC kasutamiseks objektorienteeritud süntaksit.

Scala

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. real saab TC parameetri määrata ka kogu laienduse jaoks, et vältida kordusi. 30. real kasutame laienduse TC-d uuesti, et defineerida `penultimateBlock` (kuigi seda saab rakendada `LastBlock`omadus otseselt)

Maagia juhtub siis, kui TC-d kasutatakse. Väljend tundub palju loomulikum, andes illusiooni, et käitumine "lastBlock` segatakse eksemplariga.

Üldine tüüp TC-ga
Scala

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))

Rida 34 kasutab funktsioon kaudset TC-d. Pange tähele, et TC-d ei pea nimetama, kui see nimi pole vajalik.

TC-mustrit kasutatakse nii laialdaselt, et on olemas üldine tüübisüntaks, mis väljendab "kaudse käitumisega tüüpi". 36. rea süntaks on ülevaatlikum alternatiiv eelmisele (rida 34). See väldib nimetamata kaudse TC parameetri konkreetset deklareerimist.

Sellega lõpetatakse arendajakogemuse jaotis. Oleme näinud, kuidas laiendused, implitsiitsed ja teatud süntaktiline suhkur võivad TC kasutamisel ja määratlemisel pakkuda vähem segast süntaksit.

Automaatne tuletamine

Paljud Scala teegid kasutavad TC-d, jättes programmeerijale need oma koodibaasi juurutama.

Näiteks Circe (jsoni deserialiseerimise teek) kasutab TC-d `Encoder[T]` ja `Decoder[T]` programmeerijatele oma koodibaasi juurutamiseks. Pärast rakendamist saab kasutada kogu raamatukogu ulatust. 

Need TC rakendused on rohkem kui sageli andmetele orienteeritud kaardistajad. Nad ei vaja mingit äriloogikat, nende kirjutamine on igav ja neid on raske juhtumiklassidega sünkroonis hoida.

Sellises olukorras pakuvad need raamatukogud nn automaatne tuletus või poolautomaatne tuletus. Vt näiteks Circe automaatne ja poolautomaatne tuletus. Poolautomaatse tuletamise korral saab programmeerija deklareerida tüübiklassi eksemplari mõne väiksema süntaksiga, samas kui automaatne tuletamine ei nõua koodi muutmist, välja arvatud importimine.

Kapoti all, kompileerimise ajal, üldised makrod sisekaemus liigid puhta andmestruktuurina ja luua raamatukogu kasutajatele TC[T]. 

Üldine TC tuletamine on väga levinud, seetõttu tutvustas Scala selleks otstarbeks täielikku tööriistakasti. Seda meetodit ei reklaamita alati raamatukogu dokumentatsioonis, kuigi see on Scala 3 tuletamise viis.

Scala

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)

Rida 18 uus TC `Named` tutvustatakse. See TC ei ole rangelt võttes plokiahela äriga seotud. Selle eesmärk on anda plokiahelale nimi juhtumiklassi nime põhjal.

Kõigepealt keskenduge definitsiooniridadele 36–38. TC tuletamiseks on kaks süntaksit:

  1. Real 36 saab TC eksemplari defineerida otse juhtumiklassis `derives` märksõna. Kapoti all genereerib kompilaator etteantud `Named` näide in `Polkadot` kaasobjekt.
  2. Rida 37 ja 38, tüübiklasside eksemplarid antakse juba olemasolevatele klassidele koos `TC.derived

Rida 31 on määratletud üldine laiend (vt eelmisi jaotisi) ja `blockchainName` kasutatakse loomulikult.  

`derives`märksõna ootab meetodit vormiga `inline def derived[T](using Mirror.Of[T]): TC[T] = ???`, mis on määratletud real 24. Ma ei selgita põhjalikult, mida kood teeb. Üldjoontes:

  • `inline def` määrab makro
  • `Mirror` on osa tüüpide enesevaatluse tööriistakastist. Peegleid on erinevat tüüpi ja kood keskendub reale 26Product` peeglid (korpuse klass on toode). Rida 27, kui programmeerijad püüavad tuletada midagi, mis ei ole `Product`, koodi ei kompileerita.
  • `Mirror` sisaldab muid tüüpe. Üks neist, `MirrorLabel` on string, mis sisaldab tüübi nime. Seda väärtust kasutatakse real 29 real `Named` TC.

TC autorid saavad kasutada metaprogrammeerimist, et pakkuda funktsioone, mis genereerivad teatud tüüpi TC eksemplare. Programmeerijad saavad oma koodi jaoks eksemplaride loomiseks kasutada spetsiaalset teegi API-d või Scala tuletamistööriistu.

Ükskõik, kas vajate TC juurutamiseks üldist või spetsiifilist koodi, iga olukorra jaoks on lahendus olemas. 

Kokkuvõte kõigist eelistest

  • See lahendab väljendusprobleemi
    • Uued tüübid saavad rakendada olemasolevat käitumist traditsiooniliste tunnuste pärimise kaudu
    • Olemasolevatel tüüpidel saab rakendada uusi käitumisviise
  • Mure eraldamine
    • Kood ei ole segatud ja kergesti kustutatav. TC eraldab andmed ja käitumise, mis on funktsionaalse programmeerimise moto.
  • See on ohutu
    • See on tüüpiline, sest see ei tugine sisekaemusele. See väldib suurt mustrite sobitamist, mis hõlmab tüüpe. Kui kohtate end sellist koodi kirjutamas, võite tuvastada juhtumi, kus TC muster sobib ideaalselt.
    • Kaudne mehhanism on kompileerimine ohutu! Kui eksemplar puudub kompileerimise ajal, siis koodi ei kompileerita. Käitusajal pole üllatust.
  • See toob kaasa ad-hoc polümorfismi
    • Traditsioonilises objektorienteeritud programmeerimises puudub ad hoc polümorfism.
    • Ad-hoc polümorfismi abil saavad arendajad rakendada sama käitumist erinevate mitteseotud tüüpide jaoks, kasutamata traditsioonilist alamtüüpi (mis seob koodi)
  • Sõltuvussüst on tehtud lihtsaks
    • TC eksemplari saab muuta Liskovi asenduspõhimõtte järgi. 
    • Kui komponent sõltub TC-st, saab pilatud TC-d hõlpsasti testimise eesmärgil süstida. 

Vastunäidustused

Iga haamer on loodud mitmesuguste probleemide jaoks.

Tüübiklassid on mõeldud käitumisprobleemide jaoks ja neid ei tohi kasutada andmete pärimiseks. Kasutage selleks kompositsiooni.

Tavaline alamtüüp on lihtsam. Kui teil on koodibaas ja te ei soovi laiendada, võivad tüübiklassid olla üle jõu käivad.

Näiteks Scala tuumas on `Numeric` tüüpi klass:

Scala

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

Sellist tüübiklassi on tõesti mõttekas kasutada, kuna see võimaldab mitte ainult algebraliste algoritmide taaskasutamist tüüpidel, mis on Scalasse manustatud (Int, BigInt, …), vaid ka kasutaja määratud tüüpidel (a `ComplexNumber` näiteks).

Teisest küljest kasutatakse Scala kogude juurutamisel tüübiklassi asemel enamasti alamtüüpi. See disain on mõttekas mitmel põhjusel:

  • Kogumise API peaks olema täielik ja stabiilne. See paljastab levinud käitumise juurutuste kaudu päritud tunnuste kaudu. Väga laiendatav olemine ei ole siin eriline eesmärk.
  • Seda peab olema lihtne kasutada. TC lisab lõppkasutaja programmeerijale vaimse lisakulu.
  • TC võib jõudluses ka väikeseid üldkulusid tekitada. See võib olla kogumise API jaoks kriitiline.
  • Kogu API on siiski laiendatav kolmandate osapoolte teekide määratletud uue TC kaudu.

Järeldus

Oleme näinud, et TC on lihtne muster, mis lahendab suure probleemi. Tänu Scala rikkalikule süntaksile saab TC-mustrit rakendada ja kasutada mitmel viisil. TC muster on kooskõlas funktsionaalse programmeerimise paradigmaga ja on suurepärane tööriist puhta arhitektuuri jaoks. Hõbekuul puudub ja TC muster tuleb rakendada siis, kui see sobib.

Loodetavasti saite seda dokumenti lugedes teadmisi juurde. 

Kood on saadaval aadressil https://github.com/jprudent/type-class-article. Palun võtke minuga ühendust, kui teil on küsimusi või märkusi. Soovi korral saate hoidlas kasutada probleeme või koodi kommentaare.


Jerome PRUDENT

Tarkvara insener

Ajatempel:

Veel alates pearaamat