5 lépés a legelső típusosztály létrehozásához a Scala-ban

Ebben a blogbejegyzésben megismerheti az első típusú osztály megvalósítását, amely alapvető nyelvi funkció a funkcionális programozási nyelvek ikonjában - Haskell.

Fotó: Stanley Dai az Unsplash-en

A Type Class egy olyan minta, amely Haskell-ből származik, és ez a polimorfizmus megvalósításának szokásos módja. Az ilyen típusú polimorfizmust ad-hoc polimorfizmusnak nevezzük. A neve abból a tényből származik, hogy a közismert alírási polimorfizmussal ellentétben a könyvtár bizonyos funkcióit kibővíthetjük anélkül, hogy hozzáférnénk a könyvtár és az osztály forráskódjához, és melyik funkciót szeretnénk kiterjeszteni.

Ebben a bejegyzésben látni fogja, hogy a típusosztályok használata ugyanolyan kényelmes lehet, mint a szokásos OOP polimorfizmus használata. Az alábbi tartalom végigvezeti Önt a Type Class minta bevezetésének minden szakaszában, hogy jobban megértse a funkcionális programozási könyvtárak belső részeit.

Az első típusosztály létrehozása

A műszaki típusú osztály csak egy paraméteres tulajdonság, számos elvont módszerrel, amelyek az adott tulajdonságot kiterjesztő osztályokban megvalósíthatók. Mindeddig valóban úgy néz ki, mint egy jól ismert al-gépelési modellben.
Az egyetlen különbség az, hogy az altipizálás segítségével a szerződést olyan kategóriákban kell végrehajtani, amelyek egy darab tartományi modellek, a típusosztályokban a tulajdonság megvalósítása teljesen más osztályba kerül, amelyet típusparaméter kapcsol össze a “domain class” -rel.

Példaként a cikkben a Macskák könyvtár Eq Type osztályát fogom használni.

tulajdonság Eq [A] {
  def areEquals (a: A, b: A): logikai
}

Az Eq [A] típusú osztály olyan szerződés, amely képes ellenőrizni, hogy két A típusú objektum azonos-e az areEquals módszerben alkalmazott kritériumok alapján.

Típusosztályunk példányának létrehozása olyan egyszerű, mint az azonnali osztályozás, amely kiterjeszti az említett tulajdonságot csak egyetlen különbséggel, hogy a típusosztály példányunk implicit objektumként lesz elérhető.

def moduloEq (osztó: Int): Eq [Int] = új Eq [Int] {
 felülbírálás def areEquals (a: Int, b: Int) = a% osztó == b% osztó
}
implicit val modulo5Eq: Eq [Int] = moduloEq (5)

A fenti kódrészlet kicsit tömöríthető a következő formában.

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

De várjon, hogyan lehet hozzárendelni az (Int, Int) => Boolean függvényt az Eq [Int] típushoz ?! Ez a dolog a Java 8 Funkciónak, az Single Abstract Method típusú interfésznek köszönhetően lehetséges. Meg tudjuk csinálni egy ilyen dolgot, ha tulajdonságunkban csak egy elvont módszer van.

Típusosztály felbontás

Ebben a bekezdésben megmutatom, hogyan kell használni a típusosztály példányokat, és hogyan varázslatosan összekapcsolhatjuk az Eq [A] típusú osztályt az A típusú megfelelő objektummal, amikor erre szükség lesz.

Itt valósítottuk meg a két Int érték összehasonlításának funkcionalitását, ellenőrizve, hogy a modulo osztási értékek azonosak-e. Mindezen elvégzett munkákkal felhasználhatjuk a típusosztályunkat valamilyen üzleti logika végrehajtására, pl. két olyan modult akarunk párosítani, amelyek modulo egyenlők.

def pairEquals [A] (a: A, b: A) (implicit ekv: Eq [A]): ​​Opció [(A, A)] = {
 if (eq.areEquals (a, b)) Néhány ((a, b)) egyéb Nincs
}

Paraméterezett függvénypár-egyenleteket alkalmaztunk minden olyan típushoz, amely az Eq [A] osztály példányát tartalmazza, annak implicit hatókörén belül.

Ha a fordító nem talál olyan példányt, amely megfelel a fenti nyilatkozatnak, akkor fordítási hibával figyelmezteti a megfelelő példány hiányát a közvetett hatókörben.
  1. A fordító következtetni fog a megadott paraméterek típusára, érveket alkalmazva függvényünkre, és hozzárendelve az A álnévhez.
  2. Előző argumentum eq: Az Eq [A] implicit kulcsszóval javaslatot vált arra, hogy az Eq [A] típusú objektumot implicit hatókörben keresse.

Az impliciteknek és a beírt paramétereknek köszönhetően a fordító képes az osztályt összekapcsolni a megfelelő típusú osztálypéldányával.

Minden példányt és funkciót meghatároztunk. Ellenőrizzük, hogy kódunk érvényes eredményeket ad-e

pairEquals (2,7)
res0: Opció [(Int, Int)] = Néhány ((2,7))
pairEquals (2,3)
res0: Opció [(Int, Int)] = Nincs

Amint látja, várt eredményeket kaptunk, így a típusosztályunk jól teljesít. De ez kissé zsúfoltnak tűnik, megfelelő mennyiségű kazánnal. Scala szintaxisának varázsának köszönhetően sok kazánlemez elveszhet.

Háttér-korlátok

Az első dolog, amit javítani akarok a kódban, az, hogy megszabaduljunk a második argumentumlistától (az implicit kulcsszóval). Nem közvetlenül adjuk át ezt a funkciót, amikor meghívjuk a funkciót, tehát hagyjuk, hogy az implicit ismét implicit legyen. A Scala esetében a típusparaméterekkel kapcsolatos implicit argumentumok helyettesíthetők a Context Bound nevű nyelvi konstrukcióval.

A Context Bound deklaráció a típusparaméterek listájában, amelynek A szintaxisa: Az Eq azt mondja, hogy minden, a pairEquals függvény argumentumaként használt típusnak implicit értékű Eq [A] típusúnak kell lennie az implicit hatókörben.

def pairEquals [A: Eq] (a: A, b: A): Opció [(A, A)] = {
 if (implicit módon [Eq [A]]. areEquals (a, b)) Néhány ((a, b)) else Nincs
}

Amint észrevetted, végül semmi utalás nem mutatott implicit értékre. A probléma kiküszöbölésére implicit módon [F [_]] függvényt használunk, amely húzza a talált implicit értéket azáltal, hogy meghatározza, hogy melyik típusra utalunk.

Ez az, amit a Scala nyelv kínál nekünk, hogy mindent tömörebbé tegyünk. Ennek ellenére számomra még mindig nem elég jó. A Context Bound egy igazán hűvös szintaktikai cukor, de úgy tűnik, hogy ez implicit módon szennyezi a kódunkat. Nagyon jó trükköt fogok csinálni, hogyan lehet legyőzni ezt a problémát, és hogyan csökkentjük a megvalósítás átláthatóságát.

Mit tehetünk, hogy paraméteres alkalmazási funkciót biztosítunk a típusosztályunk társobjektumában.

objektum Eq {
 def alkalmazni kell [A] (implicit ekv .: Eq [A]): ​​Eq [A] = ekv
}

Ez az igazán egyszerű dolog lehetővé teszi számunkra, hogy megszabaduljunk a hallgatólagostól, és meghúzzuk a példányunkat a végbélből, hogy a tartomány logikájában felhasználható legyen kazán nélkül.

def pairEquals [A: Eq] (a: A, b: A): Opció [(A, A)] = {
 if (Eq [A] .areEquals (a, b)) Néhány ((a, b)) egyéb Nincs
}

Implicit konverziók - más néven. Szintaxis modul

A következő dolog, amelyet fel akarok venni a munkapadra, az Eq [A] .areEquals (a, b). Ez a szintaxis nagyon szó szerint néz ki, mert kifejezetten utalunk a típusosztály példányra, amelynek implicitnek kell lennie, nem? A második dolog az, hogy itt a típusú osztálypéldányunk úgy működik, mint a Service (DDD jelentésben), ahelyett, hogy a valódi A osztálykiterjesztés lenne. Szerencsére ez is javítható az implicit kulcsszó egy másik hasznos használatával.

Amit itt fogunk tenni, az úgynevezett szintaxis vagy (ops, mint néhány FP könyvtárban) modul biztosítása implicit konverziókkal, amelyek lehetővé teszik, hogy kibővítsük az egyes osztályok API-ját a forráskód módosítása nélkül.

implicit osztály EqSyntax [A: Eq] (a: A) {
 def === (b: A): logikai = egyenérték [A] .areEquals (a, b)
}

Ez a kód azt mondja a fordítónak, hogy az Eq [A] típusú osztályú példányt tartalmazó A osztályt konvertálja EqSyntax osztályba, amelynek egy === funkciója van. Mindez azt a benyomást kelti, hogy a === függvényt hozzáadtuk az A osztályhoz forráskód módosítása nélkül.

Nemcsak rejtett típusú osztálypéldány-referenciákat nyújtunk, hanem több osztályos szintaxist is biztosítunk, amely benyomást kelti a === módszernek az A osztályban történő végrehajtásáról, még akkor is, ha nem tudunk semmit erről az osztályról. Két madár meghalt egy kövvel.

Most engedélyezhetjük a === módszer alkalmazását az A típusra, amikor az EqSyntax osztály hatókörrel rendelkezik. Most a pairEquals megvalósítása kissé megváltozik, és a következő lesz.

def pairEquals [A: Eq] (a: A, b: A): Opció [(A, A)] = {
 if (a === b) Néhány ((a, b)) egyéb Nincs
}

Mint ígértem, befejeztük a megvalósítást, ahol az OOP megvalósításhoz képest az egyetlen látható különbség a Context Bound kommentár egy A típusú paraméter után. A típusosztály minden műszaki szempontját elválasztjuk a domain logikától. Ez azt jelenti, hogy sokkal több jó dolgot érhet el (amelyet megemlítek a hamarosan közzéteendő külön cikkben) anélkül, hogy megsértené a kódját.

Implicit hatály

Mint látja a típusú osztályokat a Scala-ban szigorúan függ az implicit szolgáltatás használatától, ezért alapvető fontosságú megérteni, hogyan kell az implicit hatókörrel dolgozni.

Az implicit hatókör olyan terület, amelyben a fordító implicit példányokat fog keresni. Számos választási lehetőség van, ezért meg kellett határozni annak a sorrendnek a meghatározását, amelyben a példányokat keresik. A sorrend a következő:

1. Helyi és örökölt példányok
2. Importált példányok
3. Meghatározások a típusosztály társobjektumából vagy a paraméterekből

Ez annyira fontos, mert ha a fordító több példányt talál, vagy egyáltalán nem, akkor hibát fog felmutatni. Számomra a legkényelmesebb módja a típusosztály példányok beszerzésének, ha maga a típus osztály társobjektumába helyezzük őket. Ennek köszönhetően nem kell aggódnunk a helyben található példányok importálásával vagy végrehajtásával, ami lehetővé teszi számunkra, hogy elfelejtsük a helymeghatározási kérdéseket. Mindent varázslatosan a fordító biztosít.

Tehát vitassuk meg a 3. pontot a Scala szokásos könyvtárából ismert közismert függvény példájával, a rendezett funkciók alapján, amelyek implicit módon szolgáltatott összehasonlítókon alapulnak.

rendezve [B>: A] (implicit utasítás: math.Ordering [B]): [A] lista

A típus osztálypéldányt a következő helyen fogja keresni:
 * Társobjektum megrendelése
 * Sorolja fel a társobjektumot
 * B társobjektum (amely alsó határok meghatározásának létezése miatt társ-objektum is lehet)

bálványkép

Mindez nagyon sokat segít a típusosztály mintázat használatakor, de ez megismételhető munka, amelyet minden projektben el kell végezni. Ezek a nyomok egyértelmű jele annak, hogy a folyamat kibontható a könyvtárba. Van egy kiváló Simulacrum nevű makró alapú könyvtár, amely kézzel kezeli a szintaxis modul (azaz ops Simulacrumban) stb. Létrehozásához szükséges összes anyagot.

Az egyetlen változtatás, amelyet be kell vezetnünk, a @typeclass kommentár, amely jelöli a makrókat a szintaxis modul kibővítéséhez.

import simulacrum._
@minőségi tulajdonság Eq [A] {
 @op (“===”) def areEquals (a: A, b: A): Boolean
}

A végrehajtás többi részében nincs szükség változtatásra. Ez minden. Most már tudja, hogyan kell saját maga által megvalósítani a típusosztály mintát a Scala-ban, és remélem, hogy felhívta a figyelmet a Simulacrum könyvtárak működésére.

Köszönjük, hogy elolvastat. Nagyon értékelni fogok Öntől kapott bármilyen visszajelzést, és nagyon várom, hogy a jövőben találkozzunk veled egy újabb közzétett cikkel.