5 trin til at oprette din allerførste Type Klasse i Scala

I dette blogindlæg lærer du, hvordan du implementerer din første type klasse, som er grundlæggende sprogfunktion i ikonet for de funktionelle programmeringssprog - Haskell.

Foto af Stanley Dai på Unsplash

Type Class er et mønster, der stammer fra Haskell, og det er dets standard måde at implementere polymorfisme på. Denne type polymorfisme kaldes ad-hoc polymorfisme. Dets navn stammer fra det faktum, at i modsætning til velkendt polyporfistisk subtypeindtastning, kan vi udvide bibliotekets funktionalitet, selv uden at have adgang til kildekoden til biblioteket og klassen, hvilken funktionalitet vi ønsker at udvide til.

I dette indlæg vil du se, at brug af typeklasser kan være så praktisk som at bruge almindelig OOP-polymorfisme. Indhold nedenfor fører dig gennem alle faser med implementering af Type Class-mønster for at hjælpe dig med at få bedre forståelse af internals i funktionelle programmeringsbiblioteker.

Oprettelse af din første Type Class

Teknisk er Type Class bare en parametriseret egenskab med antal abstrakte metoder, der kan implementeres i klasser, der udvider denne egenskab. Så vidt alt ser virkelig ud i en velkendt undertypemodel.
Den eneste forskel er, at vi ved hjælp af undertypning er nødt til at implementere kontrakt i klasser, der er et stykke domænemodel, i Type klasser er implementering af egenskab placeret i en helt anden klasse, der er knyttet til "domæneklasse" efter typeparameter.

Som et eksempel i denne artikel vil jeg bruge Eq Type Class fra Cats-biblioteket.

træk Eq [A] {
  def erekvivalenter (a: A, b: A): Boolsk
}

Type klasse Eq [A] er en kontrakt om at have mulighed for at kontrollere, om to objekter af type A er ens baseret på nogle kriterier implementeret i areEquals-metoden.

Oprettelse af forekomst af vores Type-klasse er så simpelt som genoptagelsesklasse, der udvider den nævnte egenskab med kun en forskel, at vores type-klasse-forekomst vil være tilgængelig som implicit objekt.

def moduloEq (divisor: Int): Eq [Int] = ny Eq [Int] {
 tilsidesætte def erEquals (a: Int, b: Int) = a% divisor == b% divisor
}
implicit val modulo5Eq: Eq [Int] = moduloEq (5)

Ovenfor koden kan komprimeres lidt i en følgende form.

def moduloEq: Eq [Int] = (a: Int, b: Int) => a% 5 == b% 5

Men vent, hvordan kan du tildele funktion (Int, Int) => Boolsk til reference med type Eq [Int] ?! Denne ting er muligt takket være Java 8-funktionen kaldet Single Abstract Method type interface. Vi kan gøre sådan noget, når vi kun har en abstrakt metode i vores egenskab.

Type klasse opløsning

I dette afsnit viser jeg dig, hvordan du bruger typeklasse-forekomster, og hvordan man magisk sammenbinder type klasse Eq [A] med tilsvarende objekt af type A, når det er nødvendigt.

Her har vi implementeret funktionalitet til sammenligning af to Int-værdier ved at kontrollere, om deres modulopdelingsværdier er ens. Med alt dette arbejde er vi i stand til at bruge vores Type Class til at udføre en vis forretningslogik, f.eks. vi ønsker at parre to værdier, der er modulo lige.

def pairEquals [A] (a: A, b: A) (implicit eq: Eq [A]): ​​Option [(A, A)] = {
 hvis (eq.areEquals (a, b)) Nogle ((a, b)) andet Ingen
}

Vi har parametreret funktionsparekvivalenter til at arbejde med alle typer, der leverer forekomst af klasse Eq [A], der er tilgængelig i dets implicit omfang.

Når compiler ikke finder nogen instans, der matcher ovennævnte erklæring, ender det med en advarsel om kompilationsfejl om mangel på korrekt instans i forudsat implicit rækkevidde.
  1. Compiler vil udlede typen af ​​leverede parametre ved at anvende argumenter på vores funktion og tildele den til alias A.
  2. Forudgående argument eq: Eq [A] med implicit nøgleord udløser forslag om at lede efter objekt af type Eq [A] i implicit rækkevidde.

Takket være implikationer og indtastede parametre er compiler i stand til at binde klasse sammen med den tilhørende typeklasseinstans.

Alle forekomster og funktioner er defineret, lad os kontrollere, om vores kode giver gyldige resultater

pairEquals (2,7)
res0: Option [(Int, Int)] = Nogle ((2,7))
pairEquals (2,3)
res0: Option [(Int, Int)] = Ingen

Som du ser, modtog vi forventede resultater, så vores type klasse klarer sig godt. Men denne ser lidt rodet ud med en ret mængde kedelplade. Takket være magien med Scalas syntaks kan vi få en kedelplade til at forsvinde.

Kontektsgrænser

Den første ting, jeg vil forbedre vores kode, er at slippe af med den anden argumentliste (det med implicit nøgleord). Vi passerer ikke direkte den ene, når vi påkalder funktion, så lad implicit være implicit igen. I Scala kan implicitte argumenter med typeparametere erstattes af sprogkonstruktion kaldet Context Bound.

Context Bound er deklaration i listen med typeparametre, hvilken syntaks A: Eq siger, at enhver type, der bruges som argument for pairEquals-funktion, skal have en implicit værdi af typen Eq [A] i det implicitte omfang.

def pairEquals [A: Eq] (a: A, b: A): Option [(A, A)] = {
 hvis (implicit [Æ [A]]. er Kvinder (a, b)) Nogle ((a, b)) ellers Ingen
}

Som du har bemærket, endte vi med at der ikke var nogen henvisning til den implicitte værdi. For at overvinde dette problem bruger vi implicit funktion [F [_]], der trækker fundet implicit værdi ved at specificere, hvilken type vi refererer til.

Dette er, hvad Scala-sprog tilbyder os for at gøre det hele mere kortfattet. Det ser dog stadig ikke godt ud for mig. Context Bound er et rigtig coolt syntaktisk sukker, men dette synes implicit at forurene vores kode. Jeg laver et dejligt trick, hvordan man overvinder dette problem og mindsker vores implementeringsdosering.

Hvad vi kan gøre, er at levere parameteriseret anvendelsesfunktion i ledsagerobjekt af vores typeklasse.

objekt Eq {
 def anvende [A] (implicit eq: Eq [A]): ​​Eq [A] = ækv
}

Denne virkelig enkle ting giver os mulighed for at slippe af med implicit og trække vores eksempel fra limbo til brug i domænelogik uden kedelplade.

def pairEquals [A: Eq] (a: A, b: A): Option [(A, A)] = {
 if (Eq [A] .arequals (a, b)) Nogle ((a, b)) andet Ingen
}

Implicitte konverteringer - alias. Syntaks modul

Den næste ting, jeg vil komme ind på min arbejdsbænk, er Eq [A] .areEquals (a, b). Denne syntaks ser meget ordret ud, fordi vi eksplicit refererer til type klasseinstans, som burde være implicit, ikke? Den anden ting er, at vores type klasseinstans her fungerer som Service (i DDD-betydning) i stedet for ægte A-klasseudvidelse. Heldigvis kan man også fikse det ved hjælp af en anden nyttig brug af implicit nøgleord.

Hvad vi vil gøre her, er at tilbyde såkaldt syntaks eller (ops som i nogle FP-biblioteker) modul ved hjælp af implicitte konverteringer, som giver os mulighed for at udvide API for en eller anden klasse uden at ændre dens kildekode.

implicit klasse EqSyntax [A: Eq] (a: A) {
 def === (b: A): Boolean = Æg [A] .areEquals (a, b)
}

Denne kode fortæller kompilatoren om at konvertere klasse A med forekomst af type klasse Eq [A] til klasse EqSyntax, der har en funktion ===. Alt dette gør indtryk af, at vi har tilføjet funktion === til klasse A uden kildekodemodifikation.

Vi har ikke kun skjult type klasseinstanshenvisning, men giver også flere klassesyntaxer, der gør indtryk af metode === implementeret i klasse A, selv vi ved ikke noget om denne klasse. To fugle dræbt med en sten.

Nu har vi lov til at anvende metode === til type A, når vi har EqSyntax-klasse i omfang. Nu vil vores implementering af pairEquals ændre sig lidt og vil være som følger.

def pairEquals [A: Eq] (a: A, b: A): Option [(A, A)] = {
 hvis (a === b) Nogle ((a, b)) ellers Ingen
}

Som jeg lovede, har vi endt med implementering, hvor den eneste synlige forskel sammenlignet med OOP-implementering er Context Bound-annotation efter en type-parameter. Alle tekniske aspekter af typen klasse er adskilt fra vores domænelogik. Det betyder, at du kan opnå langt mere seje ting (som jeg vil nævne i den separate artikel, hvad der snart vil blive offentliggjort) uden at skade din kode.

Implicit rækkevidde

Som du ser, er typeklasser i Scala strengt afhængige af at bruge implicit funktion, så det er vigtigt at forstå, hvordan man arbejder med implicit rækkevidde.

Implicit scope er et omfang, hvor compiler søger efter implicitte tilfælde. Der er mange valgmuligheder, så der var behov for at definere en rækkefølge, hvor der søges efter tilfælde. Ordren er som følger:

1. Lokale og nedarvede tilfælde
2. Importerede tilfælde
3. Definitioner fra ledsagerobjektet af typen klasse eller parametrene

Det er så vigtigt, fordi når compiler finder flere tilfælde eller ikke tilfælde overhovedet, vil det give en fejl. For mig er den mest bekvemme måde at få forekomster af typeklasser at placere dem i ledsagerobjektet af selve typeklassen. Takket være det behøver vi ikke at genere os selv med at importere eller implementere steder, der giver os mulighed for at glemme placeringsproblemer. Alt leveres magisk af kompilatoren.

Så lad os diskutere punkt 3 ved hjælp af et eksempel på velkendt funktion fra Scalas standardbibliotek sorteret hvilken funktionalitet er baseret på implicit leverede komparatorer.

sorteret [B>: A] (implicit ord: matematik.Ordering [B]): Liste [A]

Type klasseinstans søges i:
 * Bestilling af ledsagerobjekt
 * Liste ledsager objekt
 * B-ledsagerobjekt (hvilket også kan være et ledsagerobjekt på grund af eksistensen af ​​definition af lavere grænser)

simulakrum

Alt dette hjælper meget, når man bruger typeklasse, men det er gentagne arbejde, der skal udføres i hvert projekt. Disse spor er et åbenlyst tegn på, at processen kan udvindes til biblioteket. Der er et fremragende makrobaseret bibliotek kaldet Simulacrum, som håndterer alt, hvad der er nødvendigt for at generere syntaksmodul (kaldet ops i Simulacrum) osv. For hånd.

Den eneste ændring, vi skal indføre, er @typeclass-annotationen, som er mærket for makroer til at udvide vores syntaksmodul.

import simulacrum._
@typeklasseegenskab Eq [A] {
 @op (“===”) def er Kvaliteter (a: A, b: A): Boolsk
}

De andre dele af vores implementering kræver ingen ændringer. Det er alt. Nu ved du, hvordan du implementerer type klassen mønster i Scala på egen hånd, og jeg håber, at du fik opmærksomhed på, hvordan biblioteker, når Simulacrum fungerer.

Tak for at have læst, jeg vil virkelig sætte pris på enhver feedback fra dig, og jeg ser frem til at møde dig i fremtiden med en anden offentliggjort artikel.