bevezetés a typeclasses-be a Scala-ban

  • 1. rész: ez a bejegyzés
  • 2. rész: Type-Safe serialization a Scala-ban
  • 3.rész: Typeful hibakezelés magasabb rendű típusokkal

mi az a type osztály

az egyik legnagyobb akadály a type osztályok megértésében és használatában a saját kódjában maga a név. Ellentétben azzal, amit a név idéz, a típusosztály nem objektumorientált nyelv osztálya. A wikipedia szerint:

ban ben számítástechnika, a típusosztály egy típusrendszer konstrukció, amely támogatja ad hoc polimorfizmus. Ezt úgy érjük el, hogy korlátokat adunk a parametrikusan polimorf típusú változókhoz.

kicsomagolnunk kell:

  • mi az ad hoc polimorfizmus? Bármi is az, ez az, amit típusú osztályok fognak segíteni elérni.
  • mi az a parametrikusan polimorf típus? Bármi is az, így fogjuk létrehozni a saját típusú osztályainkat.

a Típusosztályok először a Haskellben jelentek meg, és alapvető módon építik fel az absztrakciókat Haskellben. Annyira létfontosságúak, hogy a Learn You a Haskell könyv első oldalán mutatják be őket:

a type osztály egyfajta interfész, amely meghatároz valamilyen viselkedést. Ha egy Típus egy type osztály része, az azt jelenti, hogy támogatja és végrehajtja a type osztály által leírt viselkedést.

Ok, tehát úgy tűnik, hogy megpróbálunk meghatározni néhány viselkedést a típusainkhoz, és ezt a viselkedést a típustól függően másképp valósítjuk meg. Ez az utolsó rész, “a viselkedést a típustól függően másképp valósítja meg”, a korábbi “polimorfizmus” bit. Tehát a kifejezésnek van értelme: ez egy osztály a típusok számára. Egy osztály, amely végrehajtja a type osztály által kijelölt viselkedést, azt mondják, hogy tagja annak a type osztálynak — ‘type osztály’.

ezen a ponton helyénvaló lenne azt mondani, hogy ez öröklésnek hangzik. Mindkettő lehetővé teszi számunkra, hogy absztrakciókat építsünk fel, és polimorfizmust biztosítsunk.

akkor miért vizsgálja típusú osztályok egyáltalán? Sok esetben egy kicsit erősebbek és kiterjeszthetőbbek, mint egy öröklési hierarchia. Sokkal egyszerűbbek lehetnek, és sokkal jobban illeszkednek, ha a kódbázisod már az FP-t az OO fölé hajolja. Bár még egy erősen OO projektben is, a típusosztályok még mindig megmenthetik a napot.

Typeclass meghatározása

ez elég bevezetés — a cikk további részében egy nagyon ismerős funkcionalitást fogunk feltárni, amely valójában a Java-ban öröklődés útján valósul meg, és egy type class: stringification használatával valósítjuk meg.

mindannyian ismerjük a Java (és így a Scala) összes objektumán létező módszert: .toString. Célja a kérdéses objektum karakterlánc-ábrázolásának előállítása. Nézzük meg, hogyan fogjuk használni típusú osztályok elérni ezt. Haskell toString verziójának neve Show, tehát nagyjából ragaszkodunk ehhez a névhez:

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

ez az: ott van a Showable típusú osztályunk. Miért van szükség a A típusú paraméterre? Ha felidézzük a típusosztályok első definícióját, azt olvassuk, hogy ” korlátok hozzáadásával érhető el a parametrikusan polimorf típusú változókhoz.”Showable a parametrikusan polimorf típusunk, az A pedig a típusváltozónk, ami egyszerűen azt jelenti, hogy egy típust (A) vesz fel típusparaméterként. (Megjegyzendő, hogy a A – ra nem szabtunk semmilyen korlátozást, a definíció szerint, de ez rendben van. Ez az, amit csinálunk, amikor alkotó típusú osztályok együtt, vagy kihasználva őket, ami később jön.)

Typeclass megvalósítása

most meg kell adnunk a Showable tulajdonság implementációit minden olyan típushoz, amelyet meg akarunk mutatni. Ezeket a megvalósításokat type osztályunk “példányainak” nevezzük, mivel szó szerint a Showabletulajdonság példányai lesznek. Ezek leggyakrabban anonim osztályok, amelyek felülbírálják a .show – et, és mivel van néhány boilerplate a meghatározásukban (emlékszel a ‘function objects’-re a lambda előtti Java programozásban?), definiálunk egy make nevű segítőt. Kezdjük egy felhasználónév-típus megvalósításával:

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

szóval hogyan hivatkozhatnánk erre? Közvetlenül meghívhatjuk a módszert a bemutatható példányból, így:

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

továbbfejlesztett szintaxis

sikerült hivatkozni a Showable végrehajtása UserName, de ez nem volt túl szép. Kellett egy hivatkozás a type class példány körül (defaultUserNameShowable), és tudja a nevét. Ezt szeretnénk elvonatkoztatni. Ha a történet itt véget ért, a Scala típusú osztályokat nem lenne olyan jó használni. Szerencsére az ergonómiát drasztikusan javíthatjuk implicit módon. Kiegészítjük Showable társobjektumát egy meghívó segítővel:

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

ha ezt a “kiterjesztési módszert” valahol a hatókörbe helyezzük, egyszerűen úgy tehetünk, mintha minden objektum rendelkezik .show módszerrel.

mit ért el ez

akkor miért nem írja felül a toString – ot UserName helyett? Nos, tegyük fel, hogy UserName egy olyan könyvtárban él, amelytől függünk, és már össze van állítva, mire elfogyasztjuk. Nem tudjuk alosztályozni, mivel ez egy final osztály. A típusosztályok használatával azonban szabadon ad hoc módon lényegében új viselkedést “csatolhatunk” hozzá. Ez a fenti meghatározás “ad hoc” darabja: ezt a polimorfizmust külön adhatjuk hozzá attól a helytől, ahol maga a típus meg van határozva.

továbbá, az öröklődéssel ellentétben, amikor a Scala-ban típusosztályokat definiálunk, nem szükséges, hogy’ koherensek ‘ legyenek, ami azt jelenti, hogy több implementációt is definiálhatunk a Showable számára, és választhatunk közöttük. Bizonyos típusú osztályok esetében ez jó dolog, mások számára nem annyira.

Haskellben a típusosztályok koherensek. A Scala 3-ban Martin Odersky szerint képesek leszünk minden típusosztályhoz kiválasztani, hogy koherensnek kell-e lennie vagy sem.

de most adjuk meg a show alternatív jelentését UserName:

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

csak annyit kell tennünk, hogy megbizonyosodunk arról, hogy egyszerre csak egy Showable példányunk van hatályban. Ha nem, akkor nem nagy ügy, fordítási idejű’ kétértelmű implicit értékek ‘ hibát kapunk:

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

fontos megjegyezni, hogy nem kapcsoltuk össze UserName és Showable. Mindössze annyit tettünk, hogy meghatároztuk a Showable jelentését UserName – re, miközben teljesen függetlenek voltak egymástól. Ez a szétválasztás hatalmas karbantarthatósági következményekkel jár, és segít megelőzni a kódban lévő kusza függőségi kapcsolatokat.

fordítási idejű biztonság

mi történik, ha megpróbáljuk meghívni a .show-et egy olyan objektumon, amelynek nem adtuk meg a show jelentését? Más szóval, ha nincs példánya Showable hatókörében a cél? Mi egyszerűen kap egy fordító hiba:

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

valójában testreszabhatjuk ezt a hibaüzenetet, amikor meghatározzuk a type osztályunkat:

@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.

kulcs elvihető

  • a Típusosztályok lehetővé teszik számunkra, hogy a viselkedések halmazát teljesen elkülönítsük azoktól a tárgyaktól és típusoktól, amelyek végrehajtják ezeket a viselkedéseket.
  • a Típusosztályokat tiszta Scala-ban fejezik ki olyan tulajdonságokkal, amelyek típusparamétereket vesznek fel, és implicit módon teszik tisztává a szintaxist.
  • a Típusosztályok lehetővé teszik a viselkedés kiterjesztését vagy megvalósítását olyan típusok és objektumok esetében, amelyek forrását nem tudjuk módosítani. (Biztosítják a kifejezési probléma megoldásához szükséges ad hoc polimorfizmust).

mi következik

megadott, Showable egy triviális típusú osztály, bár nem felhasználása nélkül. Könnyen el tudunk képzelni sokkal hasznosabb is, azonban:

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 lehet egy nagyszerű alternatíva valami hasonló Akka Serialization ahol várjuk, amíg futásidejű, hogy e vagy sem a serializer regisztrált a típus. Sokkal biztonságosabb implicit módon átadni a Sorosítási megvalósítást fordításkor!

Convertible nagyszerű újrafelhasználható módszer lehet az adatok típusonkénti konvertálására. Van egy könyvtár a Twitter által, ugyanazzal a céllal, amelyet Bijekciónak hívnak.

Functor egy építőköve néhány rendkívül erős absztrakciónak a funkcionális programozás egyik területén, amelyet kategorikus programozásnak neveznek. Az olyan könyvtárak, mint a ScalaZ és a Cats, olyan típusú osztályokra épülnek, mint a Functor. Az egyik fontos elképzelés, amelyet ezúttal nem tárgyaltunk, az, hogy a típusosztályok könnyen összeállíthatók, hogy egyre összetettebb viselkedést építsenek fel olyan egyszerű építőelemekből, mint a Functor.

ezen kívül számos alapvető könyvtárak a Scala ökoszisztéma (Play JSON, Slick, et. al) használja a típusosztályokat, és sok esetben az API fő hajtóerejeként tárja fel őket(pl.G.JSON Format típusú osztályának lejátszása). A típusosztályok megértése nagyszerű módja annak, hogy hatékonyabbá váljon a Scala ökoszisztémájában rejlő lehetőségek kihasználásában, még akkor is, ha soha nem írsz sajátot.

a jövőben néhány azonnal gyakorlati típusú osztályt fogok megvizsgálni, mint például SafeSerializable. A Functor] és Monad] típusú absztrakt osztályok bevezetéséhez ne keresse tovább a cats könyvtár dokumentációját . A Scala programozásának részletesebb útmutatója a típusosztályok használatával, a underscore.io van egy fantasztikus könyv a témában: Scala macskákkal

2. rész: típusbiztos sorosítás a Scala – ban

lényeg a kóddal

Vélemény, hozzászólás?

Az e-mail-címet nem tesszük közzé.