This shows you the differences between two versions of the page.
pp:24:teme:haskell-imagini-functionale [2024/04/09 15:58] mihnea.muraru [Etapa 1] |
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: 09.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 72: | Line 72: | ||
Este suficient ca arhiva pentru **vmchecker** să conțină doar modulul ''Shallow''. | Este suficient ca arhiva pentru **vmchecker** să conțină doar modulul ''Shallow''. | ||
+ | ===== Etapa 2 ===== | ||
+ | |||
+ | În cadrul reprezentării concrete din etapa 1, funcțiile de **definire și manipulare** a regiunilor și transformărilor surprind **complexitatea** operațiilor, în timp ce funcția ''inside'', de **interogare** a regiunilor, are o implementare **banală**, în termenii reprezentării regiunilor ca funcții caracteristice. Această abordare are **avantajul** că **noi regiuni, transformări și operații** pe acestea pot fi **adăugate ușor**, independent de definițiile anterioare. De exemplu, dacă dorim să adăugăm o nouă regiune elementară, cum este un triunghi, sau o nouă operație, ca diferența a două regiuni, putem introduce funcții separate, fără a modifica definițiile existente. | ||
+ | |||
+ | Ce se întâmplă totuși dacă încercăm să **schimbăm interpretarea** unei regiuni? Spre exemplu, în loc de **funcția caracteristică** din etapa 1, care stabilește apartenența unui punct la o regiune, acum am dori ca printr-o regiune să înțelegem **numărul de subregiuni și transformări elementare** din descrierea acelei regiuni. De exemplu, regiunea ''union (circle 2) (applyTransformation (translation 1 0) (rectangle 2 2))'' ar căpăta reprezentarea ''(2, 1)'', pentru că sunt utilizate 2 subregiuni elementare (un cerc și un dreptunghi) și 1 transformare elementară (o translație) în construcția ei. Într-o altă situație, am putea dori ca printr-o regiune să înțelegem **descrierea textuală a operațiilor** care construiesc acea regiune. De exemplu, pentru aceeași regiune ca mai sus, reprezentarea textuală ar putea fi ''"Circle 2 + Translation 1 0 @ Rectangle 2 2"''. Această schimbare de interpretare s-ar realiza **anevoios**, fiind necesară **reimplementarea** tuturor funcțiilor existente în termenii noii interpretări. | ||
+ | |||
+ | În etapa 2, vom explora așa-numitele **//deep embeddings//** pentru regiuni și transformări, în sensul că funcțiile de definire și manipulare a regiunilor nu vor mai viza o anumită reprezentare concretă, ci vor genera un **arbore de sintaxă abstractă (AST)**, care surprinde operațiile generice utilizate în construcția unei regiuni sau transformări. De exemplu, regiunea de mai sus ar căpăta reprezentarea abstractă ''Union (Circle 2) (Transform (Translation 1 0) (Rectangle 2 2))'', unde simbolurile reprezintă constructori de date definiți de noi. Abordarea are următoarele **avantaje**: | ||
+ | |||
+ | * Putem adăuga **ușor** diverse **interpretări** ale acestui AST, în forma unor funcții, care să recupereze orice reprezentare concretă dorim (de exemplu, funcțiile caracteristice din etapa 1 sau celelalte două exemple de reprezentări de mai sus). Practic, situația este **inversată** față de etapa 1, în sensul că funcțiile de **definire și manipulare** vor avea de data aceasta implementări banale, în termenii unor constructori de date, în timp ce **complexitatea** operațiilor va fi surprinsă în funcțiile de **interpretare**. | ||
+ | * Avem posibilitatea **preprocesării** AST-ului înaintea interpretării lui, în sensul că regiunile și transformările pot fi simplificate. De exemplu, două translații succesive pot fi înlocuite cu una singură cu parametrii corespunzători. | ||
+ | |||
+ | Ca **dezavantaj**, adăugarea de **noi regiuni, transformări și operații** pe acestea se realizează dificil, fiind necesară modificarea **tipurilor** care descriu AST-urile, și în consecință extinderea tuturor **interpretărilor existente**. | ||
+ | |||
+ | Observați **dualismul** celor două abordări, bazate pe //shallow//, respectiv //deep embeddings//, în sensul că ce se realizează ușor într-una se implementează dificil în cealaltă, fără ca una dintre abordări să o domine pe cealaltă. În literatură, acest lucru poartă numele de **//problema expresivității//** (//[[https://en.wikipedia.org/wiki/Expression_problem|expression problem]]//), care se manifestă la mai multe niveluri, inclusiv între diferite paradigme de programare. | ||
+ | |||
+ | În cadrul etapei 2, veți porni de la tipuri de date deja definite în schelet pentru reprezentarea AST-urilor regiunilor și transformărilor, și **veți implementa** funcții care: | ||
+ | |||
+ | * **interpretează** AST-ul pentru a recupera reprezentările concrete din etapa 1 | ||
+ | * **descompun** secvențe imbricate de transformări (vezi ''combineTransformations'') în variante liniarizate | ||
+ | * **fuzionează** transformări elementare consecutive de același fel (translații cu translații și scalări cu scalări) | ||
+ | * **optimizează** (simplifică) transformările dintr-un AST, prin deplasare în sus și fuzionare. | ||
+ | |||
+ | Construcțiile și mecanismele noi de limbaj pe care le veți exploata în rezolvare, pe lângă cele din etapa 1, sunt: | ||
+ | |||
+ | * **tipurile de date utilizator**. | ||
+ | |||
+ | Întrucât **optimizarea** AST-ului constituie funcționalitatea cea mai pretențioasă din această etapă, o **exemplificăm**, pas cu pas, pentru AST-ul de mai jos (exemplul apare și în comentariile funcției ''optimizeTransformations''): | ||
+ | |||
+ | <code haskell> | ||
+ | Union (Transform (Combine [ Translation 1 2 | ||
+ | , Combine [ Translation 3 4 | ||
+ | , Scaling 2 | ||
+ | ] | ||
+ | , Scaling 3 | ||
+ | ]) | ||
+ | (Complement (Transform (Scaling 4) | ||
+ | (Transform (Scaling 2) (Circle 5))))) | ||
+ | (Transform (Translation 4 6) (Rectangle 6 7)) | ||
+ | </code> | ||
+ | |||
+ | Principiile de optimizare sunt descrise pe larg în schelet, dar întotdeauna când prelucrăm o anumită regiune, trebuie **să optimizăm mai întâi recursiv subregiunile**. Prima optimizare propriu-zisă se produce la nivelul subarborelui ''Transform (Scaling 4) (Transform (Scaling 2) (Circle 5))'', care conține două scalări succesive, care pot fi **alipite** în ''Transform (Scaling 8) (Circle 5)'', iar întregul AST este acum: | ||
+ | |||
+ | <code haskell> | ||
+ | Union (Transform (Combine [ Translation 1 2 | ||
+ | , Combine [ Translation 3 4 | ||
+ | , Scaling 2 | ||
+ | ] | ||
+ | , Scaling 3 | ||
+ | ]) | ||
+ | (Complement (Transform (Scaling 8) (Circle 5)))) | ||
+ | (Transform (Translation 4 6) (Rectangle 6 7)) | ||
+ | </code> | ||
+ | |||
+ | În continuare, mergem un nivel mai sus, la subarborele ''Complement (Transform (Scaling 8) (Circle 5))''. Pentru a permite eventuala alipire a scalării cu 8 cu o altă transformare de deasupra, este util să **extragem transformarea în fața complementului**, ca în ''Transform (Scaling 8) (Complement (Circle 5))'', iar întregul AST devine: | ||
+ | |||
+ | <code haskell> | ||
+ | Union (Transform (Combine [ Translation 1 2 | ||
+ | , Combine [ Translation 3 4 | ||
+ | , Scaling 2 | ||
+ | ] | ||
+ | , Scaling 3 | ||
+ | ]) | ||
+ | (Transform (Scaling 8) (Complement (Circle 5)))) | ||
+ | (Transform (Translation 4 6) (Rectangle 6 7)) | ||
+ | </code> | ||
+ | |||
+ | Mergem iarăși un nivel mai sus, la subarborele ''Transform (Combine ...) (Transform (Scaling 8) (Complement (Circle 5)))'', unde observăm că transformările din secvența ''Combine'', și anume două translații, respectiv două scalări consecutive, pot fi **alipite**; mai departe, scalările mai pot **fuziona** cu scalarea cu 8 din fața complementului. Din păcate, secvența ''Combine'' conține transformări **imbricate**, care trebuie mai întâi **liniarizate** pentru a putea fi ușor alipite. În urma liniarizării, se obține lista ''[Translation 1 2, Translation 3 4, Scaling 2, Scaling 3]'', iar dacă la aceasta **adăugăm și prima transformare de mai jos**, ''Scaling 8'', și fuzionăm, obținem lista ''[Translation 4 6, Scaling 48]'', iar întregul AST devine: | ||
+ | |||
+ | <code haskell> | ||
+ | Union (Transform (Combine [Translation 4 6, Scaling 48]) (Complement (Circle 5))) | ||
+ | (Transform (Translation 4 6) (Rectangle 6 7)) | ||
+ | </code> | ||
+ | |||
+ | Rămâne să prelucrăm întreaga reuniune. Obiectivul este același: să înlesnim eventuale alipiri cu transformări de deasupra, **extrăgând în fața reuniunii transformările de dedesubt**, dacă este posibil, cum am procedat mai sus la complement. Din moment ce avem acum două ramuri, nu putem să extragem decât **cel mai lung prefix de transformări comune**, lăsând sufixele necomune dedesubt. Prefixul cu pricina este ''[Translation 4 6]'', iar AST-ul devine în final: | ||
+ | |||
+ | <code haskell> | ||
+ | Transform (Translation 4 6) | ||
+ | (Union (Transform (Scaling 48) (Complement (Circle 5))) | ||
+ | (Rectangle 6 7)) | ||
+ | </code> | ||
+ | |||
+ | Modulul de interes din **schelet** este ''Deep'', care conține **reprezentarea** AST-urilor regiunilor și a transformărilor, 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 ''TestDeep'' și evaluați ''main''. | ||
+ | |||
+ | 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 79: | 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}} |
+ | * {{pp:24:teme:haskell:etapa2.zip|Schelet etapa 2}} | ||
+ | * {{pp:24:teme:haskell:etapa3.zip|Schelet etapa 3}} | ||
+ | |||
+ | ===== Referințe ===== | ||
+ | |||
+ | * 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 ===== | ||
+ | |||
+ | * 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. | ||
+ | * 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. | ||
+ | * 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ă. |