Tipski razredi v Scala3: Vodnik za začetnike | Ledger

Tipski razredi v Scala3: Vodnik za začetnike | Ledger

Tipski razredi v Scala3: Vodnik za začetnike | Podatkovna inteligenca Ledger PlatoBlockchain. Navpično iskanje. Ai.

Ta dokument je namenjen razvijalcu Scala3 začetnikom, ki je že seznanjen s prozo Scala, vendar je zmeden glede vseh `implicits` in parametrizirane lastnosti v kodi.

Ta dokument pojasnjuje zakaj, kako, kje in kdaj Tipski razredi (TC).

Po branju tega dokumenta bo začetnik razvijalec Scala3 pridobil dobro znanje za uporabo in se poglobil v izvorno kodo veliko knjižnic Scala in začnite pisati idiomatsko kodo Scala.

Začnimo s tem, zakaj …

Problem izražanja

V 1998, je dejal Philip Wadler da je »izrazni problem novo ime za stari problem«. To je problem razširljivosti programske opreme. Po pisanju gospoda Wadlerja mora rešitev izraznega problema upoštevati naslednja pravila:

  • Pravilo 1: Dovolite izvajanje obstoječa vedenja (pomislite na lastnost Scala), za katero je treba uporabiti nove reprezentance (pomislite na razred primerov)
  • 2. pravilo: dovolite izvedbo nova vedenja ki se nanaša na obstoječa predstavništva
  • 3. pravilo: Ne sme ogroziti vrsta varnosti
  • Pravilo 4: Ne sme zahtevati ponovnega prevajanja obstoječo kodo

Rešitev tega problema bo srebrna nit tega članka.

Pravilo 1: izvajanje obstoječega vedenja na novi predstavitvi

Vsak objektno usmerjen jezik ima vgrajeno rešitev za pravilo 1 s podtipski polimorfizem. Varno lahko izvajate kateri koli `trait` definiran v odvisnosti od `class` v lastni kodi, brez ponovnega prevajanja odvisnosti. Poglejmo to v akciji:

Lestvica

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

V tem izmišljenem primeru knjižnica `Lib1` (vrstica 5) definira lastnost `Blockchain` (vrstica 6) z dvema njegovima implementacijama (vrstici 2 in 9). `Lib1` bo ostal enak v VSEH tem dokumentu (uveljavitev pravila 4).

`Lib2` (vrstica 15) izvaja obstoječe vedenje `Blockchain`v novem razredu`Polkadot` (pravilo 1) na tipsko varen (pravilo 3) način, brez ponovnega prevajanja `Lib1` (pravilo 4). 

2. pravilo: implementacija novih vedenj, ki se uporabljajo za obstoječe predstavitve

Predstavljajmo si v `Lib2`želimo novo vedenje`lastBlock` implementirati posebej za vsakega `Blockchain`.

Prva stvar, ki pride na misel, je ustvarjanje velikega stikala glede na vrsto parametra.

Lestvica

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

Ta rešitev je šibka ponovna implementacija polimorfizma na osnovi tipa, ki je že vgrajen v jezik!

`Lib1` ostane nedotaknjen (ne pozabite, vsiljeno pravilo 4 po vsem tem dokumentu). 

Rešitev, implementirana v `Lib2` je v redu dokler v ` ne bo predstavljena še ena veriga blokovLib3`. Krši varnostno pravilo tipa (pravilo 3), ker ta koda ne uspe med izvajanjem v vrstici 37. In spreminjanje `Lib2` bi kršil 4. pravilo.

Druga rešitev je uporaba `extension`.

Lestvica

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` ostane nedotaknjen (uveljavljanje pravila 4 v celotnem dokumentu). 

`Lib2` definira vedenje za svoj tip (vrstica 21) in `razširitve` za obstoječe tipe (vrstici 23 in 25).

Vrstice 28-30, novo vedenje je mogoče uporabiti v vsakem razredu. 

Toda tega novega vedenja ni mogoče imenovati polimorfno (vrstica 32). Vsak poskus tega vodi do napak pri prevajanju (vrstica 33) ali do tipskih preklopov. 

To pravilo št. 2 je zapleteno. Poskušali smo ga implementirati z lastno definicijo polimorfizma in trikom `razširitve`. In to je bilo čudno.

Manjka del imenovan ad hoc polimorfizem: zmožnost varnega odpošiljanja izvedbe vedenja glede na vrsto, kjerkoli sta vedenje in tip definirana. Vnesite Vrsta razreda vzorec.

Vzorec tipa razreda

Recept za vzorec tipa Type Class (kratko TC) ima 3 korake. 

  1. Določite novo vedenje
  2. Izvedite vedenje
  3. Uporabite vedenje

V naslednjem razdelku implementiram vzorec TC na najbolj preprost način. Je besedno, okorno in nepraktično. Toda počakaj, ta opozorila bodo popravljena korak za korakom naprej v dokumentu.

1. Določite novo vedenje
Lestvica

object Lib2:
 import Lib1.*

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

`Lib1` spet ostane nedotaknjen.

Novo vedenje is TC, ki ga materializira lastnost. Funkcije, opredeljene v lastnosti, so način za uporabo nekaterih vidikov tega vedenja.

Parameter `A` predstavlja tip, za katerega želimo uporabiti vedenje, ki so podtipi `Blockchain` v našem primeru.

Nekaj ​​pripomb:

  • Če je potrebno, parametriran tip `A` lahko dodatno omejimo s sistemom tipa Scala. Lahko bi na primer uveljavili `A`biti `Blockchain`. 
  • Prav tako bi lahko imel TC deklariranih veliko več funkcij.
  • Končno ima lahko vsaka funkcija veliko več poljubnih parametrov.

Vendar naj bodo stvari preproste zaradi berljivosti.

2. Izvedite vedenje
Lestvica

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

Za vsako vrsto je nov `LastBlock` vedenje je pričakovano, obstaja določen primer tega vedenja. 

`Ethereum`izvedbena vrstica 22 je izračunana iz `eth` primerek podan kot parameter. 

Izvedba `LastBlock`za`Bitcoin` vrstica 25 je implementirana z neupravljanim IO in ne uporablja njegovega parametra.

Torej, `Lib2` izvaja novo vedenje `LastBlock`za`Lib1` razredi.

3. Uporabite vedenje
Lestvica

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

Vrstica 30`useLastBlock` uporablja primerek `A` in `LastBlock` vedenje, definirano za ta primer.

Vrstica 33`useLastBlock` se kliče s primerkom `Ethereum` in implementacija `LastBlock` definirano v `Lib2`. Upoštevajte, da je mogoče posredovati katero koli alternativno izvedbo `LastBlock[A]` (pomisli na injekcija odvisnosti).

`useLastBlock` je vez med reprezentacijo (dejanskim A) in njegovim obnašanjem. Podatki in vedenje so ločeni, za kar se zavzema funkcionalno programiranje.

Razprava

Povzemimo pravila izrazne težave:

  • Pravilo 1: Dovolite izvajanje obstoječa vedenja  ki se nanaša na novi razredi
  • 2. pravilo: dovolite izvedbo nova vedenja ki se nanaša na obstoječi razredi
  • 3. pravilo: Ne sme ogroziti vrsta varnosti
  • Pravilo 4: Ne sme zahtevati ponovnega prevajanja obstoječo kodo

Pravilo 1 je mogoče takoj rešiti s polimorfizmom podtipa.

Pravkar predstavljeni vzorec TC (glejte prejšnji posnetek zaslona) rešuje 2. pravilo. Varen je za tip (3. pravilo) in nikoli se nismo dotaknili `Lib1` (pravilo 4). 

Vendar je njegova uporaba nepraktična iz več razlogov:

  • Vrstice 33-34 moramo eksplicitno prenesti vedenje vzdolž njegove instance. To so dodatni režijski stroški. Morali bi samo napisati `useLastBlock(Bitcoin())`.
  • V vrstici 31 sintaksa ni običajna. Raje bi napisali jedrnato in bolj objektno usmerjeno `instance.lastBlock()` izjava.

Poudarimo nekaj funkcij Scala za praktično uporabo TC. 

Izboljšana izkušnja razvijalca

Scala ima edinstven nabor funkcij in sintaktičnih sladkorjev, zaradi katerih je TC resnično prijetna izkušnja za razvijalce.

Implicitno

Implicitni obseg je poseben obseg, razrešen v času prevajanja, kjer lahko obstaja samo en primerek danega tipa. 

Program postavi primerek v implicitni obseg z `given` ključna beseda. Druga možnost je, da lahko program pridobi primerek iz implicitnega obsega s ključno besedo `using`.

Implicitni obseg je razrešen v času prevajanja, obstaja način, kako ga dinamično spremeniti med izvajanjem. Če se program prevede, je implicitni obseg razrešen. Med izvajanjem ni mogoče imeti manjkajočih implicitnih primerkov, kjer so uporabljeni. Edina možna zmeda lahko izvira iz uporabe napačnega implicitnega primerka, vendar je to vprašanje prepuščeno bitju med stolom in tipkovnico.

Od globalnega obsega se razlikuje, ker: 

  1. Rešuje se kontekstualno. Dve lokaciji programa lahko uporabljata primerek iste dane vrste v implicitnem obsegu, vendar sta ta dva primerka lahko različna.
  2. V zakulisju koda posreduje funkciji implicitnih argumentov, da deluje, dokler ni dosežena implicitna uporaba. Ne uporablja globalnega pomnilniškega prostora.

Če se vrnem k tipskemu razredu! Vzemimo popolnoma enak primer.

Lestvica

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` je ista nespremenjena koda, ki smo jo predhodno definirali. 

Lestvica

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

Vrstica 19 novo vedenje `LastBlock` je definiran, natanko tako kot prej.

Vrstica 22 in vrstica 25, `val` se nadomesti z `given`. Obe izvedbi `LastBlock` so postavljeni v implicitni obseg.

Vrstica 31`useLastBlock` deklarira vedenje `LastBlock` kot implicitni parameter. Prevajalnik razreši ustrezen primerek `LastBlock` iz implicitnega obsega, kontekstualiziranega iz lokacij klicatelja (vrstici 33 in 34). Vrstica 28 uvozi vse iz `Lib2`, vključno z implicitnim obsegom. Torej prevajalnik posreduje instanci, definirani vrstici 22 in 25, kot zadnji parameter `useLastBlock`. 

Kot uporabnik knjižnice je uporaba razreda tipa lažja kot prej. Vrstici 34 in 35 mora razvijalec samo zagotoviti, da je primerek vedenja vstavljen v implicitni obseg (in to je lahko zgolj `import`). Če implicitno ni `given` kjer je koda `using` to, mu pove prevajalnik.

Scala implicitno olajša nalogo posredovanja primerkov razreda skupaj s primerki njihovega vedenja.

Implicitni sladkorji

Vrstici 22 in 25 prejšnje kode je mogoče še izboljšati! Ponovimo implementacije TC.

Lestvica

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

Vrstici 22 in 25, če ime primerka ni uporabljeno, ga lahko izpustite.

Lestvica


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

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

Vrstici 22 in 25 lahko ponovitev tipa zamenjamo z `with` ključna beseda.

Lestvica

given LastBlock[Ethereum] = _.lastBlock

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

Ker uporabljamo degenerirano lastnost z eno samo funkcijo v njej, lahko IDE predlaga poenostavitev kode z izrazom SAM. Čeprav je pravilno, mislim, da to ni pravilna uporaba SAM, razen če ležerno kodirate golf.

Scala ponuja sintaktične sladkorje za racionalizacijo sintakse, odstranjevanje nepotrebnega poimenovanja, deklaracije in odvečnosti tipov.

Podaljšanje

Če ga pametno uporabimo, `extension` mehanizem lahko poenostavi sintakso za uporabo razreda tipa.

Lestvica

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)

Vrstice 28-29 generična metoda razširitve `lastBlock` je definiran za vse `A`z `LastBlock` Parameter TC v implicitnem obsegu.

Vrstice 33-34 razširitev uporablja objektno usmerjeno sintakso za uporabo TC.

Lestvica

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)

V vrstici 28 lahko parameter TC definirate tudi za celotno razširitev, da se izognete ponavljanju. V vrstici 30 ponovno uporabimo TC v razširitvi za definiranje `penultimateBlock` (čeprav bi ga lahko implementirali na `LastBlock` lastnost neposredno)

Čarovnija se zgodi, ko se uporabi TC. Izraz se zdi veliko bolj naraven, saj daje iluzijo, da je vedenje `lastBlock` je povezan s primerkom.

Generični tip s TC
Lestvica

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

V vrstici 34 funkcija uporablja implicitni TC. Upoštevajte, da TC ni treba poimenovati, če je to ime nepotrebno.

Vzorec TC se tako pogosto uporablja, da obstaja generična sintaksa tipa za izražanje "tipa z implicitnim vedenjem". Sintaksa vrstice 36 je bolj jedrnata alternativa prejšnji (vrstica 34). Izogne ​​se izrecni deklaraciji neimenovanega implicitnega parametra TC.

S tem se zaključi razdelek o izkušnjah razvijalcev. Videli smo, kako lahko razširitve, implicitno in nekaj skladenjskega sladkorja zagotovijo manj natrpano sintakso, ko se uporablja in definira TC.

Samodejna izpeljava

Veliko knjižnic Scala uporablja TC, programerju pa prepušča, da jih implementira v svojo bazo kode.

Circe (knjižnica za deserializacijo json) na primer uporablja TC `Encoder[T]` in `Decoder[T]` za programerje, da jih implementirajo v svojo kodno zbirko. Ko je implementirana, je mogoče uporabiti celoten obseg knjižnice. 

Te izvedbe TC so več kot pogoste podatkovno usmerjeni preslikavci. Ne potrebujejo nobene poslovne logike, dolgočasni so za pisanje in jih je težko vzdrževati v sinhronizaciji s primeri.

V takšnih razmerah te knjižnice ponujajo tako imenovano avtomatski izpeljava oz polavtomatski izpeljava. Glej na primer Circe avtomatski in polavtomatski izpeljava. S polavtomatsko izpeljavo lahko programer deklarira primerek razreda tipa z nekaj manjše sintakse, medtem ko samodejna izpeljava ne zahteva nobene spremembe kode, razen uvoza.

Pod pokrovom, v času prevajanja, introspektiva generičnih makrov Vrste kot čisto podatkovno strukturo in ustvarite TC[T] za uporabnike knjižnice. 

Izpeljava generičnih TC je zelo pogosta, zato je Scala za ta namen uvedla popolno orodje. Knjižnična dokumentacija te metode ne oglašuje vedno, čeprav je to način uporabe izpeljave v Scali 3.

Lestvica

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)

Vrstica 18 nov TC `Named` je uveden. Ta TC strogo gledano ni povezan s poslom blockchain. Njegov namen je poimenovati verigo blokov na podlagi imena razreda primerov.

Najprej se osredotočite na vrstice definicij 36-38. Obstajata 2 sintaksi za izpeljavo TC:

  1. V vrstici 36 lahko primerek TC definirate neposredno v razredu primerov z `derives` ključna beseda. Pod pokrovom prevajalnik ustvari dani `Named` primerek v `Polkadot` spremljevalni predmet.
  2. Vrstici 37 in 38 so primerki razredov tipov podani na že obstoječih razredih z `TC.derived

V vrstici 31 je definirana generična razširitev (glejte prejšnje razdelke) in `blockchainName` se uporablja naravno.  

`derives` ključna beseda pričakuje metodo z obliko `inline def derived[T](using Mirror.Of[T]): TC[T] = ???`, ki je definirana v vrstici 24. Ne bom podrobno razlagal, kaj počne koda. V širokih obrisih:

  • `inline def` definira makro
  • `Mirror` je del orodjarne za samopregled tipov. Obstajajo različne vrste ogledal in vrstica 26 se kode osredotoča na `Product` ogledala (razred primera je izdelek). Vrstica 27, če programerji poskušajo izpeljati nekaj, kar ni `Product`, koda ne bo prevedena.
  • `Mirror` vsebuje druge vrste. Eden od njih, `MirrorLabel`, je niz, ki vsebuje ime tipa. Ta vrednost se uporablja v izvedbi, vrstica 29, `Named` TC.

Avtorji TC lahko uporabljajo metaprogramiranje za zagotavljanje funkcij, ki generično generirajo primerke TC glede na vrsto. Programerji lahko uporabijo API za namensko knjižnico ali orodja za izpeljavo Scala za ustvarjanje primerkov za svojo kodo.

Ne glede na to, ali potrebujete splošno ali specifično kodo za implementacijo TC, obstaja rešitev za vsako situacijo. 

Povzetek vseh prednosti

  • Rešuje problem izražanja
    • Novi tipi lahko izvajajo obstoječe vedenje s tradicionalnim dedovanjem lastnosti
    • Nova vedenja je mogoče implementirati na obstoječe vrste
  • Ločitev skrbi
    • Koda ni pokvarjena in jo je enostavno izbrisati. TC ločuje podatke in vedenje, kar je moto funkcionalnega programiranja.
  • Varno je
    • Je tipsko varen, ker se ne zanaša na introspekcijo. Izogne ​​se velikemu ujemanju vzorcev, ki vključuje tipe. če naletite na pisanje takšne kode, lahko zaznate primer, ko bo vzorec TC popolnoma ustrezal.
    • Implicitni mehanizem je varen za prevajanje! Če v času prevajanja primerek manjka, se koda ne bo prevedla. Med izvajanjem ni presenečenja.
  • Prinaša ad-hoc polimorfizem
    • Ad hoc polimorfizem običajno manjka v tradicionalnem objektno usmerjenem programiranju.
    • Z ad-hoc polimorfizmom lahko razvijalci izvajajo enako vedenje za različne nepovezane tipe brez uporabe tradicionalnega podtipkanja (ki združi kodo)
  • Injekcija odvisnosti je preprosta
    • Primerek TC je mogoče spremeniti glede na načelo zamenjave Liskova. 
    • Ko je komponenta odvisna od TC, je mogoče preprosto vnesti lažni TC za namene testiranja. 

Protiindikacije

Vsako kladivo je zasnovano za vrsto težav.

Tipski razredi so namenjeni vedenjskim težavam in se ne smejo uporabljati za dedovanje podatkov. V ta namen uporabite sestavo.

Običajno podtipiranje je bolj preprosto. Če imate osnovo kode in ne ciljate na razširljivost, so tipski razredi morda pretirani.

Na primer, v jedru Scala je `Numeric` vrsta razreda:

Lestvica

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

Resnično je smiselno uporabiti takšen tipski razred, ker ne omogoča samo ponovne uporabe algebrskih algoritmov na tipih, ki so vdelani v Scala (Int, BigInt, …), ampak tudi na uporabniško definiranih tipih (a `ComplexNumber` na primer).

Po drugi strani implementacija zbirk Scala večinoma uporablja podtip namesto razreda tipa. Ta oblika je smiselna iz več razlogov:

  • Zbirni API naj bi bil popoln in stabilen. Izpostavlja običajno vedenje skozi lastnosti, podedovane z implementacijami. Biti zelo razširljiv tukaj ni poseben cilj.
  • Biti mora preprost za uporabo. TC programerju končnega uporabnika doda miselne stroške.
  • TC lahko povzroči tudi manjše stroške delovanja. To je lahko kritično za API zbirke.
  • Kljub temu je API za zbiranje še vedno razširljiv prek novega TC, ki ga definirajo knjižnice tretjih oseb.

zaključek

Videli smo, da je TC preprost vzorec, ki rešuje velik problem. Zahvaljujoč bogati sintaksi Scala je vzorec TC mogoče implementirati in uporabljati na več načinov. Vzorec TC je v skladu s paradigmo funkcionalnega programiranja in je čudovito orodje za čisto arhitekturo. Ni srebrne krogle in vzorec TC je treba uporabiti, ko se prilega.

Upam, da ste ob branju tega dokumenta pridobili znanje. 

Koda je na voljo na https://github.com/jprudent/type-class-article. Če imate kakršna koli vprašanja ali pripombe, se obrnite name. Če želite, lahko uporabite težave ali komentarje kode v repozitoriju.


Jeronim PRUDENT

Software Engineer

Časovni žig:

Več od Ledger