Differences

This shows you the differences between two versions of the page.

Link to this comparison view

pp:24:teme:haskell-imagini-functionale [2024/04/23 21:20]
mihnea.muraru [Changelog]
pp:24:teme:haskell-imagini-functionale [2024/05/11 22:42] (current)
mihnea.muraru [Notă pentru curioși (facultativ)]
Line 2: Line 2:
  
   * Data publicării:​ 09.04.2024   * Data publicării:​ 09.04.2024
-  * Data ultimei modificări: ​23.04.2024+  * Data ultimei modificări: ​11.05.2024
   * Deadline hard: ziua laboratorului 10   * Deadline hard: ziua laboratorului 10
   * [[https://​curs.upb.ro/​2023/​mod/​forum/​view.php?​id=158012|Forum temă]]   * [[https://​curs.upb.ro/​2023/​mod/​forum/​view.php?​id=158012|Forum temă]]
Line 159: Line 159:
 Este suficient ca arhiva pentru **vmchecker** să conțină modulele ''​Deep''​ și ''​Shallow''​. Este suficient ca arhiva pentru **vmchecker** să conțină modulele ''​Deep''​ și ''​Shallow''​.
  
 +===== Etapa 3 =====
 +
 +În etapa 2, am văzut cum **interpretarea** //deep embeddings//​ (AST-urilor) ale transformărilor și regiunilor prin funcțiile ''​toTransformation''​ și ''​toRegion''​ a permis **recuperarea** //shallow embeddings//​ din etapa 1. Mai precis, **paralela** dintre implementările celor două etape este următoarea:​
 +
 +  * **Un singur caz** al funcției de interpretare din etapa 2, aferent unui anumit //​pattern//,​ corespunde unei **funcții de sine stătătoare** din etapa 1. De exemplu, cazul ''​toRegion (Union region1 region2) = ...''​ din etapa 2 corespunde funcției ''​union region1 region2 = ...''​ din etapa 1.
 +  * O funcție din etapa 1 primește ca parametri reprezentări concrete pe care le **manipulează direct**, în timp ce în etapa 2 este necesară **interpretarea explicită** a parametrilor. De exemplu, în etapa 1, definiția unei funcții are forma ''​union region1 region2 = ... region1 ... region2 ...'',​ în care parametrii sunt utilizați ca atare, în timp ce în etapa 2, un caz are forma ''​toRegion (Union region1 region2) = ... (toRegion region1) ... (toRegion region2) ...'',​ în care parametrii trebuie interpretați în prealabil.
 +
 +Din cele de mai sus decurg natural două **întrebări**,​ pe care le vom discuta în continuare:
 +
 +  - Fiecărui //shallow embedding// îi **corespunde** o interpretare a //deep embeddings//​ și viceversa sau relația dintre ele este mai complicată?​
 +  - Putem **ascunde interpretarea recursivă explicită** a substructurilor din cadrul //deep embeddings//,​ pentru a ne concentra doar pe maniera în care **combinăm** produsele interpretării (de exemplu, pentru a evita invocarea explicită a lui ''​toRegion''​ pe ''​region1''​ și ''​region2''​)?​
 +
 +Pentru a răspunde la **întrebarea 1**, introducem distincția dintre operații compoziționale și necompoziționale:​
 +
 +  * Rezultatul unei operații **compoziționale** aplicate pe o structură depinde doar de rezultatul **aceleiași operații** aplicate recursiv substructurilor,​ **nu și de alte caracteristici** ale acestora. De exemplu, operația de calcul al **lungimii unei liste** este compozițională,​ întrucât ''​length (x : xs)''​ depinde doar de ''​length xs'',​ nu și de alt aspect al lui ''​xs'',​ cum ar fi ''​xs''​ însuși, primul element al lui ''​xs''​ sau suma elementelor lui ''​xs''​. Alte exemple de operații compoziționale sunt calculul **sumei elementelor** unei liste, al **înălțimii** unui arbore binar (funcția ''​height''​ din laboratorul 8) și funcția **''​toRegion''​** din etapa 2.
 +  * Rezultatul unei operații **necompoziționale** depinde și de **alte caracteristici** ale substructurilor,​ în afară de rezultatul aplicării recursive a operației pe acestea. De exemplu, calcul **mediei aritmetice** a elementelor unei liste este o operație necompozițională,​ întrucât nu putem determina ''​average (x : xs)''​ doar pe baza lui ''​average xs'',​ având nevoie separat de suma elementelor și de lungimea listei. De asemenea, ​ funcția ''​isBalanced''​ din laboratorul 8, care verifică dacă un arbore binar este **echilibrat**,​ este o operație necompozițională,​ întrucât ''​isBalanced (Node key left right)''​ depinde nu numai de ''​isBalanced left''​ și ''​isBalanced right'',​ ci și de ''​height left''​ și ''​height right''​.
 +
 +Cu toate acestea, o operație necompozițională poate fi **transformată** într-una compozițională,​ o metodă fiind îmbogățirea rezultatului calculat. De exemplu, funcția ''​isBalanced''​ de mai sus poate fi rescrisă compozițional dacă, în loc să întoarcă doar rezultatul boolean, întoarce o **pereche** cu rezultatul boolean **și** înălțimea arborelui; în acest fel, am avea la dispoziție toată informația necesară doar din aplicațiile recursive ''​isBalanced left''​ și ''​isBalanced right''​.
 +
 +În concluzie, având în vedere că în cadrul unui //shallow embedding// avem direct la dispoziție o **anumită reprezentare concretă** a entităților noastre (de exemplu, regiuni ca funcții caracteristice în etapa 1), din care în general nu putem recupera alte caracteristici (de exemplu, nu putem recupera secvența operațiilor care au construit o regiune având la dispoziție doar funcția ei caracteristică),​ //shallow embeddings//​ **corespund exclusiv interpretărilor compoziționale** ale //deep embeddings//​ (cum este ''​toRegion''​ din etapa 2). Aceasta înseamnă că un //shallow embedding// poate fi privit întotdeauna ca o interpretare a //deep embeddings//,​ dar **nu și viceversa** (cel puțin nu direct).
 +
 +Mergând mai departe la **întrebarea 2**, regăsim ideea de **//fold// (reducere)**. De exemplu, pe liste, funcționala ''​foldr''​ are tocmai scopul de a încapsula prelucrarea recursivă a restului listei, permițându-i programatorului să se concentreze doar pe maniera de **îmbinare** a elementului curent cu rezultatul prelucrării recursive (acumulatorul),​ prin funcția binară trimisă ca parametru către ''​foldr''​. Având în vedere că funcția de combinare, pe care o vom denumi de acum înainte **//​combiner//​**,​ primește ca parametru direct rezultatul prelucrării recursive a restului listei, operațiile implementabile ca reduceri (prin funcționala ''​foldr''​) sunt întotdeauna **compoziționale**.
 +
 +Pasul următor este să vedem cum putem **extinde** ideea de reducere pe alte tipuri de date, în afară de liste; în particular, pe transformări și regiuni. În laboratorul 9, ați descoperit că funcționala ''​foldr''​ aparține clasei ''​Foldable'',​ instanțiată inclusiv de constructorul listă. Din păcate, tipul funcției este inspirat de constructorii de date ai tipului listă (//cons// și lista vidă) și **nu este suficient de expresiv** pentru a permite implementarea **oricărei operații compoziționale** pe un tip de date oarecare. De exemplu, deși pentru un tip de arbore binar poate fi dată o definiție a funcționalei ''​foldr''​ (ca în laboratorul 9), unele operații compoziționale,​ ca determinarea numărului de chei sau a sumei cheilor **pot** fi implementate prin ''​foldr'',​ în timp ce alte operații compoziționale,​ ca determinarea înălțimii arborelui, **nu pot** fi implementate prin ''​foldr''​. Motivul este că //​combiner//​-ul ia ca parametru un singur acumulator, în timp ce pentru determinarea înălțimii ar fi necesari doi acumulatori,​ aferenți celor doi subarbori.
 +
 +Pentru a ne da seama cum putem defini un mecanism de reducere **particularizat** pe un anumit tip de date, care să permită exprimarea **oricărei operații compoziționale** pe acesta, să ne amintim cum au fost aleși parametrii funcționalei ''​foldr'',​ inițial dedicată listelor. Listele posedă doi constructori de date, //cons// și lista vidă; pentru **fiecare constructor de date**, definim **câte un parametru** al funcționalei ''​foldr'',​ al cărui tip se obține **înlocuind referirile recursive la tipul listă cu tipul acumulatorului**. Mai precis:
 +
 +  * //cons// are tipul ''​(:​) :: e -> [e] -> [e]'',​ unde ''​e''​ este tipul elementelor. Înlocuind referirile recursive la ''​[e]''​ cu tipul ''​a''​ al acumulatorului,​ obținem tipul primului parametru al lui ''​foldr'',​ și anume ''​e -> a -> a''​.
 +  * Lista vidă are tipul ''​[] :: [e]''​. În urma aceleiași înlocuiri, obținem tipul celui de-al doilea parametru al lui ''​foldr'',​ și anume ''​a''​.
 +
 +Astfel, se obține binecunoscutul tip al lui ''​foldr''​ particularizat pe liste, și anume ''​(e -> a -> a) -> a -> [e] -> a''​.
 +
 +Dacă dorim să **adaptăm** această idee pentru tipul ''​RegionAST'',​ trebuie să inventăm o nouă funcțională de reducere, ''​foldRegionAST'',​ și, la fel ca mai sus, să asociem fiecărui constructor de date câte un parametru al acestei funcții. De exemplu:
 +
 +  * Constructorul ''​FromPoints''​ are tipul ''​[Point] -> RegionAST''​. Înlocuind referirea recursivă la ''​RegionAST''​ cu tipul ''​a''​ al acumulatorului,​ obținem tipul parametrului aferent al lui ''​foldRegionAST'',​ și anume ''​[Point] -> a''​.
 +  * Contructorul ''​Union''​ are tipul ''​RegionAST -> RegionAST -> RegionAST''​. În urma aceleiași înlocuiri, obținem tipul parametrului aferent al lui ''​foldRegionAST'',​ și anume ''​a -> a -> a''​ ș.a.m.d.
 +
 +Din păcate, ''​RegionAST''​ posedă șapte constructori de date (ar putea fi și mai mulți), și transmiterea unui **număr atât de mare de parametri** funcționalei ''​foldRegionAST''​ devine greoaie. Din fericire, se poate recurge la următorul artificiu:
 +
 +  - Definim mai întâi un nou tip de date, ''​RegionShape a'',​ cu aceiași constructori de date ca ''​RegionAST'',​ dar care utilizează tipul ''​a''​ pentru câmpuri. De exemplu, constructorul ''​Union''​ este acum definit prin ''​Union a a''​ în loc de ''​Union RegionAST RegionAST''​. Rolul lui ''​RegionShape''​ este de a constitui un **eșafodaj comun**, atât pentru **construcția AST-urilor**,​ cât și pentru mecanismul de **reducere**.
 +  - Tipul original ''​RegionAST''​ poate fi **recuperat** particularizând parametrul de tip ''​a''​ al constructorului de tip ''​RegionShape''​ la ''​RegionAST''​ însuși. Mai precis, ''​RegionAST''​ este definit prin **ecuația de punct fix** ''​RegionAST = RegionShape RegionAST''​.
 +  - **//​Combiner//​-ele** pot fi definite ca operând direct pe valori ale tipului ''​RegionShape a'',​ cu tipul ''​a''​ particularizat în raport cu rezultatul dorit al operației de reducere. De exemplu, dacă se dorește reducerea unui ''​RegionAST''​ la o descriere textuală a sa, //​combiner//​-ul poate căpăta tipul ''​RegionShape String''​. Aceasta înseamnă că, atunci când //​combiner//​-ul tratează prin //pattern matching// cazul ''​Union string1 string2'',​ presupune că cele două câmpuri ale constructorului ''​Union''​ **conțin deja rezultatele reducerilor recursive ale subarborilor** la șiruri de caractere, astfel încât se poate **concentra direct pe combinarea** lor pentru a obține reprezentarea ca șir de caractere a întregii regiuni (exact ca la funcționala ''​foldr''​ pe liste).
 +
 +În final, în loc ca funcționala ''​foldRegionAST''​ să primească drept parametri **șapte //​combiner//​-e**,​ câte unul pentru fiecare constructor de date al tipului ''​RegionAST'',​ primește **un singur //​combiner//​**,​ care tratează prin //pattern matching// șapte cazuri, asigurând o mult mai bună **modularizare**.
 +
 +O întrebare pertinentă este dacă **beneficiile** abordării bazate pe reduceri (//​folds//​),​ prezentate mai sus, justifică efortul de elaborare. Dincolo de **simplificarea** implementărilor operațiilor compoziționale,​ prin ascunderea interpretării recursive a substructurilor,​ care sporește **lizibilitatea**,​ există avantaje mai puțin evidente: reducerile au multe proprietăți matematice cunoscute, și descrierea explicită a unei operații ca o reducere poate înlesni **demonstrarea formală a corectitudinii** unui program și chiar aplicarea de **optimizări** la nivelul compilatorului.
 +
 +În cadrul etapei 3, veți instanția diverse clase și veți implementa mecanismul de reducere descris mai sus. Mai precis, veți scrie funcții care:
 +
 +  * reprezintă AST-urile transformărilor și regiunilor ca **șiruri de caractere**,​ pentru o mai bună vizualizare la consolă, instanțiind clasa ''​Show''​
 +  * permit construcția regiunilor prin intermediul **operatorilor aritmetici** (de exemplu, reuniune văzută ca adunare), instanțiind clasa ''​Num''​
 +  * permit **aplicarea unei funcții** asupra câmpurilor aferente unor tipuri, instanțiind clasa ''​Functor''​
 +  * permit **reducerea** AST-urilor transformărilor și a regiunilor
 +  * definesc anumite **operații compoziționale** pe transformări și regiuni în termenii unor reduceri.
 +
 +Construcțiile și mecanismele noi de limbaj pe care le veți exploata în rezolvare, pe lângă cele din etapa 2, sunt:
 +
 +  * **polimorfismul ad-hoc**
 +  * **clasele**.
 +
 +Modulul de interes din **schelet** este ''​Folds'',​ care conține definițiile **tipurilor de date** necesare, precum și **operațiile** pe care trebuie să le implementați. De asemenea, trebuie să aveți în același director și modulul ''​Shallow''​ din etapa 1. Găsiți detaliile despre **funcționalitate** și despre **constrângerile de implementare**,​ precum și **exemple**,​ direct în schelet. Aveți de completat definițiile care încep cu ''​%%*** TODO ***%%''​.
 +
 +Pentru **rularea testelor**, încărcați în interpretor modulul ''​TestFolds''​ și evaluați ''​main''​.
 +
 +Este suficient ca arhiva pentru **vmchecker** să conțină modulele ''​Folds''​ și ''​Shallow''​.
 +
 +==== Notă pentru curioși (facultativ) ====
 +
 +Secțiunea prezintă niște informații suplimentare despre etapa 3, și nu afectează rezolvarea temei.
 +
 +Mecanismul de reducere din schelet poate fi generalizat în felul următor. În primul rând, ideea de definire a unui AST (precum ''​RegionAST''​) ca **punct fix al unui ''​Functor''​** (precum ''​RegionShape''​),​ poate fi surprinsă explicit:
 +
 +<code haskell>
 +newtype Fix f = C (f (Fix f))
 +
 +type TransformationAST = Fix TransformationShape
 +type RegionAST ​        = Fix RegionShape
 +</​code>​
 +
 +În acest fel, poate fi definită o **unică funcțională** ''​foldAST'',​ cu tipul mai general ''​Functor f => (f a -> a) -> Fix f -> a'',​ pentru care funcționalele ''​foldTransformationAST''​ și ''​foldRegionAST''​ devin cazuri particulare. Constrângerea ''​Functor f''​ este necesară pentru utilizarea lui ''​fmap''​ în implementare,​ ca în cerința temei, care știe să propage idiosincratic reducerea la substructuri. **//​Combiner//​-ul** are acum tipul mai general ''​f a -> a''​ și poartă numele de **algebră asociată functorului** ''​f''​.
 ===== Precizări ===== ===== Precizări =====
  
Line 166: Line 246:
 ===== Resurse ===== ===== Resurse =====
  
-  * [[https://​ocw.cs.pub.ro/​courses/​_media/​pp/24/teme/haskell/etapa1.zip|Schelet etapa 1]] +  * {{pp:24:teme:haskell:etapa1.zip|Schelet etapa 1}} 
-  * [[https://​ocw.cs.pub.ro/​courses/​_media/​pp/24/teme/haskell/etapa2.zip|Schelet etapa 2]]+  * {{pp:24:teme:haskell:etapa2.zip|Schelet etapa 2}} 
 +  * {{pp:​24:​teme:​haskell:​etapa3.zip|Schelet etapa 3}}
  
 ===== Referințe ===== ===== Referințe =====
  
-  *  de Moor, O., & Gibbons, J. (Eds.). (2003). //The Fun of Programming//​. Palgrave Macmillan.+  * de Moor, O., & Gibbons, J. (Eds.). (2003). //The Fun of Programming//​. Palgrave Macmillan
 +  * Gibbons, J., & Wu, N. (2014). Folding domain-specific languages: deep and shallow embeddings (Functional pearl). In //​Proceedings of the 19th ACM SIGPLAN international conference on Functional programming//​.
  
 ===== Changelog ===== ===== Changelog =====
  
 +  * 11.05 (12:05): Publicat checker etapa 3.
 +  * 06.05 (11:40): Publicat etapa 3, enunț și schelet; urmează checker-ul.
   * 23.04 (21:20): Publicat etapa 2.   * 23.04 (21:20): Publicat etapa 2.
   * 19.04 (10:25): Flexibilizat checker etapa 1 în privința comparațiilor pe numere reale, pentru reducerea riscului de eșec al testelor din cauza erorilor de aproximare. Dacă testele treceau oricum pe checker-ul anterior, nu este necesar să mai faceți nimic.   * 19.04 (10:25): Flexibilizat checker etapa 1 în privința comparațiilor pe numere reale, pentru reducerea riscului de eșec al testelor din cauza erorilor de aproximare. Dacă testele treceau oricum pe checker-ul anterior, nu este necesar să mai faceți nimic.
   * 16.04 (10:10): Încărcat checker etapa 1.   * 16.04 (10:10): Încărcat checker etapa 1.
   * 14.04 (17:00): Adăugat clarificare în comentariile funcției ''​combineTransformations'',​ pentru evidențierea corespondenței cu aplicările individuale ale transformărilor din listă.   * 14.04 (17:00): Adăugat clarificare în comentariile funcției ''​combineTransformations'',​ pentru evidențierea corespondenței cu aplicările individuale ale transformărilor din listă.
pp/24/teme/haskell-imagini-functionale.1713896445.txt.gz · Last modified: 2024/04/23 21:20 by mihnea.muraru
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0