Typklasser i Scala3: En nybörjarguide | Huvudbok

Typ klasser i Scala3: En nybörjarguide | Huvudbok

Typklasser i Scala3: En nybörjarguide | Ledger PlatoBlockchain Data Intelligence. Vertikal sökning. Ai.

Detta dokument är avsett för nybörjaren Scala3-utvecklare som redan är bevandrad i Scala-prosa, men är förbryllad över allaimplicits` och parametriserade egenskaper i koden.

Detta dokument förklarar varför, hur, var och när Typklasser (TC).

Efter att ha läst detta dokument kommer nybörjaren Scala3-utvecklaren att få gedigen kunskap att använda och dyka in i källkoden till mycket av Scala-bibliotek och börja skriva idiomatisk Scala-kod.

Låt oss börja med varför...

Uttrycksproblemet

1998, Philip Wadler uppgav att "uttrycket problem är ett nytt namn på ett gammalt problem". Det är problemet med mjukvaruutvidgning. Enligt herr Wadlers skrift måste lösningen på uttrycksproblemet följa följande regler:

  • Regel 1: Tillåt implementering av existerande beteenden (tänk på Scala-egenskap) som ska tillämpas på nya representationer (tänk på en fallklass)
  • Regel 2:  Tillåt implementering av nya beteenden att tillämpas på befintliga representationer
  • Regel 3: Det får inte äventyra typ säkerhet
  • Regel 4: Det får inte vara nödvändigt att kompilera om befintlig kod

Att lösa detta problem kommer att vara silvertråden i den här artikeln.

Regel 1: implementering av befintligt beteende på ny representation

Alla objektorienterade språk har en inbyggd lösning för regel 1 med subtyp polymorfism. Du kan säkert implementera någon `trait` definieras i ett beroende av en `class` i din egen kod, utan att kompilera om beroendet. Låt oss se det i aktion:

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 detta fiktiva exempel, bibliotek `Lib1` (rad 5) definierar en egenskap `Blockchain` (rad 6) med 2 implementeringar av den (rad 9 & 12). `Lib1` kommer att förbli densamma i ALLA detta dokument (upprätthållande av regel 4).

`Lib2` (rad 15) implementerar det befintliga beteendet `Blockchain` på en ny klass `Polkadot` (regel 1) på ett typsäkert (regel 3) sätt, utan att kompilera om `Lib1` (regel 4). 

Regel 2: implementering av nya beteenden som ska tillämpas på befintliga representationer

Låt oss föreställa oss i `Lib2"vi vill ha ett nytt beteende".lastBlock` ska implementeras specifikt för varje `Blockchain`.

Det första som kommer att tänka på är att skapa en stor switch baserat på typen av 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()))

Denna lösning är en svag omimplementering av typbaserad polymorfism som redan är inbakat i språket!

`Lib1` lämnas orörd (kom ihåg att regel 4 tillämpas i hela detta dokument). 

Lösningen implementerad i `Lib2` är okej tills ännu en blockkedja introduceras i `Lib3`. Det bryter mot typsäkerhetsregeln (regel 3) eftersom den här koden misslyckas vid körning på rad 37. Och modifierar `Lib2" skulle bryta mot regel 4.

En annan lösning är att använda 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` lämnas orörd (upprätthållande av regel 4 i hela dokumentet). 

`Lib2` definierar beteende för sin typ (rad 21) och `tillägg` för befintliga typer (rad 23 & 25).

Rad 28-30, det nya beteendet kan användas i varje klass. 

Men det finns inget sätt att kalla detta nya beteende polymorft (rad 32). Varje försök att göra det leder till kompileringsfel (rad 33) eller till typbaserade växlar. 

Denna regel nr 2 är knepig. Vi försökte implementera det med vår egen definition av polymorfism och "extension"-trick. Och det var konstigt.

Det finns en saknad bit som heter ad hoc polymorfism: förmågan att säkert skicka en beteendeimplementering enligt en typ, varhelst beteendet och typen definieras. Gå in i Typ Klass mönster.

Typklassmönstret

Typklass (TC för kort) mönsterrecept har 3 steg. 

  1. Definiera ett nytt beteende
  2. Implementera beteendet
  3. Använd beteendet

I följande avsnitt implementerar jag TC-mönstret på det mest enkla sättet. Det är mångsidigt, klumpigt och opraktiskt. Men håll ut, dessa varningar kommer att fixas steg för steg längre fram i dokumentet.

1. Definiera ett nytt beteende
Skala

object Lib2:
 import Lib1.*

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

`Lib1` lämnas återigen orörd.

Det nya beteendet is TC som materialiseras av egenskapen. Funktionerna som definieras i egenskapen är ett sätt att tillämpa vissa aspekter av det beteendet.

Parametern `A` representerar den typ vi vill tillämpa beteende på, som är undertyper av `Blockchain` i vårt fall.

Några anmärkningar:

  • Om det behövs, den parametrerade typen `A` kan ytterligare begränsas av systemet av typen Scala. Till exempel kan vi genomdriva `A"att vara en".Blockchain`. 
  • Dessutom kan TC ha många fler funktioner deklarerade i sig.
  • Slutligen kan varje funktion ha många fler godtyckliga parametrar.

Men låt oss hålla saker enkelt för läsbarhetens skull.

2. Implementera beteendet
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")

För varje typ den nya `LastBlock` beteende förväntas, finns det ett specifikt exempel på det beteendet. 

Den `Ethereum` implementeringsrad 22 beräknas från `eth` instans skickas som parameter. 

Genomförandet av `LastBlock` för `Bitcoin` rad 25 är implementerad med en ohanterad IO och använder inte dess parameter.

Så, `Lib2` implementerar nytt beteende `LastBlock` för `Lib1` klasser.

3. Använd beteendet
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))

Rad 30 `useLastBlock` använder en instans av `A` och `LastBlock` beteende definierat för det fallet.

Rad 33 `useLastBlock` anropas med en instans av `Ethereum` och en implementering av `LastBlock` definieras i `Lib2`. Observera att det är möjligt att godkänna valfri alternativ implementering av `LastBlock[A]` (tänk på beroende injektion).

`useLastBlock` är limmet mellan representation (det faktiska A) och dess beteende. Data och beteende separeras, vilket är vad funktionell programmering förespråkar.

Diskussion

Låt oss sammanfatta reglerna för uttrycksproblemet:

  • Regel 1: Tillåt implementering av existerande beteenden  att tillämpas på nya klasser
  • Regel 2:  Tillåt implementering av nya beteenden att tillämpas på befintliga klasser
  • Regel 3: Det får inte äventyra typ säkerhet
  • Regel 4: Det får inte vara nödvändigt att kompilera om befintlig kod

Regel 1 kan lösas direkt med subtyp polymorfism.

TC-mönstret som just presenterades (se föregående skärmdump) löser regel 2. Det är typsäkert (regel 3) och vi rörde aldrig `Lib1` (regel 4). 

Men det är opraktiskt att använda av flera anledningar:

  • Raderna 33-34 måste vi uttryckligen skicka beteendet längs dess instans. Detta är en extra omkostnad. Vi borde bara skriva `useLastBlock(Bitcoin())`.
  • Rad 31 syntaxen är ovanlig. Vi skulle hellre föredra att skriva en kortfattad och mer objektorienterad  `instance.lastBlock()` uttalande.

Låt oss lyfta fram några Scala-funktioner för praktisk TC-användning. 

Förbättrad utvecklarupplevelse

Scala har en unik uppsättning funktioner och syntaktiska sockerarter som gör TC till en riktigt trevlig upplevelse för utvecklare.

Implicit

Den implicita räckvidden är en speciell räckvidd som löses vid kompileringstidpunkten där endast en instans av en given typ kan existera. 

Ett program sätter en instans i det implicita omfånget med `given` nyckelord. Alternativt kan ett program hämta en instans från det implicita omfånget med nyckelordet `using`.

Den implicita omfattningen löses vid kompilering, det finns ett känt sätt att ändra det dynamiskt under körning. Om programmet kompilerar, är det implicita omfattningen löst. Under körning är det inte möjligt att sakna implicita instanser där de används. Den enda möjliga förvirringen kan komma från att använda fel implicita instans, men denna fråga lämnas till varelsen mellan stolen och tangentbordet.

Det skiljer sig från en global räckvidd eftersom: 

  1. Det löses kontextuellt. Två platser i ett program kan använda en instans av samma givna typ i implicit omfattning, men dessa två instanser kan vara olika.
  2. Bakom scenen skickar koden implicita argumentfunktion för att fungera tills den implicita användningen uppnås. Det använder inte ett globalt minnesutrymme.

Går tillbaka till typklassen! Låt oss ta exakt samma exempel.

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` är samma omodifierade kod som vi tidigare definierat. 

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

Rad 19 ett nytt beteende `LastBlock` definieras, precis som vi gjorde tidigare.

Rad 22 och rad 25, `val` ersätts med `given`. Båda implementeringarna av `LastBlock` sätts i den implicita omfattningen.

Rad 31 `useLastBlock` förklarar beteendet `LastBlock` som en implicit parameter. Kompilatorn löser lämplig instans av `LastBlock` från implicit omfattning kontextualiserad från uppringarens platser (raderna 33 och 34). Rad 28 importerar allt från `Lib2`, inklusive den implicita räckvidden. Så kompilatorn skickar instanser definierade raderna 22 och 25 som den sista parametern för `useLastBlock`. 

Som biblioteksanvändare är det enklare att använda en typklass än tidigare. Rad 34 och 35 behöver en utvecklare bara se till att en instans av beteendet injiceras i den implicita omfattningen (och detta kan vara en ren `import`). Om en implicit inte är `given` där koden är `using` det, säger kompilatorn till honom.

Scalas implicita underlättar uppgiften att skicka klassinstanser tillsammans med instanser av deras beteenden.

Implicita sockerarter

Rad 22 och 25 i tidigare kod kan förbättras ytterligare! Låt oss iterera på TC-implementeringarna.

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

Rad 22 och 25, om namnet på instansen är oanvänt, kan det utelämnas.

Skala


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

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

Rad 22 och 25, upprepningen av typen kan ersättas med `with` nyckelord.

Skala

given LastBlock[Ethereum] = _.lastBlock

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

Eftersom vi använder en degenererad egenskap med en enda funktion i den, kan IDE föreslå att du förenklar koden med ett SAM-uttryck. Även om det är korrekt, tror jag inte att det är en korrekt användning av SAM, såvida du inte är tillfälligtvis kodgolf.

Scala erbjuder syntaktiska sockerarter för att effektivisera syntaxen och ta bort onödig namngivning, deklaration och typredundans.

Förlängning

Används klokt, den `extension`-mekanismen kan förenkla syntaxen för att använda en typklass.

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)

Raderna 28-29 en generisk förlängningsmetod `lastBlock` är definierad för någon `A` med en `LastBlock` TC-parameter i implicit omfattning.

Raderna 33-34 förlängningen utnyttjar en objektorienterad syntax för att använda 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)

Rad 28, TC-parametern kan också definieras för hela förlängningen för att undvika upprepning. Rad 30 återanvänder vi TC i tillägget för att definiera `penultimateBlock` (även om det kunde implementeras på `LastBlock`egenskap direkt)

Magin händer när TC används. Uttrycket känns mycket mer naturligt, vilket ger en illusion av att beteende `lastBlock` blandas ihop med instansen.

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

Rad 34 använder funktionen en implicit TC. Observera att TC inte behöver namnges om det namnet är onödigt.

TC-mönstret är så flitigt använt att det finns en generisk typsyntax för att uttrycka "en typ med ett implicit beteende". Rad 36 syntaxen är ett mer kortfattat alternativ till den föregående (rad 34). Den undviker att specifikt deklarera den icke namngivna implicita TC-parametern.

Detta avslutar avsnittet utvecklarupplevelse. Vi har sett hur tillägg, impliciter och en del syntaktisk socker kan ge en mindre rörig syntax när TC används och definieras.

Automatisk härledning

Många Scala-bibliotek använder TC, vilket låter programmeraren implementera dem i sin kodbas.

Till exempel använder Circe (ett json-avserialiseringsbibliotek) TC `Encoder[T]` och `Decoder[T]` för programmerare att implementera i sin kodbas. När det väl är implementerat kan hela bibliotekets omfattning användas. 

Dessa implementeringar av TC är mer än ofta dataorienterade kartläggare. De behöver ingen affärslogik, är tråkiga att skriva och en börda att hålla i synk med fallklasser.

I en sådan situation erbjuder dessa bibliotek vad som kallas automatisk härledning eller halvautomatisk härledning. Se till exempel Circe automatisk och halvautomatisk härledning. Med halvautomatisk härledning kan programmeraren deklarera en instans av en typklass med någon mindre syntax, medan automatisk härledning inte kräver någon kodändring förutom en import.

Under huven, vid kompileringstillfället, introspekterar generiska makron typer som ren datastruktur och generera en TC[T] för biblioteksanvändare. 

Att generiskt härleda en TC är mycket vanligt, så Scala introducerade en komplett verktygslåda för det ändamålet. Denna metod annonseras inte alltid av biblioteksdokumentation även om det är Scala 3-sättet att använda härledning.

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` introduceras. Denna TC är strikt sett inte relaterad till blockchain-verksamheten. Dess syfte är att namnge blockkedjan baserat på namnet på fallklassen.

Fokus först på definitioner rad 36-38. Det finns två syntaxer för att härleda en TC:

  1. Rad 36 kan TC-instansen definieras direkt på fallklassen med `derives` nyckelord. Under huven genererar kompilatorn en given `Named` instans i `Polkadot` medföljande objekt.
  2. Rad 37 och 38, typklassinstanser ges på redan existerande klasser med `TC.derived

Rad 31 en generisk förlängning definieras (se tidigare avsnitt) och `blockchainName` används naturligt.  

Den `derives` nyckelord förväntar sig en metod med formen `inline def derived[T](using Mirror.Of[T]): TC[T] = ???` som är definierad rad 24. Jag kommer inte att förklara på djupet vad koden gör. I stora drag:

  • `inline def` definierar ett makro
  • `Mirror` är en del av verktygslådan för att introspektera typer. Det finns olika typer av speglar, och rad 26 fokuserar koden på `Product` speglar (en fallklass är en produkt). Rad 27, om programmerare försöker härleda något som inte är en `Product`, koden kompileras inte.
  • den `Mirror` innehåller andra typer. En av dem, `MirrorLabel`, är en sträng som innehåller typnamnet. Detta värde används i implementeringen, rad 29, av `Named` TC.

TC-författare kan använda metaprogrammering för att tillhandahålla funktioner som generiskt genererar instanser av TC givet en typ. Programmerare kan använda dedikerade biblioteks-API eller Scala-härledande verktyg för att skapa instanser för sin kod.

Oavsett om du behöver generisk eller specifik kod för att implementera en TC, finns det en lösning för varje situation. 

Sammanfattning av alla fördelar

  • Det löser uttrycksproblemet
    • Nya typer kan implementera existerande beteende genom traditionellt arvsdrag
    • Nya beteenden kan implementeras på befintliga typer
  • Separation av oro
    • Koden är inte manglad och lätt att radera. En TC separerar data och beteende, vilket är ett funktionellt programmeringsmotto.
  • Det är säkert
    • Det är typsäkert eftersom det inte förlitar sig på introspektion. Det undviker stor mönstermatchning som involverar typer. om du stöter på att du skriver sådan kod kan du upptäcka ett fall där TC-mönster passar perfekt.
    • Den implicita mekanismen är kompileringssäker! Om en instans saknas vid kompilering kommer koden inte att kompileras. Ingen överraskning vid körning.
  • Det ger ad-hoc polymorfism
    • Ad hoc polymorfism saknas vanligtvis i traditionell objektorienterad programmering.
    • Med ad-hoc polymorfism kan utvecklare implementera samma beteende för olika orelaterade typer utan att använda traditionell subtyping (som kopplar koden)
  • Beroendeinjektion på ett enkelt sätt
    • En TC-instans kan ändras med hänsyn till Liskov-substitutionsprincipen. 
    • När en komponent är beroende av en TC, kan en hånad TC lätt injiceras för teständamål. 

Motindikationer

Varje hammare är designad för en rad problem.

Typklasser är avsedda för beteendeproblem och får inte användas för nedärvning av data. Använd sammansättning för detta ändamål.

Den vanliga subtypningen är mer okomplicerad. Om du äger kodbasen och inte siktar på utökbarhet kan typklasser vara överdrivna.

Till exempel, i Scala-kärnan finns en `Numeric` typklass:

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 är verkligen vettigt att använda en sådan typklass eftersom den inte bara tillåter återanvändning av algebraiska algoritmer på typer som är inbäddade i Scala (Int, BigInt, …), utan också på användardefinierade typer (en `ComplexNumber` till exempel).

Å andra sidan använder implementering av Scala-samlingar för det mesta subtyping istället för typklass. Denna design är vettig av flera anledningar:

  • Insamlings-API:et ska vara komplett och stabilt. Det avslöjar vanligt beteende genom egenskaper som ärvts av implementeringar. Att vara mycket töjbar är inte ett särskilt mål här.
  • Det måste vara enkelt att använda. TC lägger till en mental overhead på slutanvändarprogrammeraren.
  • TC kan också medföra små omkostnader i prestanda. Detta kan vara avgörande för ett samlings-API.
  • Men samlings-API:et kan fortfarande utökas genom nya TC som definieras av tredje parts bibliotek.

Slutsats

Vi har sett att TC är ett enkelt mönster som löser ett stort problem. Tack vare Scala rik syntax kan TC-mönstret implementeras och användas på många sätt. TC-mönstret är i linje med det funktionella programmeringsparadigmet och är ett fantastiskt verktyg för en ren arkitektur. Det finns ingen silverkula och TC-mönster måste appliceras när det passar.

Hoppas du fick kunskap när du läste detta dokument. 

Koden finns tillgänglig på https://github.com/jprudent/type-class-article. Kontakta mig gärna om du har några frågor eller kommentarer. Du kan använda problem eller kodkommentarer i arkivet om du vill.


Jerome PRUDENT

Programvara ingenjör

Tidsstämpel:

Mer från Ledger