Type klasser i Scala3: En nybegynnerveiledning | Ledger

Type klasser i Scala3: A Beginner's Guide | Ledger

Type klasser i Scala3: En nybegynnerveiledning | Ledger PlatoBlockchain Data Intelligence. Vertikalt søk. Ai.

Dette dokumentet er ment for nybegynnere Scala3-utviklere som allerede er bevandret i Scala-prosa, men er forvirret over alle `implicits` og parameteriserte egenskaper i koden.

Dette dokumentet forklarer hvorfor, hvordan, hvor og når Typeklasser (TC).

Etter å ha lest dette dokumentet vil nybegynneren Scala3-utvikleren få solid kunnskap å bruke og dykke ned i kildekoden til mye av Scala-biblioteker og begynn å skrive idiomatisk Scala-kode.

La oss starte med hvorfor...

Uttrykksproblemet

I 1998, Philip Wadler uttalte at "uttrykket problem er et nytt navn på et gammelt problem". Det er problemet med programvareutvidbarhet. I følge herr Wadler skriver, må løsningen på uttrykksproblemet overholde følgende regler:

  • Regel 1: Tillat implementering av eksisterende atferd (tenk på Scala-trekk) som skal brukes på nye representasjoner (tenk på en saksklasse)
  • Regel 2:  Tillat implementering av ny atferd skal søkes på eksisterende representasjoner
  • Regel 3: Det må ikke sette den i fare type sikkerhet
  • Regel 4: Det må ikke være nødvendig å rekompilere eksisterende kode

Å løse dette problemet vil være sølvtråden i denne artikkelen.

Regel 1: implementering av eksisterende atferd på ny representasjon

Ethvert objektorientert språk har en innebygd løsning for regel 1 med subtype polymorfisme. Du kan trygt implementere hvilken som helst `trait` definert i en avhengighet av en `class` i din egen kode, uten å rekompilere avhengigheten. La oss se det i aksjon:

Skala

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 eksemplet, bibliotek `Lib1` (linje 5) definerer en egenskap `Blockchain` (linje 6) med 2 implementeringer av den (linje 9 og 12). `Lib1` vil forbli den samme i ALT dette dokumentet (håndhevelse av regel 4).

`Lib2` (linje 15) implementerer den eksisterende atferden `Blockchain`på en ny klasse`Polkadot` (regel 1) på en typesikker (regel 3) måte, uten å rekompilere `Lib1` (regel 4). 

Regel 2: implementering av ny atferd som skal brukes på eksisterende representasjoner

La oss forestille oss i `Lib2`vi vil ha en ny oppførsel`lastBlock` skal implementeres spesifikt for hver `Blockchain`.

Det første du tenker på er å lage en stor bryter basert på typen parameter.

Skala

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øsningen er en svak reimplementering av typebasert polymorfisme som allerede er innbakt i språk!

`Lib1` blir stående urørt (husk at regel 4 håndheves over hele dette dokumentet). 

Løsningen implementert i `Lib2` er greit nok inntil enda en blokkjede introduseres i `Lib3`. Det bryter med typesikkerhetsregelen (regel 3) fordi denne koden feiler ved kjøretid på linje 37. Og endrer `Lib2` ville bryte regel 4.

En annen løsning er å bruke en `extension`.

Skala

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` blir stående urørt (håndhevelse av regel 4 i hele dokumentet). 

`Lib2` definerer atferd for sin type (linje 21) og `utvidelse` for eksisterende typer (linje 23 og 25).

Linje 28-30, den nye atferden kan brukes i hver klasse. 

Men det er ingen måte å kalle denne nye oppførselen polymorf (linje 32). Ethvert forsøk på å gjøre det fører til kompileringsfeil (linje 33) eller til typebaserte brytere. 

Denne regel nr. 2 er vanskelig. Vi prøvde å implementere det med vår egen definisjon av polymorfisme og "utvidelse"-triks. Og det var rart.

Det er en manglende brikke kalt ad hoc polymorfisme: muligheten til å sende en atferdsimplementering på en sikker måte i henhold til en type, uansett hvor atferden og typen er definert. Skriv inn Type klasse mønster.

Type Class-mønsteret

Type Class (TC for kort) mønsteroppskrift har 3 trinn. 

  1. Definer en ny atferd
  2. Implementer atferden
  3. Bruk oppførselen

I den følgende delen implementerer jeg TC-mønsteret på den mest enkle måten. Det er detaljert, klønete og upraktisk. Men hold fast, disse forbeholdene vil bli løst trinn for trinn videre i dokumentet.

1. Definer en ny atferd
Skala

object Lib2:
 import Lib1.*

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

`Lib1` er igjen urørt.

Den nye oppførselen is TC materialisert av egenskapen. Funksjonene som er definert i egenskapen er en måte å bruke noen aspekter ved den atferden på.

Parameteren `A` representerer typen vi ønsker å bruke atferd på, som er undertyper av `Blockchain` i vårt tilfelle.

Noen bemerkninger:

  • Om nødvendig, den parameteriserte typen `A` kan begrenses ytterligere av Scala-systemet. For eksempel kan vi håndheve `A`å være en`Blockchain`. 
  • TC kan også ha mange flere funksjoner deklarert i den.
  • Til slutt kan hver funksjon ha mange flere vilkårlige parametere.

Men la oss holde ting enkelt for lesbarhetens skyld.

2. Implementer atferden
Skala

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` atferd er forventet, det er et spesifikt tilfelle av den oppførselen. 

Den `Ethereum` implementeringslinje 22 beregnes fra `eth` forekomst sendt som parameter. 

Gjennomføringen av `LastBlock` for `Bitcoin` linje 25 er implementert med en uadministrert IO og bruker ikke parameteren.

Så, `Lib2` implementerer ny atferd `LastBlock` for `Lib1` klasser.

3. Bruk atferden
Skala

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` bruker en forekomst av `A` og `LastBlock` oppførsel definert for dette tilfellet.

Linje 33 `useLastBlock` kalles med en forekomst av `Ethereum` og en implementering av `LastBlock` definert i `Lib2`. Merk at det er mulig å passere enhver alternativ implementering av `LastBlock[A]` (tenk på avhengighetsinjeksjon).

`useLastBlock` er limet mellom representasjon (den faktiske A) og dens oppførsel. Data og atferd er atskilt, noe funksjonell programmering tar til orde for.

Diskusjon

La oss oppsummere reglene for uttrykksproblemet:

  • Regel 1: Tillat implementering av eksisterende atferd  skal søkes på nye klasser
  • Regel 2:  Tillat implementering av ny atferd skal søkes på eksisterende klasser
  • Regel 3: Det må ikke sette den i fare type sikkerhet
  • Regel 4: Det må ikke være nødvendig å rekompilere eksisterende kode

Regel 1 kan løses ut av boksen med subtype polymorfisme.

TC-mønsteret som nettopp ble presentert (se forrige skjermbilde) løser regel 2. Det er typesikkert (regel 3) og vi rørte aldri `Lib1` (regel 4). 

Det er imidlertid upraktisk å bruke av flere grunner:

  • Linje 33-34 må vi eksplisitt føre atferden langs instansen. Dette er en ekstra overhead. Vi burde bare skrive `useLastBlock(Bitcoin())`.
  • Linje 31 er syntaksen uvanlig. Vi foretrekker heller å skrive en kortfattet og mer objektorientert  `instance.lastBlock()` uttalelse.

La oss fremheve noen Scala-funksjoner for praktisk TC-bruk. 

Forbedret utvikleropplevelse

Scala har et unikt sett med funksjoner og syntaktiske sukkerarter som gjør TC til en virkelig hyggelig opplevelse for utviklere.

Implisitt

Det implisitte omfanget er et spesielt omfang som løses på kompileringstidspunktet der bare én forekomst av en gitt type kan eksistere. 

Et program setter en instans i det implisitte omfanget med `given` nøkkelord. Alternativt kan et program hente en forekomst fra det implisitte omfanget med nøkkelordet `using`.

Det implisitte omfanget løses ved kompilering, det er kjent måte å endre det dynamisk på under kjøring. Hvis programmet kompilerer, er det implisitte omfanget løst. Under kjøring er det ikke mulig å ha manglende implisitte tilfeller der de brukes. Den eneste mulige forvirringen kan komme fra å bruke feil implisitt instans, men dette problemet er overlatt til skapningen mellom stolen og tastaturet.

Det er forskjellig fra et globalt omfang fordi: 

  1. Det er løst kontekstuelt. To plasseringer av et program kan bruke en forekomst av samme gitte type i implisitt omfang, men disse to forekomstene kan være forskjellige.
  2. Bak scenen sender koden implisitte argumenter som fungerer til den implisitte bruken er nådd. Den bruker ikke en global minneplass.

Går tilbake til typeklassen! La oss ta nøyaktig samme eksempel.

Skala

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 umodifiserte koden vi tidligere definerte. 

Skala

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 oppførsel `LastBlock` er definert, akkurat som vi gjorde tidligere.

Linje 22 og linje 25, `val` er erstattet med `given`. Begge implementeringene av `LastBlock` er satt i det implisitte omfanget.

Linje 31 `useLastBlock` erklærer atferden `LastBlock` som en implisitt parameter. Kompilatoren løser den aktuelle forekomsten av `LastBlock` fra implisitt omfang kontekstualisert fra oppringerplasseringer (linje 33 og 34). Linje 28 importerer alt fra `Lib2`, inkludert det implisitte omfanget. Så kompilatoren sender forekomster definert linje 22 og 25 som den siste parameteren til `useLastBlock`. 

Som bibliotekbruker er det enklere å bruke en typeklasse enn før. Linje 34 og 35 må en utvikler bare sørge for at en forekomst av atferden injiseres i det implisitte omfanget (og dette kan være bare `import`). Hvis en implisitt ikke er `given` der koden er `using` det, forteller kompilatoren ham.

Scalas implisitte forenkler oppgaven med å sende klasseforekomster sammen med forekomster av deres oppførsel.

Implisitte sukkerarter

Linje 22 og 25 i forrige kode kan forbedres ytterligere! La oss gjenta TC-implementeringene.

Skala

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å forekomsten er ubrukt, kan det utelates.

Skala


 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, repetisjonen av typen kan erstattes med `with` nøkkelord.

Skala

given LastBlock[Ethereum] = _.lastBlock

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

Fordi vi bruker en degenerert egenskap med en enkelt funksjon i den, kan IDE foreslå å forenkle koden med et SAM-uttrykk. Selv om det er riktig, tror jeg ikke det er en riktig bruk av SAM, med mindre du tilfeldig spiller golf.

Scala tilbyr syntaktiske sukkerarter for å strømlinjeforme syntaksen, og fjerner unødvendig navngivning, erklæring og typeredundans.

Extension

Brukt med omhu, den `extension`-mekanisme kan forenkle syntaksen for bruk av en typeklasse.

Skala

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 utvidelsesmetode `lastBlock` er definert for enhver `A` med en `LastBlock` TC-parameter i implisitt omfang.

Linje 33-34 bruker utvidelsen en objektorientert syntaks for å bruke TC.

Skala

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 utvidelsen for å unngå repetisjon. Linje 30 gjenbruker vi TC i utvidelsen for å definere `penultimateBlock` (selv om det kunne implementeres på `LastBlock` egenskap direkte)

Magien skjer når TC brukes. Uttrykket føles mye mer naturlig, og gir en illusjon om at oppførsel `lastBlock` er forvekslet med instansen.

Generisk type med TC
Skala

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 bruker funksjonen en implisitt TC. Merk at TC ikke trenger å bli navngitt hvis det navnet er unødvendig.

TC-mønsteret er så mye brukt at det er en generisk typesyntaks for å uttrykke "en type med implisitt oppførsel". Linje 36 syntaksen er et mer kortfattet alternativ til den forrige (linje 34). Den unngår spesifikt å deklarere den ikke navngitte implisitte TC-parameteren.

Dette avslutter delen for utvikleropplevelse. Vi har sett hvordan utvidelser, implisitt og noe syntaktisk sukker kan gi en mindre rotete syntaks når TC brukes og defineres.

Automatisk utledning

Mange Scala-biblioteker bruker TC, og lar programmereren implementere dem i kodebasen.

For eksempel bruker Circe (et json-deserialiseringsbibliotek) TC `Encoder[T]` og `Decoder[T]` for programmerere å implementere i sin kodebase. Når det er implementert, kan hele omfanget av biblioteket brukes. 

Disse implementeringene av TC er mer enn ofte dataorienterte kartleggere. De trenger ingen forretningslogikk, er kjedelige å skrive og en byrde å opprettholde synkronisert med saksklasser.

I en slik situasjon tilbyr disse bibliotekene det som kalles automatisk avledning eller Halvautomatisk avledning. Se for eksempel Circe automatisk og Halvautomatisk avledning. Med semi-automatisk avledning kan programmereren erklære en forekomst av en typeklasse med en viss mindre syntaks, mens automatisk avledning ikke krever noen kodemodifikasjon bortsett fra en import.

Under panseret, på kompileringstidspunktet, introspekterer generiske makroer typer som ren datastruktur og generere en TC[T] for bibliotekbrukere. 

Å utlede generisk en TC er veldig vanlig, så Scala introduserte en komplett verktøykasse for det formålet. Denne metoden er ikke alltid annonsert av bibliotekdokumentasjon, selv om det er Scala 3-måten å bruke avledning.

Skala

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` er introdusert. Denne TC er strengt tatt ikke relatert til blockchain-virksomheten. Hensikten er å navngi blokkjeden basert på navnet på caseklassen.

Først fokus på definisjoner linje 36-38. Det er 2 syntakser for å utlede en TC:

  1. Linje 36 kan TC-forekomsten defineres direkte på saksklassen med `derives` nøkkelord. Under panseret genererer kompilatoren en gitt `Named` forekomst i `Polkadot` følgeobjekt.
  2. Linje 37 og 38, type klasseforekomster er gitt på forhåndseksisterende klasser med `TC.derived

Linje 31 er en generisk utvidelse definert (se tidligere avsnitt) og `blockchainName` brukes naturlig.  

Den `derives` nøkkelord forventer en metode med skjemaet `inline def derived[T](using Mirror.Of[T]): TC[T] = ???` som er definert linje 24. Jeg vil ikke forklare i dybden hva koden gjør. I store trekk:

  • `inline def` definerer en makro
  • `Mirror` er en del av verktøykassen for å introspektere typer. Det finnes forskjellige typer speil, og linje 26 fokuserer koden på `Product` speil (en kasseklasse er et produkt). Linje 27, hvis programmerere prøver å utlede noe som ikke er en `Product`, koden vil ikke kompilere.
  • den `Mirror` inneholder andre typer. En av dem, `MirrorLabel`, er en streng som inneholder typenavnet. Denne verdien brukes i implementeringen, linje 29, av `Named` TC.

TC-forfattere kan bruke metaprogrammering for å gi funksjoner som generisk genererer forekomster av TC gitt en type. Programmerere kan bruke dedikert bibliotek-API eller Scala-avledede verktøy for å lage forekomster for koden deres.

Enten du trenger generisk eller spesifikk kode for å implementere en TC, finnes det en løsning for hver situasjon. 

Oppsummering av alle fordelene

  • Det løser uttrykksproblemet
    • Nye typer kan implementere eksisterende atferd gjennom tradisjonell egenskapsarv
    • Ny atferd kan implementeres på eksisterende typer
  • Separasjon av bekymring
    • Koden er ikke ødelagt og kan enkelt slettes. En TC skiller data og atferd, som er et funksjonelt programmeringsmotto.
  • Det er trygt
    • Det er type trygt fordi det ikke er avhengig av introspeksjon. Den unngår stor mønstermatching som involverer typer. hvis du støter på at du skriver slik kode, kan du oppdage et tilfelle der TC-mønsteret vil passe perfekt.
    • Den implisitte mekanismen er kompileringssikker! Hvis en forekomst mangler på kompileringstidspunktet, kompileres ikke koden. Ingen overraskelse under kjøring.
  • Det bringer ad hoc polymorfisme
    • Ad hoc polymorfisme mangler vanligvis i tradisjonell objektorientert programmering.
    • Med ad-hoc polymorfisme kan utviklere implementere den samme oppførselen for ulike urelaterte typer uten å bruke tradisjonell subtyping (som kobler koden)
  • Avhengighetsinjeksjon gjort enkelt
    • En TC-instans kan endres i forhold til Liskov-substitusjonsprinsippet. 
    • Når en komponent er avhengig av en TC, kan en hånet TC enkelt injiseres for testformål. 

Kontraindikasjoner

Hver hammer er designet for en rekke problemer.

Typeklasser er for atferdsproblemer og må ikke brukes til dataarv. Bruk komposisjon til det formålet.

Den vanlige subtypingen er mer grei. Hvis du eier kodebasen og ikke sikter etter utvidbarhet, kan typeklasser være overkill.

For eksempel, i Scala-kjerne, er det en `Numeric` type klasse:

Skala

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 er virkelig fornuftig å bruke en slik typeklasse fordi den ikke bare tillater gjenbruk av algebraiske algoritmer på typer som er innebygd i Scala (Int, BigInt, …), men også på brukerdefinerte typer (en `ComplexNumber` for eksempel).

På den annen side bruker implementering av Scala-samlinger stort sett subtyping i stedet for typeklasse. Dette designet gir mening av flere grunner:

  • Innsamlings-APIet skal være komplett og stabilt. Den avslører vanlig atferd gjennom egenskaper som er arvet av implementeringer. Å være svært utvidbar er ikke et spesielt mål her.
  • Det må være enkelt å bruke. TC legger til en mental overhead på sluttbrukerprogrammereren.
  • TC kan også pådra seg små overhead i ytelse. Dette kan være kritisk for et samlings-API.
  • Selv om samlings-APIet fortsatt kan utvides gjennom nye TC definert av tredjepartsbiblioteker.

konklusjonen

Vi har sett at TC er et enkelt mønster som løser et stort problem. Takket være Scala-rik syntaks kan TC-mønsteret implementeres og brukes på mange måter. TC-mønsteret er i tråd med det funksjonelle programmeringsparadigmet og er et fantastisk verktøy for en ren arkitektur. Det er ingen sølvkule og TC-mønster må påføres når det passer.

Håper du fikk kunnskap ved å lese dette dokumentet. 

Koden er tilgjengelig på https://github.com/jprudent/type-class-article. Ta gjerne kontakt med meg hvis du har noen form for spørsmål eller kommentarer. Du kan bruke problemer eller kodekommentarer i depotet hvis du vil.


Jerome PRUDENT

Software Engineer

Tidstempel:

Mer fra Ledger