en introduksjon til typeklasser I Scala

  • Del 1: dette innlegget
  • Del 2: Typesikker serialisering I Scala
  • Del 3: Typeful feilhåndtering med høyere typer

hva er en typeklasse

En av de største hindringene for å forstå og bruke typeklasser i din egen kode er navnet selv. I motsetning til hva navnet fremkaller, er en type klasse ikke en klasse fra et objektorientert språk. Ifølge wikipedia:

i informatikk er en type klasse en type systemkonstruksjon som støtter ad hoc polymorfisme. Dette oppnås ved å legge til begrensninger for å skrive variabler i parametrisk polymorfe typer.

Vi har litt utpakking å gjøre:

  • hva er ad hoc polymorfisme? Uansett hva det er, det er hva slags klasser skal hjelpe oss å oppnå.
  • Hva er en ‘parametrisk polymorf’ type? Uansett hva det er, det er hvordan vi skal lage våre egne type klasser.

Type klasser først dukket opp I Haskell, og er en grunnleggende måte man bygger opp abstraksjoner I Haskell. De er så viktige at de blir introdusert på første side Av Lær deg En haskell bok:

en type klasse er en slags grensesnitt som definerer noen oppførsel. Hvis en type er en del av en type klasse, betyr det at den støtter og implementerer virkemåten typen klassen beskriver.

Ok, Så det virker som om vi prøver å definere noen oppførsel for våre typer, og implementere den oppførselen forskjellig avhengig av typen. Den siste delen, «implementer oppførselen annerledes avhengig av typen» er «polymorfismen» – biten fra tidligere. Så begrepet begynner å gi mening: det er en klasse for typer. En klasse som implementerer virkemåten utpekt av typen klassen sies å være medlem av den klassen av typer – ‘type klasse’.

På dette punktet ville det være hensiktsmessig å si at dette høres ut som arv. De begge tillater oss å bygge opp abstraksjoner, og gi polymorfisme.

Så hvorfor utforske type klasser i det hele tatt? I mange tilfeller er de ganske mye kraftigere og utvidbare enn et arvshierarki. De kan være mye enklere, og er mye bedre egnet når kodebasen din allerede lener FP over OO. Selv om selv i et sterkt oo-prosjekt, kan typeklasser fortsatt redde dagen.

Definere en typeclass

det er nok introduksjon — for resten av artikkelen vil vi utforske et stykke veldig kjent funksjonalitet, som faktisk implementeres I Java via arv, og implementere den ved hjelp av en type klasse: stringification.

vi er alle kjent med metoden som finnes på alle objekter I Java (og dermed Scala): .toString. Hensikten er å produsere en strengrepresentasjon av objektet i spørsmålet. La oss se på hvordan vi ville bruke type klasser for å oppnå dette. Haskells versjon av toString kalles Show, så vi holder oss omtrent med det navnet:

trait Showable {
def show(a: A): String
}

Det er det: det er vår Showable type klasse. Hvorfor tar det en type parameter A? Hvis vi husker vår første definisjon av typeklasser, leser vi at » oppnådd ved å legge til begrensninger for å skrive variabler i parametrisk polymorfe typer.»Showable er vår parametrisk polymorfe type, Og A er vår typevariabel, som ganske enkelt betyr at den tar en type (A) som en typeparameter. (Merk at vi ikke satte noen begrensninger på A, i henhold til definisjonen, men det er greit. Det er noe vi gjør når vi komponerer type klasser sammen, eller bruker dem, som kommer senere.)

Implementere en typeclass

nå må Vi gi implementeringer av Showable egenskap for alle typer som vi ønsker å kunne vise. Disse implementeringene kalles «forekomster» av vår type klasse, siden de bokstavelig talt vil være forekomster av egenskapen Showable. Disse vil oftest være anonyme klasser som overstyrer .show, og siden det er noen boilerplate involvert i å definere dem (husk ‘funksjonsobjekter’ i pre-lambda Java programmering?), vil vi definere en hjelper kalt make. La oss starte med en implementering For En Brukernavntype:

object Showable {
def make(showFn: A => String): Showable = new Showable {
override def show(a: A): String = showFn(a)
}
}final case class UserName(first: String, last: String)implicit val defaultUserNameShowable = Showable.make(userName => s"${userName.first} ${userName.last}")val janeName = UserName("Jane", "Doe")

så hvordan skal vi påkalle dette? Vi kunne direkte ringe metoden fra den visbare forekomsten, slik som:

defaultUserNameShowable.show(janeName) // "Jane Doe"

Forbedret Syntaks

vi har klart å påkalle vår Showable implementering av UserName, men Det var ikke veldig fint. Vi måtte ha en referanse til vår type klasse forekomst rundt (defaultUserNameShowable) og vet navnet sitt. Dette er noe vi ønsker abstrahert bort. Hvis historien endte her, ville type klasser I Scala ikke være så hyggelig å bruke. Heldigvis kan vi forbedre ergonomien drastisk ved hjelp av implicits. Vi vil øke Showable‘s companion object med en invocation helper:

implicit class ShowSyntax(a: A) extends AnyVal {
def show(implicit showable: Showable): String = showable.show(a)
}
...
janeName.show

ved å plassere denne «utvidelsesmetoden» et sted i omfang, kan vi bare late som om alle objekter har en .show – metode.

Hva har dette oppnådd

så hvorfor ikke bare overstyre toStringUserName i stedet? Vel, la oss si UserName bor i et bibliotek vi er avhengige av, og er allerede kompilert av tiden vi bruker det. Vi kan ikke underklasse det som det er en final klasse. Men ved hjelp av typeklasser er vi fri til å «knytte» ny oppførsel til den på en ad hoc-måte. Dette er «ad hoc» – delen av definisjonen ovenfor: vi kan legge til denne polymorfismen separat fra stedet der selve typen er definert.

i tillegg, i motsetning til arv, når vi definerer typeklasser I Scala, er de ikke pålagt å være ‘sammenhengende’, noe som betyr at vi kan definere flere implementeringer for Showable, og velge mellom dem. For noen type klasser er dette en god ting, for andre ikke så mye.

i Haskell er typeklasser sammenhengende. I Scala 3, Ifølge Martin Odersky, vil vi ha muligheten til å velge, for hver type klasse, om det skal være sammenhengende eller ikke.

men for nå, la oss gi en alternativ betydning av show for UserName:

implicit val secretUserNameShowable = Showable.make(_ => "<secret person>")

Alt vi trenger å gjøre nå er å være sikker på at bare en av våre Showable forekomster er i omfang om gangen. Og hvis ikke, det er ikke en stor avtale, vi får en kompilere tid ‘tvetydige implisitte verdier’ feil:

janeName.show
> ambiguous implicit values:
both value secretUserNameShowable in object typeclasses of type => Showable
and value defaultUserNameShowable in object typeclasses of type => Showable
match expected type Showable

Et viktig notat er at vi ikke har koblet UserName og Showable. Alt vi har gjort er å definere en mening av Showable for UserName, samtidig som de holder dem helt uavhengige av hverandre. Denne avkoblingen har store vedlikeholdsimplikasjoner, og bidrar til å forhindre sammenflettede avhengighetsforhold i hele koden din.

Kompileringstid sikkerhet

hva skjer hvis vi prøver å ringe .show på et objekt som vi ikke har gitt en betydning for show? Med andre ord, når det ikke er noen forekomst av Showable i omfang for vårt mål? Vi får bare en kompilatorfeil:

java.util.UUID.randomUUID().show
> could not find implicit value for parameter showable: Showable

Vi kan faktisk tilpasse denne feilmeldingen når vi definerer vår type klasse:

@scala.annotation.implicitNotFound("No Showable in scope for A = ${A}. Try importing or defining one.")
trait Showable {
...
}
...
janeName.show
> No Showable in scope for A = java.util.UUID. Try importing or defining one.

Key Takeaways

  • Typeklasser lar Oss definere et sett med atferd helt separat fra objektene og typene som vil implementere disse atferdene.
  • Typeklasser uttrykkes i ren Scala med trekk som tar typeparametere, og implisitter for å gjøre syntaksen ren.
  • Typeklasser tillater oss å utvide eller implementere atferd for typer og objekter hvis kilde vi ikke kan endre. (De gir den ad hoc polymorfismen som er nødvendig for å løse uttrykksproblemet).

Hva kommer neste

Gitt, Showable er en triviell type klasse, men ikke uten bruk. Vi kan lett forestille seg langt mer nyttige, derimot:

trait SafeSerializable {
def serialize(a: A): Array
def deserialize(bytes: Array): A
}
trait Convertible {
def to(a: A): B
def from(b: B): A
}
trait Functor] {
def map(a: F, f: A => B): F
}

SafeSerializable kan være et godt alternativ til Noe Som Akka Serialisering der vi venter til kjøretid for å se om en serializer er registrert for vår type. Det er mye tryggere å implisitt passere Serialiseringsimplementeringen din på kompileringstid!

Convertible kan være en flott gjenbrukbar måte å konvertere data på tvers av typer. Det er Et bibliotek Av Twitter med samme mål i tankene kalt Bijection.

Functor er en byggestein for noen ekstremt kraftige abstraksjoner i et område med funksjonell programmering kalt ‘kategorisk programmering’. Biblioteker som ScalaZ og Cats er bygget på type klasser som Functor. En viktig forestilling om at vi ikke diskuterte denne gangen er at type klasser lett kan komponeres for å bygge opp stadig mer kompleks oppførsel fra enkle byggeklosser som Functor.

i tillegg er mange viktige biblioteker i Scala-økosystemet (Play JSON, Slick, et. al) gjør bruk av type klasser og eksponere dem som hoveddriveren AV DERES API i mange tilfeller (e.G. Spill JSONS Format type klasse). Å forstå type klasser selv er en fin måte å bli mer effektiv på å gjøre bruk av Det som er der ute i Scala økosystemet, selv om du aldri skriver noen av dine egne.

I fremtiden vil jeg se på noen umiddelbart praktiske type klasser som SafeSerializable . For en introduksjon til de mer abstrakte kategoriske typeklassene som Functor] og Monad], se ikke lenger enn cats bibliotekdokumentasjonen . For en mer grundig guide til programmering I Scala ved hjelp av typeklasser, underscore.io har en fantastisk bok om emnet: Scala Med Katter

Del 2: Typesikker serialisering i Scala

GIST med kode

Legg igjen en kommentar

Din e-postadresse vil ikke bli publisert.