Type klasser i Scala3: En begyndervejledning | Hovedbog

Type klasser i Scala3: En begyndervejledning | Hovedbog

Type klasser i Scala3: En begyndervejledning | Ledger PlatoBlockchain Data Intelligence. Lodret søgning. Ai.

Dette dokument er beregnet til nybegynderen Scala3-udvikler, som allerede er fortrolig med Scala-prosa, men er forundret over alleimplicits` og parameteriserede træk i koden.

Dette dokument forklarer hvorfor, hvordan, hvor og hvornår Typeklasser (TC).

Efter at have læst dette dokument vil nybegynderen Scala3-udvikler få solid viden at bruge og dykke ned i kildekoden til en masse af Scala-biblioteker og begynde at skrive idiomatisk Scala-kode.

Lad os starte med hvorfor...

Udtryksproblemet

I 1998, blev sagde Philip Wadler at "udtrykket problem er et nyt navn for et gammelt problem". Det er problemet med softwareudvidelsesmuligheder. Ifølge hr. Wadlers skrift skal løsningen af ​​udtryksproblemet overholde følgende regler:

  • Regel 1: Tillad implementering af eksisterende adfærd (tænk på Scala-træk), der skal anvendes på nye repræsentationer (tænk på en sagsklasse)
  • Regel 2: Tillad implementering af ny adfærd der skal anvendes til eksisterende repræsentationer
  • Regel 3: Det må ikke bringe den i fare type sikkerhed
  • Regel 4: Det må ikke være nødvendigt at rekompilere eksisterende kode

At løse dette problem vil være sølvtråden i denne artikel.

Regel 1: implementering af eksisterende adfærd på ny repræsentation

Ethvert objektorienteret sprog har en indbygget løsning til regel 1 med subtype polymorfi. Du kan sikkert implementere enhver `trait` defineret i en afhængighed af en `class` i din egen kode uden at genkompilere afhængigheden. Lad os se det i aktion:

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

I dette fiktive eksempel, bibliotek `Lib1` (linje 5) definerer et træk `Blockchain` (linje 6) med 2 implementeringer af det (linje 9 & 12). `Lib1` vil forblive den samme i ALT dette dokument (håndhævelse af regel 4).

`Lib2` (linje 15) implementerer den eksisterende adfærd `Blockchain`på en ny klasse`Polkadot` (regel 1) på en typesikker (regel 3) måde, uden at rekompilere `Lib1` (regel 4). 

Regel 2: implementering af ny adfærd, der skal anvendes på eksisterende repræsentationer

Lad os forestille os i `Lib2`vi vil have en ny adfærd`lastBlock` skal implementeres specifikt for hver `Blockchain`.

Den første ting, der kommer til at tænke på, er at skabe en stor switch baseret på typen af ​​parameter.

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

Denne løsning er en svag genimplementering af typebaseret polymorfi, der allerede er indbygget sprog!

`Lib1` forbliver urørt (husk, håndhævet regel 4 i hele dette dokument). 

Løsningen implementeret i `Lib2` er okay indtil endnu en blockchain introduceres i `Lib3`. Det overtræder typesikkerhedsreglen (regel 3), fordi denne kode fejler ved kørsel på linje 37. Og ændrer `Lib2" ville overtræde regel 4.

En anden løsning er at bruge en `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` efterlades urørt (håndhævelse af regel 4 i hele dokumentet). 

`Lib2` definerer adfærd for sin type (linje 21) og `udvidelse` for eksisterende typer (linje 23 & 25).

Linje 28-30, den nye adfærd kan bruges i hver klasse. 

Men der er ingen måde at kalde denne nye adfærd polymorf (linje 32). Ethvert forsøg på at gøre det fører til kompileringsfejl (linje 33) eller til typebaserede switches. 

Denne regel nr. 2 er vanskelig. Vi forsøgte at implementere det med vores egen definition af polymorfi og "udvidelses"-trick. Og det var mærkeligt.

Der er en manglende brik kaldet ad hoc polymorfi: evnen til sikkert at sende en adfærdsimplementering i henhold til en type, uanset hvor adfærden og typen er defineret. Gå ind i Type Klasse mønster.

Type Klasse-mønsteret

Typeklassen (TC for kort) mønsteropskrift har 3 trin. 

  1. Definer en ny adfærd
  2. Implementer adfærden
  3. Brug adfærden

I det følgende afsnit implementerer jeg TC-mønsteret på den mest ligetil måde. Det er ordrigt, klodset og upraktisk. Men hold fast, de forbehold vil blive rettet trin for trin længere i dokumentet.

1. Definer en ny adfærd
Scala

object Lib2:
 import Lib1.*

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

`Lib1` er endnu en gang urørt.

Den nye adfærd is TC materialiseret af egenskaben. Funktionerne defineret i egenskaben er en måde at anvende nogle aspekter af denne adfærd på.

Parameteren `A` repræsenterer den type, vi ønsker at anvende adfærd på, som er undertyper af `Blockchain` i vores tilfælde.

Nogle bemærkninger:

  • Om nødvendigt, den parametrerede type `A` kan yderligere begrænses af Scala-typen. For eksempel kunne vi håndhæve `A'at være en'Blockchain`. 
  • TC kunne også have mange flere funktioner erklæret i sig.
  • Endelig kan hver funktion have mange flere vilkårlige parametre.

Men lad os holde tingene enkle for læselighedens skyld.

2. Implementer adfærden
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")

For hver type den nye `LastBlock` adfærd forventes, er der et specifikt tilfælde af den adfærd. 

Den `Ethereum` implementeringslinje 22 er beregnet ud fra `eth` instans sendt som parameter. 

Gennemførelsen af ​​`LastBlock` for `Bitcoin` Linje 25 er implementeret med en ikke-administreret IO og bruger ikke dens parameter.

Så, `Lib2` implementerer ny adfærd `LastBlock` for `Lib1` klasser.

3. Brug adfærden
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))

Linje 30 `useLastBlock` bruger en forekomst af `A` og `LastBlock` adfærd defineret for dette tilfælde.

Linje 33 `useLastBlock` kaldes med en instans af `Ethereum` og en implementering af `LastBlock` defineret i `Lib2`. Bemærk, at det er muligt at godkende enhver alternativ implementering af `LastBlock[A]` (tænk på afhængighedsindsprøjtning).

`useLastBlock` er limen mellem repræsentation (det faktiske A) og dets adfærd. Data og adfærd er adskilt, hvilket er hvad funktionel programmering går ind for.

Diskussion

Lad os opsummere reglerne for udtryksproblemet:

  • Regel 1: Tillad implementering af eksisterende adfærd  der skal anvendes til nye klasser
  • Regel 2: Tillad implementering af ny adfærd der skal anvendes til eksisterende klasser
  • Regel 3: Det må ikke bringe den i fare type sikkerhed
  • Regel 4: Det må ikke være nødvendigt at rekompilere eksisterende kode

Regel 1 kan løses ud af boksen med subtype polymorfi.

Det netop præsenterede TC-mønster (se forrige skærmbillede) løser regel 2. Det er typesikkert (regel 3), og vi rørte aldrig `Lib1` (regel 4). 

Det er dog upraktisk at bruge af flere grunde:

  • Linje 33-34 er vi nødt til eksplicit at videregive adfærden langs dens instans. Dette er en ekstra overhead. Vi skulle bare skrive `useLastBlock(Bitcoin())`.
  • Linje 31 er syntaksen ualmindelig. Vi vil hellere foretrække at skrive en kortfattet og mere objektorienteret `instance.lastBlock()` erklæring.

Lad os fremhæve nogle Scala-funktioner til praktisk TC-brug. 

Forbedret udvikleroplevelse

Scala har et unikt sæt funktioner og syntaktiske sukkerarter, der gør TC til en virkelig fornøjelig oplevelse for udviklere.

Implicitte

Det implicitte omfang er et særligt omfang, der løses på kompileringstidspunktet, hvor kun én instans af en given type kan eksistere. 

Et program sætter en instans i det implicitte omfang med `given` søgeord. Alternativt kan et program hente en instans fra det implicitte omfang med nøgleordet `using`.

Det implicitte omfang er løst på kompileringstidspunktet, der er kendt måde at ændre det dynamisk på under kørsel. Hvis programmet kompilerer, er det implicitte omfang løst. Under kørsel er det ikke muligt at have manglende implicitte tilfælde, hvor de bruges. Den eneste mulige forvirring kan komme af at bruge den forkerte implicitte instans, men dette problem er overladt til væsenet mellem stolen og tastaturet.

Det er forskelligt fra et globalt omfang, fordi: 

  1. Det er løst kontekstuelt. To placeringer af et program kan bruge en instans af samme givne type i implicit omfang, men disse to instanser kan være forskellige.
  2. Bag scenen sender koden implicitte argumenter til at fungere, indtil den implicitte brug er nået. Det bruger ikke en global hukommelsesplads.

Tilbage til typeklassen! Lad os tage nøjagtig det samme eksempel.

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` er den samme umodificerede kode, som vi tidligere har defineret. 

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

Linje 19 en ny adfærd `LastBlock` er defineret, præcis som vi gjorde tidligere.

Linje 22 og linje 25, `val` erstattes af `given`. Begge implementeringer af `LastBlock` sættes i det implicitte omfang.

Linje 31 `useLastBlock` erklærer adfærden `LastBlock` som en implicit parameter. Compileren løser den relevante forekomst af `LastBlock` fra implicit omfang kontekstualiseret fra opkaldssteder (linje 33 og 34). Linje 28 importerer alt fra `Lib2`, herunder det implicitte omfang. Så compileren sender instanser defineret linje 22 og 25 som den sidste parameter af `useLastBlock`. 

Som biblioteksbruger er det nemmere at bruge en typeklasse end før. Linje 34 og 35 skal en udvikler kun sikre sig, at en instans af adfærden er injiceret i det implicitte omfang (og dette kan være en ren `import`). Hvis en implicit ikke er `given` hvor koden er `using` det, fortæller compileren ham.

Scalas implicitte letter opgaven med at videregive klasseforekomster sammen med forekomster af deres adfærd.

Implicitte sukkerarter

Linje 22 og 25 i tidligere kode kan forbedres yderligere! Lad os gentage TC-implementeringerne.

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

Linje 22 og 25, hvis navnet på instansen er ubrugt, kan det udelades.

Scala


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

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

Linje 22 og 25 kan gentagelsen af ​​typen erstattes med `with` søgeord.

Scala

given LastBlock[Ethereum] = _.lastBlock

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

Fordi vi bruger et degenereret træk med en enkelt funktion i det, kan IDE foreslå at forenkle koden med et SAM-udtryk. Selvom det er korrekt, tror jeg ikke, at det er en ordentlig brug af SAM, medmindre du er tilfældigt kodegolf.

Scala tilbyder syntaktiske sukkerarter for at strømline syntaksen og fjerne unødvendig navngivning, erklæring og typeredundans.

Extension

Brugt klogt, den `extension` mekanisme kan forenkle syntaksen for at bruge en type klasse.

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)

Linje 28-29 en generisk udvidelsesmetode `lastBlock` er defineret for enhver `A` med et `LastBlock` TC-parameter i implicit omfang.

Linje 33-34 udvidelsen udnytter en objektorienteret syntaks til at bruge 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)

Linje 28, TC-parameteren kan også defineres for hele forlængelsen for at undgå gentagelser. Linje 30 genbruger vi TC i udvidelsen til at definere `penultimateBlock` (selvom det kunne implementeres på `LastBlock"egenskab direkte)

Magien sker, når TC'en bruges. Udtrykket føles meget mere naturligt, hvilket giver den illusion, at adfærd `lastBlock` er sammenblandet med instansen.

Generisk type med 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))

Linje 34 bruger funktionen en implicit TC. Bemærk, at TC'en ikke behøver at blive navngivet, hvis dette navn er unødvendigt.

TC-mønsteret er så udbredt, at der er en generisk typesyntaks til at udtrykke "en type med en implicit adfærd". Linje 36 er syntaksen et mere kortfattet alternativ til den forrige (linje 34). Den undgår specifikt at angive den unavngivne implicitte TC-parameter.

Dette afslutter afsnittet om udvikleroplevelse. Vi har set, hvordan udvidelser, implicitte og noget syntaktisk sukker kan give en mindre rodet syntaks, når TC bruges og defineres.

Automatisk udledning

Mange Scala-biblioteker bruger TC, og lader programmøren implementere dem i deres kodebase.

For eksempel bruger Circe (et json-de-serialiseringsbibliotek) TC `Encoder[T]` og `Decoder[T]` for programmører at implementere i deres kodebase. Når det er implementeret, kan hele bibliotekets omfang bruges. 

Disse implementeringer af TC er mere end ofte dataorienterede kortlæggere. De har ikke brug for nogen forretningslogik, er kedelige at skrive og en byrde at vedligeholde synkroniseret med sagsklasser.

I en sådan situation tilbyder disse biblioteker det, man kalder automatisk afledning el halvautomatisk afledning. Se for eksempel Circe automatisk , halvautomatisk afledning. Med semi-automatisk afledning kan programmøren erklære en forekomst af en typeklasse med en vis mindre syntaks, hvorimod automatisk afledning ikke kræver nogen kodeændring bortset fra en import.

Under hætten, på kompileringstidspunktet, introspekterer generiske makroer typer som ren datastruktur og generere en TC[T] for biblioteksbrugere. 

At udlede generisk en TC er meget almindeligt, så Scala introducerede en komplet værktøjskasse til det formål. Denne metode annonceres ikke altid af biblioteksdokumentation, selvom det er Scala 3-måden at bruge afledning.

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)

Linje 18 en ny TC `Named` introduceres. Denne TC er strengt taget ikke relateret til blockchain-forretningen. Dens formål er at navngive blockchain baseret på navnet på case-klassen.

Først fokus på definitioner linje 36-38. Der er 2 syntakser til at udlede en TC:

  1. Linje 36 kan TC-instansen defineres direkte på sagsklassen med `derives` søgeord. Under hætten genererer compileren en given `Named` forekomst i `Polkadot` ledsagende objekt.
  2. Linje 37 og 38, typeklasser er givet på allerede eksisterende klasser med `TC.derived

Linje 31 er en generisk udvidelse defineret (se tidligere afsnit) og `blockchainName` bruges naturligt.  

Den `derives` søgeord forventer en metode med formen `inline def derived[T](using Mirror.Of[T]): TC[T] = ???` som er defineret linje 24. Jeg vil ikke forklare i dybden, hvad koden gør. I store træk:

  • `inline def` definerer en makro
  • `Mirror` er en del af værktøjskassen til at introspektere typer. Der er forskellige slags spejle, og linje 26 fokuserer koden på `Product` spejle (en sagsklasse er et produkt). Linje 27, hvis programmører forsøger at udlede noget, der ikke er en `Product`, vil koden ikke kompilere.
  • den `Mirror` indeholder andre typer. En af dem, `MirrorLabel`, er en streng, der indeholder typenavnet. Denne værdi bruges i implementeringen, linje 29, af `Named` TC.

TC-forfattere kan bruge metaprogrammering til at levere funktioner, der generisk genererer forekomster af TC givet en type. Programmerere kan bruge dedikeret biblioteks-API eller Scala-afledte værktøjer til at oprette forekomster til deres kode.

Uanset om du har brug for generisk eller specifik kode for at implementere en TC, er der en løsning til hver situation. 

Oversigt over alle fordelene

  • Det løser udtryksproblemet
    • Nye typer kan implementere eksisterende adfærd gennem traditionel egenskabsarv
    • Ny adfærd kan implementeres på eksisterende typer
  • Adskillelse af bekymring
    • Koden er ikke ødelagt og kan let slettes. En TC adskiller data og adfærd, hvilket er et funktionelt programmeringsmotto.
  • Det er sikkert
    • Det er type sikkert, fordi det ikke er afhængigt af introspektion. Det undgår stor mønstermatching, der involverer typer. hvis du støder på dig selv ved at skrive en sådan kode, kan du opdage et tilfælde, hvor TC-mønster vil passe perfekt.
    • Den implicitte mekanisme er kompileringssikker! Hvis en instans mangler på kompileringstidspunktet, kompileres koden ikke. Ingen overraskelse under kørsel.
  • Det bringer ad hoc polymorfi
    • Ad hoc polymorfi mangler normalt i traditionel objektorienteret programmering.
    • Med ad-hoc polymorfi kan udviklere implementere den samme adfærd for forskellige ikke-relaterede typer uden at bruge traditionel underskrivning (som kobler koden)
  • Afhængighedsinjektion gjort let
    • En TC-instans kan ændres i henhold til Liskov substitutionsprincippet. 
    • Når en komponent er afhængig af en TC, kan en hånet TC nemt injiceres til testformål. 

Modindikationer

Hver hammer er designet til en række problemer.

Typeklasser er til adfærdsproblemer og må ikke bruges til dataarv. Brug sammensætning til det formål.

Den sædvanlige undertypning er mere ligetil. Hvis du ejer kodebasen og ikke sigter efter udvidelsesmuligheder, kan typeklasser være overkill.

For eksempel er der i Scala-kernen en `Numeric` type klasse:

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

Det giver virkelig mening at bruge sådan en typeklasse, fordi den ikke kun tillader genbrug af algebraiske algoritmer på typer, der er indlejret i Scala (Int, BigInt, …), men også på brugerdefinerede typer (en `ComplexNumber` for eksempel).

På den anden side bruger implementering af Scala-samlinger for det meste subtyping i stedet for typeklasse. Dette design giver mening af flere grunde:

  • Indsamlings-API'en formodes at være komplet og stabil. Det afslører almindelig adfærd gennem træk, der er nedarvet af implementeringer. At være meget strækbar er ikke et særligt mål her.
  • Det skal være nemt at bruge. TC tilføjer en mental overhead på slutbrugerprogrammøren.
  • TC kan også pådrage sig små overhead i ydeevne. Dette kan være kritisk for en samling API.
  • Selvom samlings-API'en stadig kan udvides gennem nye TC defineret af tredjepartsbiblioteker.

Konklusion

Vi har set, at TC er et simpelt mønster, der løser et stort problem. Takket være Scala rig syntaks kan TC-mønsteret implementeres og bruges på mange måder. TC-mønsteret er i tråd med det funktionelle programmeringsparadigme og er et fantastisk værktøj til en ren arkitektur. Der er ingen sølvkugle, og TC-mønster skal påføres, når det passer.

Håber du har fået viden ved at læse dette dokument. 

Koden fås på https://github.com/jprudent/type-class-article. Kontakt mig venligst, hvis du har nogen form for spørgsmål eller bemærkninger. Du kan bruge problemer eller kodekommentarer i lageret, hvis du vil.


Jerome PRUDENT

Software Engineer

Tidsstempel:

Mere fra Ledger