Clasele de tip în Scala3: Un ghid pentru începători | Registrul mare

Clasele de tip în Scala3: un ghid pentru începători | Registrul mare

Clasele de tip în Scala3: Un ghid pentru începători | Ledger PlatoBlockchain Data Intelligence. Căutare verticală. Ai.

Acest document este destinat dezvoltatorului Scala3 începător, care este deja versat în proza ​​Scala, dar este nedumerit de toate `implicits` și trăsături parametrizate în cod.

Acest document explică de ce, cum, unde și când Clase de tip (TC).

După ce a citit acest document, dezvoltatorul începător Scala3 va dobândi cunoștințe solide pentru a le folosi și va explora codul sursă mult din bibliotecile Scala și începeți să scrieți cod Scala idiomatic.

Să începem cu de ce...

Problema expresiei

În 1998, a declarat Philip Wadler că „expresia problemă este un nume nou pentru o problemă veche”. Este problema extensibilității software-ului. Conform scrisului domnului Wadler, soluția problemei expresiei trebuie să respecte următoarele reguli:

  • Regula 1: Permite implementarea comportamentelor existente (gândiți-vă la trăsătura Scala) pentru a fi aplicat noi reprezentări (gândiți-vă la o clasă de caz)
  • Regula 2:  Permiteți implementarea comportamente noi pentru a fi aplicat reprezentări existente
  • Regula 3: Nu trebuie să pună în pericol tip de siguranță
  • Regula 4: Nu trebuie să necesite recompilarea codul existent

Rezolvarea acestei probleme va fi firul de argint al acestui articol.

Regula 1: implementarea comportamentului existent asupra noii reprezentari

Orice limbaj orientat obiect are o soluție integrată pentru regula 1 cu polimorfism de subtip. Puteți implementa în siguranță orice `trait` definit într-o dependență de un `class` în propriul cod, fără a recompila dependența. Să vedem că în acțiune:

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

În acest exemplu fictiv, biblioteca `Lib1` (linia 5) definește o trăsătură `Blockchain` (linia 6) cu 2 implementări ale acestuia (liniile 9 și 12). `Lib1` va rămâne același în TOATE acest document (aplicarea regulii 4).

`Lib2` (linia 15) implementează comportamentul existent `Blockchain`pe o clasă nouă`Polkadot` (regula 1) într-un mod sigur de tip (regula 3), fără recompilare `Lib1` (regula 4). 

Regula 2: implementarea de noi comportamente care să fie aplicate reprezentărilor existente

Să ne imaginăm în `Lib2`ne dorim un comportament nou`lastBlock` să fie implementat în mod specific pentru fiecare `Blockchain`.

Primul lucru care îmi vine în minte este crearea unui comutator mare bazat pe tipul de parametru.

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

Această soluție este o reimplementare slabă a polimorfismului bazat pe tip, care este deja încorporat în limbaj!

`Lib1` este lăsat neatins (rețineți că a aplicat regula 4 pe tot acest document). 

Soluția implementată în `Lib2` este bine până când un alt blockchain este introdus în `Lib3`. Încalcă regula de siguranță a tipului (regula 3) deoarece acest cod nu reușește la rulare pe linia 37. Și modificarea `Lib2` ar încălca regula 4.

O altă soluție este utilizarea unui `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` este lăsat neatins (aplicarea regulii 4 în întregul document). 

`Lib2` definește comportamentul pentru tipul său (linia 21) și `extensiile` pentru tipurile existente (liniile 23 și 25).

Rândurile 28-30, noul comportament poate fi folosit în fiecare clasă. 

Dar nu există nicio modalitate de a numi acest nou comportament polimorf (linia 32). Orice încercare de a face acest lucru duce la erori de compilare (linia 33) sau la comutatoare bazate pe tip. 

Această regulă nr. 2 este complicată. Am încercat să-l implementăm cu propria noastră definiție a polimorfismului și trucul „extensie”. Și asta a fost ciudat.

Există o piesă lipsă numită polimorfism ad-hoc: capacitatea de a trimite în siguranță o implementare a comportamentului în funcție de un tip, oriunde sunt definite comportamentul și tipul. Introduceți Clasa de tip model.

Modelul clasei de tip

Rețeta de model Clasa de tip (TC pe scurt) are 3 pași. 

  1. Definiți un nou comportament
  2. Implementați comportamentul
  3. Folosește comportamentul

În secțiunea următoare, implementez modelul TC în cel mai simplu mod. Este verboroasă, neplăcută și nepractică. Dar stați, aceste avertismente vor fi fixate pas cu pas mai departe în document.

1. Definiți un nou comportament
Scala

object Lib2:
 import Lib1.*

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

`Lib1` este, din nou, lăsat neatins.

Noul comportament is TC materializat prin trăsătură. Funcțiile definite în trăsătură sunt o modalitate de a aplica unele aspecte ale acelui comportament.

Parametrul `A` reprezintă tipul căruia vrem să-i aplicăm comportamentul, care sunt subtipuri de `Blockchain` în cazul nostru.

Cateva observatii:

  • Dacă este necesar, tipul parametrizat `A` poate fi constrâns în continuare de sistemul de tip Scala. De exemplu, am putea aplica `A` a fi un `Blockchain`. 
  • De asemenea, TC ar putea avea mult mai multe funcții declarate în el.
  • În cele din urmă, fiecare funcție poate avea mult mai mulți parametri arbitrari.

Dar să păstrăm lucrurile simple de dragul lizibilității.

2. Implementați comportamentul
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")

Pentru fiecare tip noul `LastBlock` comportamentul este de așteptat, există o instanță specifică a acelui comportament. 

Ethereum` linia de implementare 22 este calculată din `eth` instanță a trecut ca parametru. 

Implementarea `LastBlock`pentru`Bitcoin` linia 25 este implementată cu un IO negestionat și nu își folosește parametrul.

Deci, `Lib2` implementează un comportament nou `LastBlock`pentru`Lib1` clase.

3. Utilizați comportamentul
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))

Linia 30`useLastBlock` folosește o instanță de `A` și `LastBlock` comportament definit pentru acel caz.

Linia 33`useLastBlock` este apelat cu o instanță de `Ethereum` și o implementare a `LastBlock` definit în `Lib2`. Rețineți că este posibil să treceți orice implementare alternativă a lui `LastBlock[A]` (gândiți-vă la injecție de dependență).

`useLastBlock` este lipiciul dintre reprezentare (A real) și comportamentul său. Datele și comportamentul sunt separate, ceea ce susține programarea funcțională.

Discuție

Să recapitulăm regulile problemei expresiei:

  • Regula 1: Permite implementarea comportamentelor existente  pentru a fi aplicat clase noi
  • Regula 2:  Permiteți implementarea comportamente noi pentru a fi aplicat clasele existente
  • Regula 3: Nu trebuie să pună în pericol tip de siguranță
  • Regula 4: Nu trebuie să necesite recompilarea codul existent

Regula 1 poate fi rezolvată imediat cu polimorfismul de subtip.

Modelul TC tocmai prezentat (vezi captura de ecran anterioară) rezolvă regula 2. Este de tip safe (regula 3) și nu am atins niciodată `Lib1` (regula 4). 

Cu toate acestea, este imposibil de utilizat din mai multe motive:

  • Liniile 33-34 trebuie să trecem explicit comportamentul de-a lungul instanței sale. Aceasta este o taxă suplimentară. Ar trebui doar să scriem `useLastBlock(Bitcoin())`.
  • Linia 31, sintaxa este neobișnuită. Am prefera mai degrabă să scriem un text concis și mai orientat pe obiecte  `instance.lastBlock()` declarație.

Să evidențiem câteva caracteristici Scala pentru utilizarea practică a TC. 

Experiență îmbunătățită a dezvoltatorului

Scala are un set unic de caracteristici și zaharuri sintactice care fac din TC o experiență cu adevărat plăcută pentru dezvoltatori.

Implicite

Sfera implicită este un domeniu special rezolvat în timpul compilării, unde poate exista o singură instanță a unui anumit tip. 

Un program pune o instanță în domeniul implicit cu `given` cuvânt cheie. Alternativ, un program poate prelua o instanță din domeniul implicit cu cuvântul cheie `using`.

Sfera implicită este rezolvată în timpul compilării, există o modalitate cunoscută de a-l schimba dinamic în timpul execuției. Dacă programul se compilează, domeniul implicit este rezolvat. În timpul execuției, nu este posibil să lipsească instanțe implicite în care sunt utilizate. Singura confuzie posibilă poate veni din utilizarea unei instanțe implicite greșite, dar această problemă este lăsată pentru creatura dintre scaun și tastatură.

Este diferit de un domeniu global deoarece: 

  1. Se rezolvă contextual. Două locații ale unui program pot folosi o instanță de același tip dat în domeniul implicit, dar acele două instanțe pot fi diferite.
  2. În spatele scenei, codul trece funcția argumentelor implicite pentru a funcționa până când se ajunge la utilizarea implicită. Nu folosește un spațiu de memorie global.

Revenind la clasa de tip! Să luăm exact același exemplu.

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` este același cod nemodificat pe care l-am definit anterior. 

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

Linia 19 un comportament nou `LastBlock` este definit, exact așa cum am făcut anterior.

Linia 22 și linia 25, `val` este înlocuit cu `given`. Ambele implementări ale lui `LastBlock` sunt puse în domeniul implicit.

Linia 31`useLastBlock`declară comportamentul`LastBlock` ca parametru implicit. Compilatorul rezolvă instanța corespunzătoare a lui `LastBlock` din domeniul implicit contextualizat din locațiile apelantului (liniile 33 și 34). Linia 28 importă totul de la `Lib2`, inclusiv domeniul de aplicare implicit. Deci, compilatorul trece instanțele definite liniile 22 și 25 ca ultimul parametru al lui `useLastBlock`. 

Ca utilizator de bibliotecă, utilizarea unei clase de tip este mai ușoară decât înainte. Linia 34 și 35 un dezvoltator trebuie doar să se asigure că o instanță a comportamentului este injectată în domeniul implicit (și acesta poate fi un simplu `import`). Dacă un implicit nu este `given` unde este codul `using` it, îi spune compilatorul.

Scala ușurează implicit sarcina de a trece instanțe de clasă împreună cu instanțe ale comportamentelor lor.

Zaharuri implicite

Linia 22 și 25 din codul anterior pot fi îmbunătățite în continuare! Să repetăm ​​implementările TC.

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

Rândurile 22 și 25, dacă numele instanței nu este folosit, acesta poate fi omis.

Scala


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

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

Liniile 22 și 25, repetarea tipului poate fi înlocuită cu `with` cuvânt cheie.

Scala

given LastBlock[Ethereum] = _.lastBlock

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

Deoarece folosim o trăsătură degenerată cu o singură funcție în ea, IDE-ul poate sugera simplificarea codului cu o expresie SAM. Deși corect, nu cred că este o utilizare adecvată a SAM, cu excepția cazului în care codificați casual.

Scala oferă zaharuri sintactice pentru a simplifica sintaxa, eliminând redundanța de denumire, declarație și tip inutile.

Extensie

Folosit cu înțelepciune, `extension` mecanism poate simplifica sintaxa pentru utilizarea unei clase de tip.

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)

Liniile 28-29 o metodă de extensie generică `lastBlock` este definit pentru orice `A` cu un `LastBlock` Parametru TC în domeniul implicit.

Liniile 33-34 extensia folosește o sintaxă orientată pe obiect pentru a utiliza TC.

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)

Linia 28, parametrul TC poate fi definit și pentru întreaga extensie pentru a evita repetarea. Linia 30 reutilizam TC în extensie pentru a defini `penultimateBlock` (chiar dacă ar putea fi implementat pe `LastBlock` trăsătură direct)

Magia se întâmplă atunci când se folosește TC. Expresia se simte mult mai naturală, dând iluzia că comportamentul `lastBlock` este confundat cu instanța.

Tip generic cu TC
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))

Linia 34 funcția folosește un TC implicit. Rețineți că TC nu trebuie să fie numit dacă numele respectiv nu este necesar.

Modelul TC este atât de utilizat pe scară largă încât există o sintaxă de tip generic pentru a exprima „un tip cu un comportament implicit”. Linia 36 sintaxa este o alternativă mai concisă față de cea anterioară (linia 34). Evită declararea specifică a parametrului TC implicit nenumit.

Aceasta se încheie secțiunea privind experiența dezvoltatorului. Am văzut cum extensiile, implicitele și ceva zahăr sintactic pot oferi o sintaxă mai puțin aglomerată atunci când TC este folosit și definit.

Derivare automată

Multe biblioteci Scala folosesc TC, lăsând programatorul să le implementeze în baza lor de cod.

De exemplu, Circe (o bibliotecă de deserializare json) folosește TC `Encoder[T]` și `Decoder[T]` pentru ca programatorii să le implementeze în baza lor de cod. Odată implementată, întregul domeniu al bibliotecii poate fi utilizat. 

Acele implementări ale TC sunt mai des cartografii orientați pe date. Nu au nevoie de nicio logică de afaceri, sunt plictisitoare de scris și o povară de menținut sincronizate cu clasele de cazuri.

Într-o astfel de situație, acele biblioteci oferă ceea ce se numește automat derivare sau semiautomat derivare. Vezi, de exemplu, Circe automat și semiautomat derivare. Cu derivarea semi-automată, programatorul poate declara o instanță a unei clase de tip cu o sintaxă minoră, în timp ce derivarea automată nu necesită nicio modificare a codului, cu excepția unui import.

Sub capotă, în timpul compilării, macro-uri generice introspect Tipuri ca structură de date pură și generează un TC[T] pentru utilizatorii bibliotecii. 

Derivarea generică a unui TC este foarte comună, așa că Scala a introdus o cutie completă de instrumente în acest scop. Această metodă nu este întotdeauna promovată de documentațiile bibliotecii, deși este modul Scala 3 de a folosi derivarea.

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)

Linia 18 un nou TC `Named` este introdus. Acest TC nu are legătură cu afacerea blockchain, strict vorbind. Scopul său este de a denumi blockchain-ul pe baza numelui clasei de caz.

În primul rând, concentrați-vă pe liniile de definiții 36-38. Există 2 sintaxe pentru derivarea unui TC:

  1. Linia 36 instanța TC poate fi definită direct pe clasa de caz cu `derives` cuvânt cheie. Sub capotă, compilatorul generează un ` datNamed` exemplu în `Polkadot` obiect însoțitor.
  2. Linia 37 și 38, instanțele claselor de tip sunt date pe clase preexistente cu `TC.derived

Linia 31 este definită o extensie generică (vezi secțiunile anterioare) și `blockchainName` este folosit în mod natural.  

derives` cuvântul cheie așteaptă o metodă cu forma `inline def derived[T](using Mirror.Of[T]): TC[T] = ???` care este definită linia 24. Nu voi explica în profunzime ce face codul. În linii mari:

  • `inline def` definește o macrocomandă
  • `Mirror` face parte din setul de instrumente pentru introspectarea tipurilor. Există diferite tipuri de oglinzi, iar linia 26 codul se concentrează pe `Product` oglinzi (o clasă de caz este un produs). Linia 27, dacă programatorii încearcă să obțină ceva care nu este un `Product`, codul nu se va compila.
  • `Mirror` conține alte tipuri. Unul dintre ei, `MirrorLabel`, este un șir care conține numele tipului. Această valoare este utilizată în implementarea, rândul 29, a `Named`TC.

Autorii TC pot folosi metaprogramarea pentru a furniza funcții care generează în mod generic instanțe de TC dat un tip. Programatorii pot folosi API-ul de bibliotecă dedicată sau instrumentele derivate Scala pentru a crea instanțe pentru codul lor.

Indiferent dacă aveți nevoie de cod generic sau specific pentru a implementa un TC, există o soluție pentru fiecare situație. 

Rezumatul tuturor beneficiilor

  • Rezolvă problema expresiei
    • Noile tipuri pot implementa comportamentul existent prin moștenirea tradițională a trăsăturilor
    • Comportamente noi pot fi implementate pe tipurile existente
  • Separarea preocupărilor
    • Codul nu este stricat și ușor de șters. Un TC separă datele și comportamentul, care este un motto de programare funcțională.
  • Este sigur
    • Este sigur pentru că nu se bazează pe introspecție. Evită potrivirea mare a modelelor care implică tipuri. dacă vă întâlniți cu dvs. scriind un astfel de cod, este posibil să detectați un caz în care modelul TC se va potrivi perfect.
    • Mecanismul implicit este compilarea în siguranță! Dacă o instanță lipsește în momentul compilării, codul nu se va compila. Nicio surpriză în timpul rulării.
  • Aduce polimorfism ad-hoc
    • Polimorfismul ad-hoc lipsește de obicei în programarea tradițională orientată pe obiecte.
    • Cu polimorfismul ad-hoc, dezvoltatorii pot implementa același comportament pentru diferite tipuri neînrudite, fără a utiliza subtasarea tradițională (care cuplează codul)
  • Injecția dependenței este ușoară
    • O instanță TC poate fi schimbată în conformitate cu principiul substituției Liskov. 
    • Când o componentă are o dependență de un TC, un TC batjocorit poate fi injectat cu ușurință în scopuri de testare. 

Contraindicații

Fiecare ciocan este proiectat pentru o serie de probleme.

Clasele de tip sunt pentru probleme de comportament și nu trebuie utilizate pentru moștenirea datelor. Utilizați compoziția în acest scop.

Subtiparea obișnuită este mai simplă. Dacă dețineți baza de cod și nu urmăriți extensibilitate, clasele de tip pot fi exagerate.

De exemplu, în nucleul Scala, există un `Numeric` clasa de tip:

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

Este într-adevăr logic să folosiți o astfel de clasă de tip, deoarece nu numai că permite reutilizarea algoritmilor algebrici pe tipuri care sunt încorporate în Scala (Int, BigInt, ...), ci și pe tipuri definite de utilizator (un `ComplexNumber` de exemplu).

Pe de altă parte, implementarea colecțiilor Scala utilizează în mare parte subtiparea în loc de clasa de tip. Acest design are sens din mai multe motive:

  • API-ul de colectare ar trebui să fie complet și stabil. Expune comportamentul comun prin trăsături moștenite de implementări. A fi foarte extensibil nu este un obiectiv special aici.
  • Trebuie să fie simplu de utilizat. TC adaugă o suprasarcină mentală asupra programatorului utilizatorului final.
  • TC poate suporta, de asemenea, o mică suprasarcină în performanță. Acest lucru poate fi critic pentru un API de colectare.
  • Cu toate acestea, API-ul de colecție este încă extensibil prin noul TC definit de biblioteci terțe.

Concluzie

Am văzut că TC este un model simplu care rezolvă o mare problemă. Datorită sintaxei bogate Scala, modelul TC poate fi implementat și utilizat în multe moduri. Modelul TC este în conformitate cu paradigma de programare funcțională și este un instrument fabulos pentru o arhitectură curată. Nu există niciun glonț de argint și modelul TC trebuie aplicat atunci când se potrivește.

Sper că ați dobândit cunoștințe citind acest document. 

Codul este disponibil la https://github.com/jprudent/type-class-article. Vă rog să mă contactați dacă aveți orice fel de întrebări sau observații. Puteți utiliza problemele sau comentariile de cod în depozit dacă doriți.


Jerome PRUDENT

Inginer Software

Timestamp-ul:

Mai mult de la carte mare