Typelessen in Scala3: een beginnershandleiding | Grootboek

Typelessen in Scala3: een beginnershandleiding | Grootboek

Typelessen in Scala3: een beginnershandleiding | Ledger PlatoBlockchain-gegevensintelligentie. Verticaal zoeken. Ai.

Dit document is bedoeld voor de beginnende Scala3-ontwikkelaar die al vertrouwd is met Scala-proza, maar zich verbaast over alle `implicits` en geparametriseerde eigenschappen in de code.

In dit document wordt uitgelegd waarom, hoe, waar en wanneer van Typeklassen (TC).

Na het lezen van dit document zal de beginnende Scala3-ontwikkelaar een gedegen kennis opdoen om de broncode te gebruiken en erin te duiken veel van Scala-bibliotheken en begin met het schrijven van idiomatische Scala-code.

Laten we beginnen met het waaromโ€ฆ

Het expressieprobleem

In 1998, aldus Philip Wadler dat โ€œde uitdrukking probleem een โ€‹โ€‹nieuwe naam is voor een oud probleemโ€. Het is het probleem van de uitbreidbaarheid van software. Volgens de heer Wadler moet de oplossing van het expressieprobleem aan de volgende regels voldoen:

  • Regel 1: Sta de implementatie toe van bestaand gedrag (denk aan Scala-eigenschap) waarop moet worden toegepast nieuwe representaties (denk aan een casusklasse)
  • Regel 2:  Sta de implementatie toe van nieuw gedrag toegepast worden bestaande representaties
  • Regel 3: Het mag de veiligheid niet in gevaar brengen type veiligheid
  • Regel 4: Het mag niet noodzakelijk zijn om opnieuw te compileren bestaande code

Het oplossen van dit probleem zal de rode draad van dit artikel zijn.

Regel 1: implementatie van bestaand gedrag op nieuwe representatie

Elke objectgeoriรซnteerde taal heeft een ingebouwde oplossing voor regel 1 subtype polymorfisme. U kunt elk `trait` gedefinieerd in een afhankelijkheid van een `class` in uw eigen code, zonder de afhankelijkheid opnieuw te compileren. Laten we dat eens in actie zien:

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

In dit fictieve voorbeeld is bibliotheek `Lib1` (regel 5) definieert een eigenschap `Blockchain` (regel 6) met 2 implementaties ervan (regels 9 en 12). `Lib1` zal hetzelfde blijven in AL dit document (handhaving van regel 4).

`Lib2` (regel 15) implementeert het bestaande gedrag `Blockchain`op een nieuwe klasse`Polkadot` (regel 1) op een typeveilige (regel 3) manier, zonder opnieuw te compileren `Lib1` (regel 4). 

Regel 2: implementatie van nieuw gedrag dat moet worden toegepast op bestaande representaties

Laten we ons voorstellen in `Lib2`we willen nieuw gedrag`lastBlock` specifiek voor elke `Blockchain`.

Het eerste dat in je opkomt is het creรซren van een grote schakelaar op basis van het type 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()))

Deze oplossing is een zwakke herimplementatie van op type gebaseerd polymorfisme dat al in de taal is ingebakken!

`Lib1` blijft onaangeroerd (onthoud dat regel 4 overal in dit document wordt afgedwongen). 

De oplossing geรฏmplementeerd in `Lib2'is okรฉ totdat er weer een nieuwe blockchain wordt geรฏntroduceerd in `Lib3`. Het schendt de typeveiligheidsregel (regel 3) omdat deze code mislukt tijdens runtime op regel 37. En het wijzigen van `Lib2' zou in strijd zijn met regel 4.

Een andere oplossing is het gebruik van een `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` blijft onaangeroerd (handhaving van regel 4 in het hele document). 

`Lib2` definieert gedrag voor zijn type (regel 21) en `extensies` voor bestaande typen (regels 23 & 25).

Regels 28-30, het nieuwe gedrag kan in elke klas worden gebruikt. 

Maar er is geen manier om dit nieuwe gedrag polymorf te noemen (regel 32). Elke poging daartoe leidt tot compilatiefouten (regel 33) of tot op type gebaseerde schakelaars. 

Deze regel nr. 2 is lastig. We probeerden het te implementeren met onze eigen definitie van polymorfisme en de 'extensie'-truc. En dat was raar.

Er is een ontbrekend stuk genaamd ad-hoc polymorfisme: de mogelijkheid om een โ€‹โ€‹gedragsimplementatie veilig te verzenden volgens een type, ongeacht waar het gedrag en het type zijn gedefinieerd. Voer de Type klasse patroon.

Het Type Class-patroon

Het Type Class (kortweg TC) patroonrecept bestaat uit 3 stappen. 

  1. Definieer een nieuw gedrag
  2. Implementeer het gedrag
  3. Gebruik het gedrag

In de volgende sectie implementeer ik het TC-patroon op de meest eenvoudige manier. Het is uitgebreid, onhandig en onpraktisch. Maar wacht even, deze kanttekeningen worden stap voor stap verder in het document opgelost.

1. Definieer nieuw gedrag
Scala

object Lib2:
 import Lib1.*

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

`Lib1' wordt wederom onaangeroerd gelaten.

Het nieuwe gedrag is de TC gematerialiseerd door de eigenschap. De functies die in de eigenschap zijn gedefinieerd, zijn een manier om sommige aspecten van dat gedrag toe te passen.

De parameter `A` vertegenwoordigt het type waarop we gedrag willen toepassen, wat subtypen zijn van `Blockchain'in ons geval.

Enkele opmerkingen:

  • Indien nodig, het geparametriseerde type `A` kan verder worden beperkt door het Scala-typesysteem. We kunnen bijvoorbeeld `A'een' zijnBlockchain`. 
  • Ook zouden in de TC veel meer functies kunnen worden gedeclareerd.
  • Ten slotte kan elke functie veel meer willekeurige parameters hebben.

Maar laten we het omwille van de leesbaarheid simpel houden.

2. Implementeer het gedrag
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")

Voor elk type wordt het nieuwe `LastBlock'gedrag wordt verwacht, er is een specifiek voorbeeld van dat gedrag. 

de `Ethereum'implementatieregel 22 wordt berekend op basis van de 'eth'instantie doorgegeven als parameter. 

De implementatie van 'LastBlock` voor `Bitcoin` regel 25 is geรฏmplementeerd met een onbeheerde IO en gebruikt de parameter ervan niet.

Dus 'Lib2` implementeert nieuw gedrag `LastBlock` voor `Lib1` klassen.

3. Gebruik het gedrag
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))

Lijn 30`useLastBlock` gebruikt een exemplaar van `A' en de 'LastBlock` gedrag gedefinieerd voor die instantie.

Lijn 33`useLastBlock` wordt aangeroepen met een instantie van `Ethereum` en een implementatie van `LastBlock` gedefinieerd in `Lib2`. Merk op dat het mogelijk is om elke alternatieve implementatie van `LastBlock[A]' (denk aan afhankelijkheidsinjectie).

`useLastBlock' is de lijm tussen representatie (de werkelijke A) en zijn gedrag. Gegevens en gedrag zijn gescheiden, en dat is waar functioneel programmeren voor pleit.

Discussie

Laten we de regels van het expressieprobleem samenvatten:

  • Regel 1: Sta de implementatie toe van bestaand gedrag  toegepast worden nieuwe lessen
  • Regel 2:  Sta de implementatie toe van nieuw gedrag toegepast worden bestaande klassen
  • Regel 3: Het mag de veiligheid niet in gevaar brengen type veiligheid
  • Regel 4: Het mag niet noodzakelijk zijn om opnieuw te compileren bestaande code

Regel 1 kan out-of-the-box worden opgelost met subtype polymorfisme.

Het zojuist gepresenteerde TC-patroon (zie vorige schermafbeelding) lost regel 2 op. Het is typeveilig (regel 3) en we hebben '' nooit aangeraaktLib1` (regel 4). 

Het is echter om verschillende redenen onpraktisch om te gebruiken:

  • Regels 33-34 moeten we het gedrag expliciet doorgeven aan de instantie ervan. Dit is een extra overhead. We moeten gewoon ' schrijvenuseLastBlock(Bitcoin())`.
  • Regel 31: de syntaxis is ongebruikelijk. Wij schrijven liever een beknopt en meer objectgericht โ€˜instance.lastBlock()` verklaring.

Laten we enkele Scala-functies uitlichten voor praktisch TC-gebruik. 

Verbeterde ontwikkelaarservaring

Scala heeft een unieke reeks functies en syntactische suikers die TC tot een echt plezierige ervaring voor ontwikkelaars maken.

Impliciet

De impliciete scope is een speciale scope die tijdens het compileren wordt opgelost en waarbij slechts รฉรฉn exemplaar van een bepaald type kan bestaan. 

Een programma plaatst een instantie in de impliciete scope met de `given` trefwoord. Als alternatief kan een programma een instantie uit de impliciete scope ophalen met het trefwoord `using`.

De impliciete reikwijdte wordt tijdens het compileren opgelost, er is een bekende manier om deze tijdens runtime dynamisch te wijzigen. Als het programma compileert, wordt de impliciete reikwijdte opgelost. Tijdens runtime is het niet mogelijk dat er impliciete exemplaren ontbreken waarin ze worden gebruikt. De enige mogelijke verwarring kan voortkomen uit het gebruik van de verkeerde impliciete instantie, maar dit probleem wordt overgelaten aan het wezen tussen de stoel en het toetsenbord.

Het verschilt van een mondiaal bereik omdat: 

  1. Het is contextueel opgelost. Twee locaties van een programma kunnen een exemplaar van hetzelfde gegeven type in impliciete reikwijdte gebruiken, maar deze twee exemplaren kunnen verschillend zijn.
  2. Achter de schermen geeft de code impliciete argumenten door, totdat het impliciete gebruik is bereikt. Het gebruikt geen globale geheugenruimte.

Terug naar de typeklasse! Laten we exact hetzelfde voorbeeld nemen.

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` is dezelfde ongewijzigde code die we eerder hebben gedefinieerd. 

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

Regel 19 een nieuw gedrag `LastBlock' is gedefinieerd, precies zoals we eerder deden.

Regel 22 en regel 25, `val` wordt vervangen door `given`. Beide implementaties van `LastBlock` worden in de impliciete reikwijdte geplaatst.

Lijn 31`useLastBlock'verklaart het gedrag'LastBlock` als een impliciete parameter. De compiler lost het juiste exemplaar van `LastBlock` uit impliciete reikwijdte, gecontextualiseerd op basis van bellerlocaties (regels 33 en 34). Regel 28 importeert alles van `Lib2', inclusief de impliciete reikwijdte. De compiler geeft dus de door instanties gedefinieerde regels 22 en 25 door als de laatste parameter van `useLastBlock`. 

Als bibliotheekgebruiker is het gebruik van een typeklasse eenvoudiger dan voorheen. Regel 34 en 35 hoeft een ontwikkelaar er alleen voor te zorgen dat een instantie van het gedrag in de impliciete scope wordt geรฏnjecteerd (en dit kan slechts een 'import`). Als een impliciete niet `given' waar de code staat 'using' it, vertelt de compiler hem.

Scala's impliciete vereenvoudiging van de taak om klasse-instanties samen met voorbeelden van hun gedrag door te geven.

Impliciete suikers

Regel 22 en 25 van de vorige code kunnen verder worden verbeterd! Laten we de TC-implementaties herhalen.

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

Regels 22 en 25: als de naam van de instantie ongebruikt is, kan deze worden weggelaten.

Scala


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

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

Regels 22 en 25, de herhaling van het type kan worden vervangen door `with` trefwoord.

Scala

given LastBlock[Ethereum] = _.lastBlock

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

Omdat we een gedegenereerde eigenschap gebruiken met een enkele functie erin, kan de IDE voorstellen om de code te vereenvoudigen met een SAM-expressie. Hoewel correct, denk ik niet dat het een juist gebruik van SAM is, tenzij je terloops aan het coderen bent met golfen.

Scala biedt syntactische suikers om de syntaxis te stroomlijnen, waardoor onnodige naamgeving, declaratie en typeredundantie worden verwijderd.

Verlenging

Verstandig gebruikt, de `extension`-mechanisme kan de syntaxis voor het gebruik van een typeklasse vereenvoudigen.

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)

Regels 28-29 een generieke uitbreidingsmethode `lastBlock` is gedefinieerd voor elke `A' met een 'LastBlock` TC-parameter in impliciet bereik.

Regels 33-34: de extensie maakt gebruik van een objectgeoriรซnteerde syntaxis om TC te gebruiken.

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)

Regel 28, de TC-parameter kan ook voor de hele uitbreiding worden gedefinieerd om herhaling te voorkomen. Regel 30 gebruiken we de TC in de extensie om `penultimateBlock` (ook al zou het kunnen worden geรฏmplementeerd op `LastBlock`eigenschap direct)

De magie gebeurt wanneer de TC wordt gebruikt. De uitdrukking voelt een stuk natuurlijker aan, waardoor de illusie ontstaat dat gedrag 'lastBlock` wordt samengevoegd met de instantie.

Generiek type met 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))

Regel 34: de functie gebruikt een impliciete TC. Houd er rekening mee dat de TC geen naam hoeft te krijgen als die naam niet nodig is.

Het TC-patroon wordt zo veel gebruikt dat er een generieke typesyntaxis bestaat om โ€œeen type met impliciet gedragโ€ uit te drukken. Regel 36 De syntaxis is een beknopter alternatief voor de vorige (regel 34). Het vermijdt het specifiek declareren van de naamloze impliciete TC-parameter.

Hiermee is het gedeelte over ontwikkelaarservaring afgesloten. We hebben gezien hoe extensies, implicieten en wat syntactische suiker voor een minder rommelige syntaxis kunnen zorgen wanneer de TC wordt gebruikt en gedefinieerd.

Automatische afleiding

Veel Scala-bibliotheken gebruiken TC en laten de programmeur deze in hun codebasis implementeren.

Circe (een json-de-serialisatiebibliotheek) gebruikt bijvoorbeeld TC `Encoder[T]` en `Decoder[T]` die programmeurs in hun codebase kunnen implementeren. Eenmaal geรฏmplementeerd kan de hele reikwijdte van de bibliotheek worden gebruikt. 

Deze implementaties van TC zijn meer dan vaak datageoriรซnteerde mappers. Ze hebben geen bedrijfslogica nodig, zijn saai om te schrijven en lastig om synchroon te houden met casusklassen.

In een dergelijke situatie bieden die bibliotheken zogenaamde automatisch afleiding of halfautomatische afleiding. Zie bijvoorbeeld Circe automatisch en halfautomatische afleiding. Met semi-automatische afleiding kan de programmeur een instantie van een typeklasse declareren met een kleine syntaxis, terwijl automatische afleiding geen codewijziging vereist, behalve een import.

Onder de motorkap, tijdens het compileren, introspecteren generieke macro's types als pure datastructuur en genereer een TC[T] voor bibliotheekgebruikers. 

Het generiek afleiden van een TC is heel gebruikelijk, dus heeft Scala daarvoor een complete gereedschapskist geรฏntroduceerd. Deze methode wordt niet altijd geadverteerd in bibliotheekdocumentatie, hoewel het de Scala 3-manier is om afleiding te gebruiken.

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)

Regel 18 een nieuwe TC`Named` wordt geรฏntroduceerd. Deze TC heeft strikt genomen niets te maken met de blockchain-activiteiten. Het doel ervan is om de blockchain een naam te geven op basis van de naam van de case-klasse.

Focus eerst op definitieregels 36-38. Er zijn 2 syntaxis voor het afleiden van een TC:

  1. Regel 36 De TC-instantie kan rechtstreeks op de case-klasse worden gedefinieerd met de `derives` trefwoord. Onder de motorkap genereert de compiler een gegeven `Named` exemplaar in `Polkadot' begeleidend object.
  2. Op regel 37 en 38 worden typeklasseninstanties gegeven op reeds bestaande klassen met `TC.derived

Op regel 31 wordt een generieke extensie gedefinieerd (zie voorgaande paragrafen) en `blockchainName` wordt op natuurlijke wijze gebruikt.  

de `derives'trefwoord verwacht een methode met de vorm 'inline def derived[T](using Mirror.Of[T]): TC[T] = ???` dat is gedefinieerd in regel 24. Ik zal niet diepgaand uitleggen wat de code doet. In grote lijnen:

  • `inline def` definieert een macro
  • `Mirror` maakt deel uit van de gereedschapskist om typen te introspecteren. Er zijn verschillende soorten spiegels, en op regel 26 richt de code zich op `Product` spiegels (een case-klasse is een product). Regel 27, als programmeurs iets proberen af โ€‹โ€‹te leiden dat geen `Product`, de code zal niet compileren.
  • de `Mirror` bevat andere typen. Een van hen, 'MirrorLabel`, is een string die de typenaam bevat. Deze waarde wordt gebruikt in de implementatie, regel 29, van de `Named` TC.

TC-auteurs kunnen metaprogrammering gebruiken om functies te bieden die generiek exemplaren van TC genereren, gegeven een bepaald type. Programmeurs kunnen speciale bibliotheek-API of de Scala-afleidende tools gebruiken om instanties voor hun code te maken.

Of u nu generieke of specifieke code nodig heeft om een โ€‹โ€‹TC te implementeren, voor elke situatie is er een oplossing. 

Samenvatting van alle voordelen

  • Het lost het expressieprobleem op
    • Nieuwe typen kunnen bestaand gedrag implementeren via traditionele overerving van eigenschappen
    • Nieuw gedrag kan worden geรฏmplementeerd op bestaande typen
  • Scheiding van zorg
    • De code is niet verminkt en gemakkelijk te verwijderen. Een TC scheidt data en gedrag, wat een functioneel programmeermotto is.
  • Het is veilig
    • Het is typeveilig omdat het niet afhankelijk is van introspectie. Het vermijdt grote patroonafstemming met typen. als u merkt dat u dergelijke code schrijft, kunt u een geval ontdekken waarin het TC-patroon perfect past.
    • Het impliciete mechanisme is compileerveilig! Als een instantie ontbreekt tijdens het compileren, zal de code niet compileren. Geen verrassing tijdens de runtime.
  • Het brengt ad-hoc polymorfisme met zich mee
    • Ad-hocpolymorfisme ontbreekt meestal in traditioneel objectgeoriรซnteerd programmeren.
    • Met ad-hoc polymorfisme kunnen ontwikkelaars hetzelfde gedrag implementeren voor verschillende niet-gerelateerde typen zonder gebruik te maken van traditionele subtypering (die de code koppelt)
  • Afhankelijkheidsinjectie gemakkelijk gemaakt
    • Een TC-instantie kan worden gewijzigd met inachtneming van het Liskov-substitutieprincipe. 
    • Wanneer een component afhankelijk is van een TC, kan voor testdoeleinden eenvoudig een nagebootste TC worden geรฏnjecteerd. 

Teller indicaties

Elke hamer is ontworpen voor een reeks problemen.

Typeklassen zijn bedoeld voor gedragsproblemen en mogen niet worden gebruikt voor gegevensovererving. Gebruik daarvoor compositie.

De gebruikelijke subtypering is eenvoudiger. Als u eigenaar bent van de codebasis en niet streeft naar uitbreidbaarheid, kunnen typeklassen overdreven zijn.

In de Scala-kern is er bijvoorbeeld een `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

Het is echt zinvol om zo'n typeklasse te gebruiken, omdat het niet alleen hergebruik van algebraรฏsche algoritmen mogelijk maakt op typen die zijn ingebed in Scala (Int, BigInt, ...), maar ook op door de gebruiker gedefinieerde typen (a `ComplexNumber'bijvoorbeeld).

Aan de andere kant maakt de implementatie van Scala-collecties meestal gebruik van subtypering in plaats van typeklasse. Dit ontwerp is om verschillende redenen zinvol:

  • De verzameling-API zou compleet en stabiel moeten zijn. Het legt gemeenschappelijk gedrag bloot via eigenschappen die door implementaties worden geรซrfd. Zeer uitbreidbaar zijn is hier geen specifiek doel.
  • Het moet eenvoudig te gebruiken zijn. TC voegt een mentale overhead toe aan de programmeur van de eindgebruiker.
  • TC kan ook een kleine overhead in de prestaties met zich meebrengen. Dit kan van cruciaal belang zijn voor een verzamelings-API.
  • De collectie-API is echter nog steeds uitbreidbaar via nieuwe TC die is gedefinieerd door bibliotheken van derden.

Conclusie

We hebben gezien dat TC een eenvoudig patroon is dat een groot probleem oplost. Dankzij de Scala-rijke syntaxis kan het TC-patroon op vele manieren worden geรฏmplementeerd en gebruikt. Het TC-patroon komt overeen met het functionele programmeerparadigma en is een fantastisch hulpmiddel voor een schone architectuur. Er is geen wondermiddel en het TC-patroon moet worden toegepast wanneer het past.

Ik hoop dat je kennis hebt opgedaan door dit document te lezen. 

Code is verkrijgbaar op https://github.com/jprudent/type-class-article. Neem gerust contact met mij op als u vragen of opmerkingen heeft. U kunt desgewenst problemen of codeopmerkingen in de repository gebruiken.


Jeroen PRUDENT

Software Engineer

Tijdstempel:

Meer van Grootboek