Macroanele și quasitele în scală

Nu cu mult timp în urmă, lansarea lui Scala 2.11.0. Una dintre inovațiile remarcabile ale acestei versiuni sunt cvasi-citatele - un mecanism convenabil pentru descrierea copacilor de sintaxă Scala utilizând șiruri compilate la momentul compilării; este evident că, în primul rând, acest mecanism este destinat să fie utilizat împreună cu macrocomenzile.







În mod surprinzător, pe habra până când tema macro-urilor din Scala nu este considerată prea activă; ultima postare
cu o analiză serioasă a macrocomenzilor a fost deja cu un an în urmă.

În acest post, vom discuta în detaliu scrierea unei macrocomenzi simple proiectate pentru a genera codul de deserializare JSON în ierarhia de clasă.

Formularea problemei

Există o bibliotecă excelentă pentru lucrul cu JSON pentru Scala - spray.json.

De obicei, pentru a deserializa un obiect JSON folosind această bibliotecă, doar câteva importuri:

Destul de simplu, nu-i așa? Și dacă vrem să deseralizăm în întregime ierarhia claselor? Voi da un exemplu de ierarhie, pe care o vom lua în considerare în viitor:

După cum puteți vedea, mai multe clase deserializabile cu un număr diferit de argumente de diferite tipuri sunt moștenite de la părintele abstract. O dorință complet naturală de a deserializa astfel de entități este de a adăuga un câmp de tip la obiectul JSON și, atunci când este dispersonizat, trimiteți-l la acest câmp. Ideea poate fi exprimată prin următorul pseudocod:

Biblioteca spray.json oferă posibilitatea de a defini conversia JSON la orice tip, în conformitate cu regulile definite de utilizator prin extinderea formatorului RootJsonFormat. Sună exact ca ceea ce avem nevoie. Miezul formatorului nostru ar trebui să arate astfel:

Acest cod pare un șablon puțin. Este o sarcină minunată pentru o macrocomandă! Restul articolului este dedicat dezvoltării unei macrocomenzi care poate genera un astfel de cod, având doar tipul de mesaj ca punct de plecare.

Organizarea proiectului

Primul obstacol pe care un programator îl întâlnește atunci când dezvoltă macrocomenzi este că SBT nu dorește să compileze atât macro, cât și codul care o folosește în același timp. Această problemă este discutată în documentația SBT și recomandăm soluția descrisă mai jos.

Trebuie să separați codul macro și codul principal al aplicației în două proiecte, care ar trebui să fie menționate în fișierul principal / Build.sbt. Aceste pregătiri sunt deja făcute în codul care însoțește articolul, aici sunt linkurile la fișierele rezultate:

O altă subtilitate este că, dacă doriți macro pentru a lucra cu ierarhia de clasă - ierarhia trebuie să fie cunoscute în momentul unei expansiuni macro. Acest lucru provoacă unele probleme, pentru că secvența procesării fișierelor de către compilator nu este întotdeauna evidentă. Soluția la această problemă - fie au clase, în care ar trebui să rula un macro într-un proiect de macro (dezvăluirea macro continuă să fie într-un alt proiect) sau pur și simplu loc clasele în același fișier, care este realizat dintr-o expansiune macro.

La depanarea macrocomenzilor, opțiunea compilator -Ymacro-debug-lite este foarte utilă. care vă permite să afișați consola de desfășurarea tuturor macro-urile din cadrul proiectului (aceste rezultate sunt foarte asemănătoare cu codul Scala, și poate fi de multe ori neschimbat compilat transferați manual compilator care poate ajuta în depanare cazuri non-triviale).

Macrourile din Scala funcționează la fel ca și reflecția. Rețineți că API-ul de reflecție Scala este semnificativ diferit de reflecția Java, deoarece nu toate conceptele Scala sunt cunoscute bibliotecii standard Java.

Mecanismul macro-urilor din Scala oferă posibilitatea de a crea secțiuni de cod la timpul de compilare. Acest lucru se face folosind un API puternic introdus, care generează arbori de sintaxă care se potrivesc cu codul pe care doriți să-l creați. Scalările macro sunt foarte diferite de toate macrocomenzile cunoscute ale limbajului C, deci nu trebuie confundate.







Scalările macro sunt bazate pe clasa Context. O instanță a acestei clase este întotdeauna trecută la macro atunci când este extinsă. Apoi, puteți să importați de la ea interiorul obiectului Univers și să le folosiți exact ca în timpul reflecției de rulare - cereți de acolo descriptori de tipuri, metode, proprietăți etc. Acest context vă permite să creați arbori de sintaxă utilizând clase ca Literal. Constant. Listă și altele.

De fapt, o macrocomandă este o funcție care acceptă și returnează copacii de sintaxă. Să scriem un șablon pentru macrocomanda noastră:

Parametrul [T] parseMessage ia tipul T. care este baza pentru ierarhia claselor deserializabile și arborele sintaxei pentru obținerea tipului de obiect de hartă deserializat. dar returnează un arbore de sintaxă pentru a obține obiectul deserializat, care este redus la tipul de bază T.

Argumentul tipului T este descris într-un mod special: este indicat că compilatorul trebuie să anexeze la el un obiect generat implicit de tip c.WeakTypeTag. În general vorbind, argumentul implicit al lui TypeTag este folosit în Scala pentru a lucra cu tipurile de argument generice, de obicei inaccesibile în timpul execuției datorită ștergerii de tip. Pentru argumentele macro, compilatorul trebuie să utilizeze nu doar TypeTag. și WeakTypeTag. care, în măsura în care înțeleg, este legată de lucrarea compilatorului (nu are un "TypeTag" complet pentru un tip care nu poate fi încă generat complet în timpul expansiunii macro). Tip asociat cu TypeTag. poate fi obținută folosind metoda typeOf [T] a obiectului Universe; respectiv, pentru WeakTypeTag există o metodă slabăTypeOf [T].

Unul dintre dezavantajele macrocomenzilor este caracterul neobișnuit al descrierii copacilor sintactici. De exemplu, un fragment de cod 2 + 2 pentru generarea ar trebui să arate ca Aplicare (Select (literale (Constant (2)), TermName ( "$ plus")), Lista (literale (Constant (2)))); chiar si cazuri mai grave apar atunci când trebuie să ne imaginăm bucăți mai mari de cod cu modele de substituție. Firește, nu ne place această complexitate și o vom depăși.

Kvazitsitaty

Lipsa mai multor macrocomenzi de la versiunea 2.11.0 Scala poate fi rezolvată cu ușurință cu ajutorul quasit-urilor. De exemplu, construcția de mai sus, care descrie expresia 2 + 2 sub formă de cvasi-cotație, va arăta exact ca q "2 + 2". care este foarte convenabil. În general, quasits în Scala este un set de interpolatori de șir care sunt localizați într-un obiect Univers. După importul acestor interpolatori în domeniul de aplicare actual, devine posibilă utilizarea unei serii de caractere înainte de constanta șirului, care determină modul în care este procesată de către compilator. În special, în implementarea problemei luate în considerare, folosim interpolatori pq pentru șabloane, cq pentru ramificațiile potrivirii expresiei. și q pentru expresiile terminate ale limbii.

În ceea ce privește interpolarea altor șiruri a limbajului Scala, puteți face referire la quasicatele din variabilele care le cuprind domeniul de aplicare. De exemplu, pentru a genera expresia 2 + 2, puteți folosi următorul cod:

Pentru variabilele de diferite tipuri, interpolarea poate avea loc în moduri diferite. De exemplu, variabilele de tip șir din arborii generați devin constante șir. Pentru a vă referi la o variabilă după nume, trebuie să creați un obiect TermName.

După cum se poate observa din codul de exemplu generat la începutul articolului, trebuie să putem genera următoarele elemente:

  • se potrivesc cu casele typeName cu sucursalele. corespunzătoare fiecărui tip de ierarhie;
  • în fiecare ramură - transmiterea unei liste cu numele argumentelor constructorului clasei corespunzătoare la metoda map.getFields;
  • există deconstrucția succesiunii primite (folosind aceeași expresie de potrivire) la variabile și trecerea acestor variabile către constructorul de tip.

Mai întâi de toate, să luăm în considerare generarea unui copac comun pentru întreaga expresie a meciului. Pentru a face acest lucru, trebuie să folosim interpolarea variabilelor în contextul quasitsets:

În această secțiune a codului, se folosește un tip special de interpolare. Cazul de expresie. Clauzele $ în interiorul blocului de potrivire vor fi deschise ca o listă a ramurilor de caz. După cum ne amintim, fiecare ramură ar trebui să arate astfel:

Sub forma unei cvasi-citate, o astfel de ramură poate fi scrisă după cum urmează:

Acest fragment de cod utilizează mai multe quasite: pq "$ name" creează un set de tipare care ulterior sunt înlocuite în expresia Seq (.). Fiecare dintre aceste expresii este de tip JsValue. care trebuie să fie convertită la tipul potrivit înainte de al transmite constructorului; Pentru aceasta, se creează o cvasi-cotată care generează un apel la metoda convertTo. Rețineți că această metodă poate apela, în mod recursiv, formatorul nostru, dacă este necesar (adică puteți să așezați reciproc obiecte Mesaj).

În cele din urmă, arborele de sintaxă rezultat, care constă dintr-o expresie de potrivire cu ramificațiile cauzate de noi, poate fi construită și prin interpolare:

Acest arbore va fi construit de compilator la locul de aplicare al macro-ului.

De-a lungul dezvoltării tehnologiei, metaprogramarea a devenit un element din ce în ce mai important al limbajelor de programare și este din ce în ce mai folosită în codul zilnic pentru a implementa diferite concepte. Scala macro-uri Scalele macro-uri sunt un instrument real care ne poate salva de la diferitele activități de rutină pe care lumea JVM le-a folosit anterior pentru a le implementa prin reflecție sau generarea de coduri.

Desigur, macrocomenzile sunt un instrument puternic care ar trebui folosit cu prudență: dacă este utilizat necorespunzător, pur și simplu trageți piciorul și cădeți în abisul codului neacceptat. Cu toate acestea, ar trebui să încercăm întotdeauna să automatizeze activitățile de rutină și dacă macro-urile pot deveni un instrument pentru noi în această sarcină - acestea vor fi utilizate și vor aduce beneficii comunității.

Materiale utilizate







Trimiteți-le prietenilor: