o introducere în typeclasses în Scala

  • Partea 1: acest post
  • Partea 2: serializare Tip-Safe în Scala
  • Partea 3: eroare de manipulare Typeful cu tipuri mai mari-kinded

ce este o clasă de tip

una dintre cele mai mari bariere în înțelegerea și utilizarea clase de tip în propriul cod este numele în sine. Contrar a ceea ce evocă numele, o clasă de tip nu este o clasă dintr-un limbaj orientat pe obiecte. Potrivit wikipedia:

în informatică, o clasă de tip este o construcție de tip sistem care acceptă polimorfismul ad-hoc. Acest lucru se realizează prin adăugarea de constrângeri la variabilele de tip în tipuri polimorfe parametric.

avem niște despachetări de făcut:

  • ce este polimorfismul ad hoc? Orice ar fi, asta e ceea ce clase de tip sunt de gând să ne ajute să realizeze.
  • ce este un tip’ parametric polimorf’? Orice ar fi, așa vom crea propriile noastre clase de tip.

clasele de tip au apărut pentru prima dată în Haskell și sunt o modalitate fundamentală de a construi abstracții în Haskell. Ele sunt atât de vitale încât sunt introduse în prima pagină a cărții Learn You a Haskell:

o clasă de tip este un fel de interfață care definește un anumit comportament. Dacă un tip face parte dintr-o clasă de tip, înseamnă că acceptă și implementează comportamentul descris de clasa de tip.

OK, deci se pare că încercăm să definim un anumit comportament pentru tipurile noastre și să implementăm acel comportament diferit în funcție de tip. Ultima parte, „implementați comportamentul diferit în funcție de tip” este bitul „polimorfismului” de mai devreme. Deci termenul începe să aibă sens: este o clasă pentru tipuri. O clasă care implementează comportamentul desemnat de clasa de tip se spune că este un membru al acelei clase de tipuri — ‘clasă de tip’.

în acest moment ar fi potrivit să spunem că acest lucru sună ca moștenire. Ambele ne permit să construim abstracții și să oferim polimorfism.

deci, de ce explora clase de tip la toate? În multe cazuri, ele sunt destul de un pic mai puternic și extensibil decât o ierarhie moștenire. Ele pot fi mult mai simple și se potrivesc mult mai bine atunci când baza de cod se apleacă deja FP peste OO. Deși chiar și într-un proiect puternic OO, clasele de tip pot salva în continuare ziua.

definirea unei clase de tip

este suficientă introducere — pentru restul articolului vom explora o bucată de funcționalitate foarte familiară, care este de fapt implementată în Java prin moștenire și implementând-o folosind o clasă de tip: stringification.

suntem cu toții familiarizați cu metoda care există pe toate obiectele din Java (și astfel Scala): .toString. Scopul său este de a produce o reprezentare șir a obiectului în cauză. Să ne uităm la modul în care ne-ar folosi clase de tip pentru a realiza acest lucru. Versiunea Haskell a toString se numește Show, așa că vom rămâne aproximativ cu acest nume:

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

asta este: există clasa noastră de tip Showable. De ce este nevoie de un parametru de tip A? Dacă ne amintim prima noastră definiție a claselor de tip, citim că ” realizat prin adăugarea de constrângeri la variabilele de tip în tipuri parametric polimorfe.”Showable este tipul nostru polimorf parametric, iar a este variabila noastră de tip, ceea ce înseamnă pur și simplu că ia un tip (A) ca parametru de tip. (Notă nu am pus nici o constrângere pe A, conform definiției, dar asta e bine. Asta facem atunci când compunem clase de tip împreună sau le folosim, care vor veni mai târziu.)

implementarea unei clase de tip

acum trebuie să furnizăm implementări ale trăsăturii Showable pentru orice tipuri pe care dorim să le putem arăta. Aceste implementări sunt numite „instanțe” ale clasei noastre de tip, deoarece vor fi literalmente instanțe ale trăsăturii Showable. Acestea vor fi cel mai adesea clase anonime care suprascriu .show și din moment ce există unele șabloane implicate în definirea lor (amintiți-vă ‘obiecte funcționale’ în programarea Java pre-lambda?), vom defini un ajutor numit make. Să începem cu o implementare pentru un tip de nume de utilizator:

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

Deci, cum am invoca acest lucru? Am putea apela direct metoda din instanța arătabilă, așa:

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

sintaxă îmbunătățită

am reușit să invocăm implementarea Showable a UserName, dar nu a fost foarte frumos. A trebuit să avem o referință la instanța noastră de clasă de tip (defaultUserNameShowable) și să-i cunoaștem numele. Acest lucru este ceva ce vrem abstractizate departe. În cazul în care povestea sa încheiat aici, clase de tip în Scala nu ar fi tot ceea ce frumos de a utiliza. Din fericire, putem îmbunătăți ergonomia drastic folosind implicits. Vom mări obiectul însoțitor al Showable cu un ajutor de invocare:

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

plasând această „metodă de extensie” undeva în domeniu, putem pur și simplu să pretindem că toate obiectele au o metodă .show.

ce a realizat acest lucru

deci, de ce nu suprascrie doar toString pe UserName în schimb? Ei bine, să spunem că UserName trăiește într-o bibliotecă de care depindem și este deja compilată de timpul pe care îl consumăm. Nu o putem subclasa, deoarece este o clasă final. Cu toate acestea, folosind clase de tip, suntem liberi să „atașăm” în mod esențial un nou comportament într-un mod ad-hoc. Aceasta este piesa „ad hoc” a definiției de mai sus: putem adăuga acest polimorfism separat de locul în care este definit tipul în sine.

în plus, spre deosebire de moștenire, atunci când definim clase de tip în Scala, acestea nu trebuie să fie ‘coerente’, ceea ce înseamnă că putem defini mai multe implementări pentru Showable și putem alege între ele. Pentru unele clase de tip acest lucru este un lucru bun, pentru alții nu atât de mult.

în Haskell, clasele de tip sunt coerente. În Scala 3, potrivit lui Martin Odersky, vom avea capacitatea de a alege, pentru fiecare clasă de tip, dacă ar trebui să fie coerentă sau nu.

dar pentru moment, să oferim o semnificație alternativă a show pentru UserName:

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

tot ce trebuie să facem acum este să fim siguri că doar una dintre instanțele noastre Showable este în domeniu la un moment dat. Și dacă nu, nu este mare lucru, vom primi o eroare de compilare a valorilor implicite ambigue:

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

o notă importantă este că nu am cuplat UserName și Showable. Tot ce am făcut este să definim un sens al Showable pentru UserName, păstrându-i în același timp complet independenți unul de celălalt. Această decuplare are implicații uriașe de întreținere și ajută la prevenirea relațiilor de dependență încurcate în întregul cod.

siguranță în timpul compilării

ce se întâmplă dacă încercăm să apelăm .show pe un obiect pentru care nu am furnizat o semnificație show? Cu alte cuvinte, atunci când nu există nici un exemplu de Showable în domeniul de aplicare pentru obiectivul nostru? Pur și simplu obținem o eroare de compilator:

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

putem personaliza de fapt acest mesaj de eroare atunci când definim clasa noastră de tip:

@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

  • clasele de tip ne permit să definim un set de comportamente complet separat de obiectele și tipurile care vor implementa aceste comportamente.
  • clasele de tip sunt exprimate în Scala pură cu trăsături care iau parametrii de tip și implică curățarea sintaxei.
  • clasele de tip ne permit să extindem sau să implementăm comportamentul pentru tipuri și obiecte a căror sursă nu o putem modifica. (Ele oferă polimorfismul ad-hoc necesar pentru a rezolva problema expresiei).

ce urmează

desigur, Showable este o clasă de tip banal, deși nu fără utilizările sale. Cu toate acestea, ne putem imagina cu ușurință altele mult mai utile:

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 ar putea fi o alternativă excelentă la ceva de genul serializării Akka, unde așteptăm până la rulare pentru a vedea dacă un serializator este înregistrat sau nu pentru tipul nostru. Este mult mai sigur pentru a trece implicit punerea în aplicare serializare în la momentul compilării!

Convertible ar putea fi o modalitate excelentă de a converti datele între tipuri. Există o bibliotecă de Twitter cu același scop în minte numit Bijection.

Functor este un element de bază pentru unele abstracții extrem de puternice într-o zonă de programare funcțională numită ‘programare categorică’. Bibliotecile precum ScalaZ și Cats sunt construite pe clase de tip Functor. O noțiune importantă pe care nu am discutat-o de data aceasta este că clasele de tip pot fi ușor compuse pentru a construi un comportament din ce în ce mai complex din blocuri simple de construcție precum Functor.

în plus, multe biblioteci esențiale din ecosistemul Scala (Play JSON, Slick, et. al) să utilizeze clasele de tip și să le expună ca principal motor al API-ului lor în multe cazuri (e.G. joacă clasa de tip JSON Format). Înțelegerea claselor de tip este o modalitate excelentă de a deveni mai eficient în utilizarea a ceea ce este acolo în ecosistemul Scala, chiar dacă nu scrieți niciodată nimic.

în viitor, mă voi uita la câteva clase de tip imediat practice, cum ar fi SafeSerializable. Pentru o introducere la clasele de tip categorice mai abstracte, cum ar fi Functor] și Monad], nu căutați mai departe de documentația bibliotecii cats. Pentru un ghid mai aprofundat pentru programarea în Scala folosind clase de tip, underscore.io are o carte fantastică pe această temă: Scala cu pisici

Partea 2: serializare sigură de tip în Scala

GIST cu cod

Lasă un răspuns

Adresa ta de email nu va fi publicată.