johdatus typeclasses in Scala

  • Osa 1: Tämä viesti
  • Osa 2: Tyyppiturvallinen sarjallistaminen Scalassa
  • Osa 3: Tyyppivirheiden käsittely korkeampikuntoisilla tyypeillä

mikä on tyyppiluokka

yksi suurimmista esteistä tyyppiluokkien ymmärtämiselle ja käyttämiselle omassa koodissa on nimi itse. Toisin kuin nimi antaa ymmärtää, tyyppiluokka ei ole oliokeskeisen kielen luokka. Wikipedian mukaan:

tietotekniikassa tyyppiluokka on tyyppijärjestelmän konstruktio, joka tukee ad hoc-polymorfismia. Tämä saavutetaan lisäämällä rajoitteita tyyppimuuttujiin parametrisesti polymorfisissa tyypeissä.

meillä on purettavaa:

  • mikä on ad hoc-polymorfismi? Mitä se onkaan, sen tyyppiluokat auttavat meitä saavuttamaan.
  • mikä on ”parametrisesti polymorfinen” tyyppi? Mitä se onkaan, niin me luomme omat tyyppiluokat.

Tyyppiluokat esiintyivät ensimmäisen kerran Haskellissa, ja ne ovat perustava tapa rakentaa haskelliin abstraktioita. Ne ovat niin tärkeitä, että ne esitellään ensimmäisen sivun Learn You a Haskell kirja:

tyyppiluokka on eräänlainen käyttöliittymä, joka määrittelee jonkin käyttäytymisen. Jos tyyppi on tyyppiluokan osa, se tarkoittaa, että se tukee ja toteuttaa tyyppiluokan kuvaamaa käyttäytymistä.

Ok, joten näyttää siltä, että yritämme määritellä jotain käyttäytymistä tyypeillemme, ja toteuttaa sitä käyttäytymistä eri tavalla tyypistä riippuen. Että viimeinen osa, ”toteuttaa käyttäytymistä eri tavalla tyypistä riippuen” on ’polymorphism’ bitti aiemmasta. Termissä alkaa siis olla järkeä: se on tyyppien Luokka. Luokan, joka toteuttaa tyyppiluokan määräämän käyttäytymisen, sanotaan olevan kyseisen tyyppiluokan jäsen — ”tyyppiluokka”.

tässä vaiheessa olisi sopivaa sanoa, että tämä kuulostaa periytymiseltä. Ne molemmat antavat meille mahdollisuuden rakentaa abstraktioita ja tarjota polymorfismia.

joten miksi tutkia tyyppiluokkia ollenkaan? Monissa tapauksissa ne ovat aika paljon tehokkaampia ja laajennettavissa kuin perintöhierarkia. Ne voivat olla paljon yksinkertaisempia ja sopivat paljon paremmin, kun codebase jo nojaa FP yli OO. Tosin vahvasti OO-projektissakin tyyppiluokat voivat vielä pelastaa päivän.

tyyppiluokan määrittely

tuo riittää esittelyksi-artikkelin loppuosassa tutustutaan hyvin tuttuun toiminnallisuuteen, joka on itse asiassa toteutettu Javassa periytyvyyden kautta, ja toteutetaan tyyppiluokalla: stringification.

me kaikki tunnemme menetelmän, joka esiintyy kaikilla Jaavan (ja siten Scalan) olioilla: .toString. Sen tarkoituksena on tuottaa merkkijonoesitys kyseisestä kohteesta. Katsotaanpa, miten käyttäisimme tyyppiluokkia tämän saavuttamiseksi. Haskellin versio toString on nimeltään Show, joten pysymme suurin piirtein sillä nimellä:

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

siinä se: siinä on meidän Showable tyyppiluokka. Miksi tarvitaan tyyppiparametri A? Jos muistamme ensimmäisen tyyppiluokkien määritelmän, luemme, että ” saavutettiin lisäämällä rajoitteita tyyppimuuttujiin parametrisesti polymorfisissa tyypeissä.”Showable on parametrisesti polymorfinen tyyppimme, ja A on tyyppimuuttujamme, mikä tarkoittaa yksinkertaisesti sitä, että se ottaa tyypin (A) tyyppiparametriksi. (Huomaa, että emme asettaneet mitään rajoitteita A: lle määritelmän mukaisesti, mutta ei se mitään. Se on jotain teemme, kun sävellämme tyyppiluokkia yhdessä, tai hyödyntämällä niitä, jotka tulevat myöhemmin.)

konstruointi

nyt on annettava toteutuksia Showable piirteestä kaikille tyypeille, joita haluamme pystyä osoittamaan. Näitä toteutuksia kutsutaan tyyppiluokkamme ”instansseiksi”, koska ne ovat kirjaimellisesti ominaisuuden ilmentymiä Showable. Nämä ovat useimmiten anonyymejä luokkia, jotka ohittavat .show, ja koska niiden määrittelyssä on mukana jokin boilerplate (Muistatko ”funktioobjektit” lambda-Java-ohjelmoinnissa?), määrittelemme auttajan nimeltä make. Aloitetaan käyttäjätunnuksen tyypin toteuttamisesta:

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

miten vetoamme tähän? Voisimme suoraan soittaa menetelmä näyttävä esimerkiksi, kuten niin:

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

parannettu syntaksi

olemme onnistuneet vetoamaan Showable toteutukseen UserName, mutta se ei ollut kovin mukavaa. Meillä piti olla viittaus meidän tyyppiluokkayksityiskohtaan ympärillä (defaultUserNameShowable) ja tietää sen nimi. Tämä on asia, jonka haluamme abstrahoida pois. Jos tarina päättyisi tähän, Scalan tyyppikurssit eivät olisi kovin mukavia käyttää. Onneksi voimme parantaa ergonomiaa rajusti implisiittisesti. We ’ll augment Showable’ s companion object with an invocation helper:

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

sijoittamalla tämän ”laajennusmenetelmän” jonnekin laajuuteen voimme yksinkertaisesti teeskennellä, että kaikilla olioilla on .show – menetelmä.

mitä tällä on saavutettu

joten miksi ei vain ohitettaisi toString UserName sen sijaan? No, sanotaan UserName asuu kirjastossa, josta olemme riippuvaisia, ja on jo koottu sen kuluttaman ajan mukaan. Emme voi alittaa sitä, koska se on final luokka. Kuitenkin käyttämällä tyyppiluokkia, olemme vapaita olennaisesti ”liittämään” uutta käyttäytymistä siihen ad hoc-tavalla. Tämä on edellä olevan määritelmän ”ad hoc” – osa: voimme lisätä tämän polymorfismin erikseen paikasta, jossa itse tyyppi on määritelty.

lisäksi, toisin kuin periytyvyydessä, kun määrittelemme Scalassa tyyppiluokkia, niiden ei tarvitse olla ”johdonmukaisia”, eli voimme määritellä useita toteutuksia Showable: lle ja valita niiden välillä. Joillekin tyyppiluokille tämä on hyvä asia, toisille ei niinkään.

haskellissa tyyppiluokat ovat koherentteja. Scala 3: ssa meillä on Martin Oderskyn mukaan mahdollisuus valita jokaiselle tyyppiluokalle, pitäisikö sen olla johdonmukainen vai ei.

mutta nyt, annetaan vaihtoehtoinen merkitys show UserName:

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

nyt täytyy vain varmistua siitä, että vain yksi Showable – instansseistamme on laajuudessa kerrallaan. Ja jos ei, se ei ole iso juttu, saamme kääntää-aika ’epäselvä implisiittisiä arvoja’ virhe:

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

yksi tärkeä huomio on, että emme ole yhdistäneet UserName ja Showable. Olemme vain määritelleet Showable: n merkityksen UserName: lle pitäen ne täysin toisistaan riippumattomina. Tällä irtikytkennällä on valtava ylläpidettävyysvaikutus, ja se auttaa ehkäisemään sotkuisia riippuvuussuhteita koko koodisi ajan.

Compile-time safety

mitä tapahtuu, jos yritämme soittaa .show kohteeseen, jolle emme ole antaneet showmerkitystä? Toisin sanoen, kun ei ole esiintymää Showable tavoitettamme varten? Saamme yksinkertaisesti kääntäjän virhe:

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

voimme itse muokata tätä virheilmoitusta, kun määrittelemme tyyppiluokan:

@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

  • Tyyppiluokat antavat meille mahdollisuuden määritellä joukon käyttäytymismalleja täysin erillään niistä esineistä ja tyypeistä, jotka toteuttavat näitä käyttäytymismalleja.
  • Tyyppiluokat ilmaistaan puhtaassa Scalassa ominaisuuksilla, jotka ottavat tyyppiparametreja, ja implisiittisesti tekevät syntaksista puhtaan.
  • Tyyppiluokat antavat meille mahdollisuuden laajentaa tai toteuttaa käyttäytymistä tyypeille ja olioille, joiden lähdettä emme voi muokata. (Ne tarjoavat ad hoc polymorfismi tarpeen ratkaista lauseke ongelma).

What comes next

Granted, Showable on triviaali tyyppiluokka, joskaan ei vailla käyttötarkoituksia. Voimme kuitenkin helposti kuvitella paljon hyödyllisempiä:

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 voisi olla hyvä vaihtoehto jotain Akka Serialization jossa odotamme runtime nähdä, onko serializer on rekisteröity meidän Tyyppi. On paljon turvallisempaa implisiittisesti siirtää Sarjallistamisen täytäntöönpanoa käännösaikaan!

Convertible voisi olla hyvä uudelleenkäytettävä tapa muuntaa dataa eri tyyppien välillä. Twitterissä on samaan päämäärään tähtäävä kirjasto nimeltään bijektio.

Functor on rakennuspalikka joillekin erittäin voimakkaille abstraktioille funktionaalisen ohjelmoinnin alueella, jota kutsutaan ”kategoriseksi ohjelmoinniksi”. Kirjastot kuten ScalaZ ja Cats rakentuvat tyyppiluokkien, kuten Functor, varaan. Yksi tärkeä ajatus, josta emme tällä kertaa keskustelleet, on se, että tyyppiluokat voidaan helposti koostaa rakentamaan yhä monimutkaisempaa käyttäytymistä yksinkertaisista rakennuspalikoista, kuten Functor.

lisäksi Scala-ekosysteemin monet keskeiset kirjastot (Play JSON, Slick, et. al) hyödyntää tyyppiluokkia ja altistaa ne API: n tärkeimmäksi ajuriksi monissa tapauksissa (e.G. Pelaa JSONin Format type class). Tyyppiluokkien ymmärtäminen itse on hyvä tapa tulla tehokkaammaksi Scala-ekosysteemin hyödyntämisessä, vaikka et koskaan kirjoittaisi mitään omaa.

tulevaisuudessa katselen joitakin välittömästi käytännöllisiä tyyppiluokkia, kuten SafeSerializable . Johdatus abstraktimpiin kategorisiin tyyppiluokkiin, kuten Functor] ja Monad], ei löydy cats kirjaston dokumentaatiosta . Syvällisempi opas ohjelmointiin Scalassa tyyppiluokkien avulla, underscore.io on fantastinen kirja aiheesta: Scala kissojen kanssa

Osa 2: Tyyppiturvallinen sarjalisaatio Scalassa

GIST koodilla

Vastaa

Sähköpostiosoitettasi ei julkaista.