een inleiding tot typeklassen in Scala

  • Part 1: This post
  • Part 2: Type-Safe serialization in Scala
  • Part 3: Typefoutafhandeling met hogere typen

Wat is een typeklasse

een van de grootste belemmeringen voor het begrijpen en gebruiken van typeklassen in uw eigen code is de naam zelf. In tegenstelling tot wat de naam oproept, is een type class geen klasse uit een objectgeoriënteerde taal. Volgens wikipedia:

in de informatica is een type klasse Een type systeemconstructie die ad hoc polymorfisme ondersteunt. Dit wordt bereikt door beperkingen toe te voegen aan typevariabelen in parametrisch polymorfe types.

We moeten nog wat uitpakken:

  • Wat is’ ad hoc ‘ polymorfisme? Wat het ook is, dat is wat type klassen ons zullen helpen bereiken.
  • Wat is een ‘parametrisch polymorf’ type? Wat het ook is, zo gaan we onze eigen type klassen creëren.

Typeklassen verschenen voor het eerst in Haskell en zijn een fundamentele manier om abstracties op te bouwen in Haskell. Ze zijn zo belangrijk dat ze worden geïntroduceerd in de eerste pagina van het Learn You a Haskell boek:

een type class is een soort interface die een bepaald gedrag definieert. Als een type deel uitmaakt van een typeklasse, betekent dit dat het het gedrag ondersteunt en implementeert dat de typeklasse beschrijft.

Ok, dus het lijkt erop dat we proberen om wat gedrag voor onze types te definiëren, en implementeren dat gedrag anders afhankelijk van het type. Dat laatste deel, “implementeer het gedrag anders afhankelijk van het type” is het ‘polymorfisme’ bit van eerder. Dus de term begint logisch te worden: het is een klasse voor types. Een klasse die het gedrag dat door de type klasse wordt aangewezen implementeert wordt gezegd een lid van die klasse van types te zijn — ’type klasse’.

op dit moment zou het gepast zijn om te zeggen dat dit klinkt als overerving. Ze stellen ons beiden in staat abstracties op te bouwen en zorgen voor polymorfisme.

dus waarom typeklassen überhaupt verkennen? In veel gevallen zijn ze een stuk krachtiger en uitbreidbaar dan een overerving hiërarchie. Ze kunnen veel eenvoudiger, en zijn een veel betere pasvorm wanneer uw codebase al leunt FP over OO. Hoewel zelfs in een sterk OO project, type klassen kunnen nog steeds de dag te redden.

het definiëren van een typeclass

dat is genoeg introductie — voor de rest van het artikel zullen we een stuk van zeer bekende functionaliteit verkennen, die daadwerkelijk in Java is geïmplementeerd via overerving, en het implementeren met behulp van een type class: stringification.

We zijn allemaal bekend met de methode die bestaat op alle objecten in Java (en dus Scala): .toString. Het doel is om een string representatie van het object in kwestie te produceren. Laten we eens kijken hoe we type klassen gebruiken om dit te bereiken. Haskell ‘ s versie van toString wordt Show genoemd, dus we houden het ruwweg bij die naam:

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

dat is het: er is onze Showable type klasse. Waarom neemt het een type parameter A? Als we onze eerste definitie van type klassen herinneren, lezen we dat ” bereikt door beperkingen toe te voegen aan type variabelen in parametrisch polymorfe types.”Showable is ons parametrisch polymorfe type, en A is onze typevariabele, wat simpelweg betekent dat het een type (A) als typeparameter neemt. (Merk op dat we geen beperkingen hebben geplaatst op A, volgens de definitie, maar dat is in orde. Dat is iets wat we doen bij het samenstellen van type klassen samen, of gebruik te maken van hen, die later zal komen.)

het implementeren van een typeclass

nu moeten we implementaties van de Showable eigenschap leveren voor alle typen die we willen kunnen tonen. Deze implementaties worden “instanties” van onze type klasse genoemd, omdat ze letterlijk instanties van de eigenschap Showablezullen zijn. Dit zullen meestal anonieme klassen zijn die .show overschrijven, en aangezien er een standaardtekst is die betrokken is bij het definiëren ervan (herinner je je ‘function objects’ in pre-lambda Java programming?), definiëren we een helper genaamd make. Laten we beginnen met een implementatie voor een gebruikersnaam type:

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

dus hoe zouden we dit aanroepen? We kunnen de methode direct bellen vanuit de showable instantie, zoals zo:

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

verbeterde syntaxis

we zijn erin geslaagd om onze Showable implementatie van UserName aan te roepen, maar het was niet erg aardig. We moesten een verwijzing hebben naar onze Type class instantie rond (defaultUserNameShowable) en de naam ervan kennen. Dit is iets dat we willen abstraheren. Als het verhaal hier eindigde, type klassen in Scala zou niet zo leuk zijn om te gebruiken. Gelukkig kunnen we de ergonomie drastisch verbeteren met behulp van implicits. We zullen Showable’s companion object vergroten met een aanroep helper:

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

door deze” extensiemethode ” ergens in het bereik te plaatsen, kunnen we gewoon doen alsof alle objecten een .show methode hebben.

wat heeft dit bereikt

dus waarom niet gewoon overschrijven toString op UserName in plaats? Nou, laten we zeggen dat UserName in een bibliotheek woont waar we van afhankelijk zijn, en al gecompileerd is tegen de tijd dat we het consumeren. We kunnen het niet onderklassen omdat het een final klasse is. Echter, met behulp van type klassen, we zijn vrij om in wezen “hechten” nieuw gedrag aan het op een ad hoc manier. Dit is het” ad hoc ” stuk van de bovenstaande definitie: we kunnen dit polymorfisme apart toevoegen van de plaats waar het type zelf is gedefinieerd.

bovendien, in tegenstelling tot overerving, wanneer we type klassen definiëren in Scala, zijn ze niet verplicht om ‘coherent’ te zijn, wat betekent dat we meerdere implementaties kunnen definiëren voor Showable, en er tussen kunnen kiezen. Voor sommige type klassen is dit een goede zaak, voor anderen niet zo veel.

in Haskell zijn typeklassen coherent. In Scala 3, volgens Martin Odersky, zullen we de mogelijkheid hebben om te kiezen, voor elke type klasse, of het coherent moet zijn of niet.

maar voor nu geven we een alternatieve betekenis van show voor UserName:

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

het enige wat we nu moeten doen is er zeker van zijn dat slechts één van onze Showable instanties tegelijk in scope is. En zo niet, het is niet een big deal, krijgen we een compilatie-tijd ‘dubbelzinnige impliciete waarden’ fout:

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

een belangrijke opmerking is dat we UserName en Showableniet hebben gekoppeld. Het enige wat we hebben gedaan is een Betekenis van Showable definiëren voor UserName, terwijl we ze volledig onafhankelijk van elkaar houden. Deze ontkoppeling heeft enorme maintainability implicaties, en helpt om verwarde afhankelijkheidsrelaties in uw code te voorkomen.

Compile-time safety

Wat gebeurt er als we proberen .show aan te roepen op een object waarvoor we geen betekenis van showhebben gegeven? Met andere woorden, wanneer er geen exemplaar van Showable in het bereik is voor onze doelstelling? We krijgen gewoon een compilerfout:

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

we kunnen deze foutmelding daadwerkelijk aanpassen wanneer we onze typeklasse definiëren:

@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

  • Type classes stellen ons in staat om een reeks gedragingen volledig gescheiden te definiëren van de objecten en typen die dit gedrag zullen implementeren.
  • Type klassen worden uitgedrukt in pure Scala met eigenschappen die type parameters nemen, en impliceert om syntaxis schoon te maken.
  • Type klassen staan ons toe om gedrag uit te breiden of te implementeren voor typen en objecten waarvan de bron niet kan worden gewijzigd. (Ze zorgen voor het ad hoc polymorfisme dat nodig is om het expressieprobleem op te lossen).

wat volgt

toegegeven, Showable is een triviale typeklasse, zij het niet zonder toepassingen. We kunnen ons gemakkelijk veel nuttiger voorstellen, echter:

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 zou een geweldig alternatief voor iets als Akka Serialization waar we wachten tot runtime om te zien of een serializer is geregistreerd voor ons type. Het is veel veiliger om je serialisatie implementatie impliciet door te geven tijdens het compileren!

Convertible zou een goede herbruikbare manier kunnen zijn om gegevens over verschillende typen te converteren. Er is een bibliotheek van Twitter met hetzelfde doel in gedachten genaamd bijectie.

Functor is een bouwsteen voor een aantal extreem krachtige abstracties in een gebied van functioneel programmeren dat ‘categorisch programmeren’wordt genoemd. Bibliotheken zoals ScalaZ en Cats zijn gebouwd op type klassen zoals Functor. Een belangrijk idee dat we deze keer niet hebben besproken is dat type klassen gemakkelijk kunnen worden samengesteld om steeds complexer gedrag op te bouwen uit eenvoudige bouwstenen zoals Functor.

daarnaast zijn er veel essentiële bibliotheken in het Scala-ecosysteem (Play JSON, Slick, et. al) gebruik maken van type klassen en bloot hen als de belangrijkste driver van hun API in veel gevallen (e.g.speel JSON ‘ s Format type klasse). Het begrijpen van type klassen zelf is een geweldige manier om effectiever te worden in het gebruik van wat er in de Scala ecosysteem, zelfs als je nooit een van je eigen te schrijven.

In de toekomst zal ik kijken naar enkele onmiddellijk praktische type klassen zoals SafeSerializable . Voor een inleiding tot de meer abstracte categorische type klassen zoals Functor] en Monad], kijk dan niet verder dan de cats bibliotheekdocumentatie . Voor een meer diepgaande gids voor programmeren in Scala met behulp van type klassen, de underscore.io heeft een fantastisch boek over het onderwerp: Scala met katten

deel 2: Typeveilige serialisatie in Scala

GIST met code

Geef een antwoord

Het e-mailadres wordt niet gepubliceerd.