5 pirmās pakāpes izveidošanas pakāpieni Scala

Šajā emuāra ierakstā jūs uzzināsit, kā ieviest savu pirmā veida klasi, kas ir valodas pamatfunkcija funkcionālo programmēšanas valodu ikonā Haskell.

Stenlija Dai foto vietnē Unsplash

Tipa klase ir modelis, kura izcelsme ir Haskell, un tas ir tā standarta veids, kā īstenot polimorfismu. Šāda veida polimorfismu sauc par ad-hoc polimorfismu. Tās nosaukums cēlies no tā, ka pretēji plaši pazīstamajam apakšrakstīšanas polimorfismam mēs varam paplašināt kādu bibliotēkas funkcionalitāti pat bez piekļuves bibliotēkas avotam un klasei, kuru funkcionalitāti mēs vēlamies paplašināt.

Šajā rakstā jūs redzēsit, ka tipa klašu izmantošana var būt tikpat ērta kā regulāra OOP polimorfisma izmantošana. Zemāk esošais saturs vedīs jūs cauri visām Type Class modeļa ieviešanas fāzēm, lai palīdzētu jums labāk izprast funkcionālās programmēšanas bibliotēku iekšējās versijas.

Pirmās klases klases izveidošana

Tehniski tipa klase ir tikai parametrēta īpašība ar vairākām abstraktām metodēm, kuras var ieviest klasēs, kuras šo pazīmi paplašina. Ciktāl viss izskatās tiešām labi zināmajā apakšrakstīšanas modelī.
Vienīgā atšķirība ir tā, ka, izmantojot apakšrakstīšanu, mums jāīsteno līgums klasēs, kas ir domēna modeļa gabals, tipa klasēs pazīmju ieviešana tiek ievietota pilnīgi citā klasē, kas pēc tipa parametra ir saistīta ar “domēna klasi”.

Kā piemēru šajā rakstā es izmantošu Eq Type Class no Cats bibliotēku.

pazīme Eq [A] {
  def areEquals (a: A, b: A): Būla
}

EQ klases klase [A] ir līgums ar spēju pārbaudīt, vai divi A tipa objekti ir vienādi, pamatojoties uz dažiem kritērijiem, kas ieviesti areEquals metodē.

Mūsu tipa klases instances izveidošana ir tikpat vienkārša kā tūlītējas klases izveidošana, kas pieminēto pazīmi paplašina tikai ar vienu atšķirību, ka mūsu tipa klases instance būs pieejama kā netiešs objekts.

def moduloEq (dalītājs: Int): Eq [Int] = jauns Eq [Int] {
 ignorēt def areEquals (a: Int, b: Int) = a% dalītājs == b% dalītājs
}
netiešs val modulo5Eq: Eq [Int] = moduloEq (5)

Virs koda var nedaudz sablīvēt šādā formā.

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

Pagaidiet, kā jūs varat piešķirt funkciju (Int, Int) => Būla atsaucei ar tipu Eq [Int]?! Šī lieta ir iespējama, pateicoties Java 8 funkcijai, ko sauc par viena abstraktās metodes veida saskarni. Mēs varam rīkoties šādi, ja mūsu īpašībās ir tikai viena abstrakta metode.

Tipa klases izšķirtspēja

Šajā rindkopā es jums parādīšu, kā izmantot tipa klases gadījumus un kā maģiski sasaistīt tipa klasi Eq [A] ar atbilstošo A tipa objektu, kad tas būs nepieciešams.

Šeit mēs esam ieviesuši divu Int vērtību salīdzināšanas funkcionalitāti, pārbaudot, vai to modulo dalījuma vērtības ir vienādas. Paveicot visu šo darbu, mēs varam izmantot savu klases klasi, lai veiktu kādu biznesa loģiku, piem. mēs vēlamies savienot pārī divas vērtības, kas ir vienādas ar modulo.

def pairEquals [A] (a: A, b: A) (netiešs ekvivalents: Eq [A]): ​​Opcija [(A, A)] = {
 if (eq.areEquals (a, b)) Daži ((a, b)) cits Nav
}

Mēs esam parametrojuši funkciju pāriEquals darbam ar visiem tipiem, kas nodrošina klases Eq [A] eksemplāru, kas pieejams tā netiešajā darbības jomā.

Ja kompilators neatradīs nevienu gadījumu, kas atbilst iepriekšminētajai deklarācijai, tas beigsies ar kompilācijas kļūdas brīdinājumu par atbilstoša gadījuma trūkumu norādītajā netiešajā darbības jomā.
  1. Sastādītājs secinās sniegto parametru veidu, piemērojot argumentus mūsu funkcijai un piešķirot to aizstājvārdam A.
  2. Iepriekšējais arguments eq: Eq [A] ar netiešu atslēgvārdu izraisīs ierosinājumu meklēt Eq [A] tipa objektu netiešā tvērumā.

Pateicoties netiešajiem un drukātajiem parametriem, kompilators spēj sasaistīt klasi kopā ar atbilstošo tipa klases instanci.

Visi gadījumi un funkcijas ir definētas, pārbaudīsim, vai mūsu kods dod derīgus rezultātus

pairEquals (2,7)
res0: Iespēja [(Int, Int)] = Daži ((2,7))
pairEquals (2,3)
res0: Iespēja [(Int, Int)] = Nav

Kā redzat, mēs saņēmām gaidītos rezultātus, tāpēc mūsu klases klase darbojas labi. Bet šis izskatās mazliet pārblīvēts, ar diezgan lielu katlu plākšņu daudzumu. Pateicoties Scala sintakses burvībai, mēs varam padarīt daudz zaudējušu katlu plāksni.

Konteksta robežas

Pirmais, ko es gribu uzlabot mūsu kodā, ir atbrīvoties no otrā argumentu saraksta (tas ir ar netiešu atslēgvārdu). Kad atsaucamies uz funkciju, mēs tieši to nepamanām, tāpēc ļaujiet netiešajam atkal netieši būt netiešam. Scala netiešos argumentus ar tipa parametriem var aizstāt ar valodas uzbūvi ar nosaukumu Context Bound.

Saistītais konteksts ir deklarācija tipa parametru sarakstā, kura sintakse A: Eq saka, ka katram tipam, ko izmanto kā funkciju pairEquals arguments, netiešajā darbības jomā jābūt Eq [A] tipa netiešajai vērtībai.

def pairEquals [A: Eq] (a: A, b: A): Opcija [(A, A)] = {
 if (netieši [Eq [A]]. irEquals (a, b)) Daži ((a, b)) cits Nav
}

Kā jūs pamanījāt, mēs nonācām bez atsauces uz netiešu vērtību. Lai novērstu šo problēmu, mēs netieši izmantojam funkciju [F [_]], kas izrauj netiešo vērtību, norādot, uz kuru veidu mēs atsaucamies.

Tas ir tas, ko Scala valoda mums piedāvā, lai padarītu to visu kodolīgāku. Tomēr tas joprojām man neizskatās pietiekami labs. Context Bound ir patiešām foršs sintaktiskais cukurs, bet tas netieši liekas, ka piesārņo mūsu kodu. Es izdarīšu jauku triku, kā pārvarēt šo problēmu un samazināt mūsu ieviešanas liekulību.

Tas, ko mēs varam darīt, ir nodrošināt parametrētu piemērošanas funkciju mūsu klases klases pavadošajā objektā.

objekts Eq {
 def piemērot [A] (netiešais ekvivalents: Eq [A]): ​​Eq [A] = ekv
}

Šī patiešām vienkāršā lieta ļauj mums netieši atbrīvoties un izvilkt savu instanci no domstarpībām, kuras var izmantot domēna loģikā bez katlu plāksnes.

def pairEquals [A: Eq] (a: A, b: A): Opcija [(A, A)] = {
 if (Eq [A] .areEquals (a, b)) Daži ((a, b)) cits Nav
}

Netiešie reklāmguvumi - aka. Sintakse modulis

Nākamā lieta, ko es vēlos iegūt uz sava darba galda, ir Eq [A] .areEquals (a, b). Šī sintakse izskatās ļoti sīka, jo mēs tieši atsaucamies uz klases klases instanci, kurai vajadzētu būt netiešai, vai ne? Otra lieta ir tā, ka šeit mūsu tipa klases instance darbojas kā pakalpojums (DDD nozīmē), nevis reāls A klases paplašinājums. Par laimi, to var arī labot, izmantojot citu netieša atslēgvārda noderīgu izmantošanu.

Tas, ko mēs šeit darīsim, ir tā saucamās sintakse vai (op. Kā dažās FP bibliotēkās) moduļa nodrošināšana, izmantojot netiešus konvertējumus, kas ļauj mums paplašināt kādas klases API, nemainot tās avota kodu.

netiešā klase EqSyntax [A: Eq] (a: A) {
 def === (b: A): Būla = Eq [A] .areEquals (a, b)
}

Šis kods kompilatoram liek pārveidot klasi A, kurai ir klases klase Eq [A], par klasi EqSyntax, kurai ir viena funkcija ===. Tas viss rada iespaidu, ka A klasei ir pievienota funkcija === bez avota koda modifikācijas.

Mēs esam ne tikai slēpuši klases klases gadījumu atsauces, bet arī nodrošinājuši vairāk klases sintakse, kas rada iespaidu, ka metode === tiek ieviesta A klasē, pat ja mēs neko nezinām par šo klasi. Ar vienu akmeni nogalināti divi putni.

Tagad mums ir atļauts piemērot metodi === A veidam, kad vien mums ir EqSyntax klases darbības joma. Tagad mūsu pārisEquals ieviešana nedaudz mainīsies, un tā būs šāda.

def pairEquals [A: Eq] (a: A, b: A): Opcija [(A, A)] = {
 ja (a === b) Daži ((a, b)) cits Nav
}

Kā es apsolīju, mēs esam pabeiguši ieviešanu, kurā vienīgā redzamā atšķirība salīdzinājumā ar OOP ieviešanu ir konteksta ierobežojuma anotācija pēc A veida parametra. Visi tipa klases tehniskie aspekti ir atdalīti no mūsu domēna loģikas. Tas nozīmē, ka, nekaitējot savam kodam, jūs varat sasniegt daudz lieliskāku saturu (ko es pieminēšu atsevišķā rakstā, kas drīz tiks publicēts).

Netiešā darbības joma

Kā redzat tipa klases Scala, ir stingri atkarīgas no netiešās funkcijas izmantošanas, tāpēc ir svarīgi saprast, kā strādāt ar netiešu tvērumu.

Netiešā darbības joma ir joma, kurā kompilators meklēs netiešos gadījumus. Ir daudz izvēles iespēju, tāpēc bija jādefinē kārtība, kādā tiek meklēti gadījumi. Rīkojums ir šāds:

1. Vietējie un mantotie gadījumi
2. Importētie gadījumi
3. Definīcijas no tipa klases pavadobjekta vai parametriem

Tas ir tik svarīgi, jo, ja kompilators atrod vairākus gadījumus vai neatrod tos vispār, tas rada kļūdu. Man ērtākais veids, kā iegūt tipa klases gadījumus, ir ievietot tos pašas klases klases papildobjektā. Pateicoties tam, mums nav jāraizējas par tādu gadījumu importēšanu vai ieviešanu, kas ļauj aizmirst par atrašanās vietas jautājumiem. Visu maģiski nodrošina kompilators.

Tātad ļaujiet apspriest 3. punktu, izmantojot labi zināmo funkciju no Scala standarta bibliotēkas, kas sakārtota pēc funkcijām, kuru pamatā ir netieši sniegti salīdzinātāji.

sakārtots [B>: A] (netiešā kārtība: math.Orders [B]): List [A]

Tipa klases piemērs tiks meklēts:
 * Kompānijas objekta pasūtīšana
 * Saraksta pavadoņa objekts
 * B pavadošais objekts (kas var būt arī papildobjekts zemākas robežas definīcijas dēļ)

Simulakroms

Visas šīs lietas daudz palīdz, izmantojot tipa klases modeli, taču tas ir atkārtojams darbs, kas jāveic katrā projektā. Šīs norādes ir acīmredzama pazīme, ka procesu var iegūt bibliotēkā. Ir lieliska makro bāzēta bibliotēka ar nosaukumu Simulacrum, kas ar rokām apstrādā visu lietu, kas vajadzīga sintakse moduļa (ko sauc par opa Simulacrum) ģenerēšanai.

Vienīgās izmaiņas, kas mums jāievieš, ir @typeclass anotācija, kas ir makro zīme, lai paplašinātu mūsu sintakse moduli.

importēt simulacrum._
@typeclass īpašība Eq [A] {
 @op (“===”) def areEquals (a: A, b: A): Boolean
}

Pārējās mūsu ieviešanas daļās nav vajadzīgas nekādas izmaiņas. Tas ir viss. Tagad jūs zināt, kā patstāvīgi ieviest tipa klases modeli Scala, un es ceru, ka jūs ieguvāt izpratni par to, kā darbojas Simulacrum bibliotēkas.

Paldies par lasīšanu, es tiešām novērtēšu visas atsauksmes no jums, un es ceru nākotnē satikties ar jums ar citu publicētu rakstu.