Haskell: Imagini funcționale

  • Data publicării: 09.04.2024
  • Data ultimei modificări: 11.05.2024
  • Deadline hard: ziua laboratorului 10

Obiective

  • Aplicarea mecanismelor funcționale, de tipuri (inclusiv polimorfism) și de evaluare leneșă din limbajul Haskell.
  • Exploatarea evaluării leneșe pentru decuplarea conceptuală a prelucrărilor realizate.

Descriere generală și organizare

În literatura de specialitate, embedded domain-specific languages (EDSL) sunt considerate unele dintre principalele aplicații ale programării funcționale. Sintagma merită explicată pe componente. Un domain-specific language (DSL) este un limbaj de programare restrâns, dedicat unui domeniu particular; exemple bine-cunoscute sunt HTML (pentru descrierea paginilor web) și SQL (pentru interogarea bazelor de date). Acestea se opun așa-ziselor general-purpose languages, care pot fi utilizate într-o gamă largă de domenii, fiind cazul limbajelor C, Java, Python, și al limbajelor studiate la PP. Deși restrânse, DSL-urile sunt înzestrate cu o serie de operații expresive dedicate, care simplifică masiv modelarea în cadrul domeniului specific către care au fost țintite.

Pentru implementarea unui DSL, există următoarele două variante:

  • stand-alone: un interpretor sau un compilator sunt scrise de la zero pentru limbajul respectiv, necesitând o cantitate mare de efort
  • embedded: limbajul este integrat sub forma unei biblioteci într-un limbaj existent (de exemplu, Haskell), valorificându-se astfel construcțiile limbajului gazdă, precum și interpretorul și/sau compilatorul existent.

Continuând pe varianta embedded (de exemplu, în Haskell), un DSL poate utiliza două tipuri de reprezentări în cadrul limbajului gazdă:

  • shallow embeddings: obiectele din cadrul DSL-ului au reprezentări concrete în limbajul gazdă; de exemplu, dacă dorim să creăm un mic limbaj pentru definirea expresiilor aritmetice, fiecare expresie ar putea fi reprezentată prin valoarea ei (Int etc.), iar operațiile pe expresii (adunare etc.) manipulează valorile acestora
  • deep embeddings: obiectele din cadrul DSL-ului au reprezentări abstracte, ca descrieri ale operațiilor care construiesc acele obiecte; continuând exemplul expresiilor aritmetice de mai sus, fiecare expresie poate fi reprezentată prin arborele ei sintactic, care poate fi interpretat ulterior în diverse moduri; o posibilă interpretare vizează obținerea valorii expresiei, realizând astfel corespondența cu shallow embedding-ul de mai sus.

Tema propune ca studiu de caz un mic DSL embedded în Haskell pentru lucrul cu imagini. Pe parcursul etapelor, vom explora ambele modalități de reprezentare de mai sus (shallow, respectiv deep), avantajele și dezavantajele fiecăreia, precum și corespondența lor conceptuală.

Tema este împărțită în 3 etape:

  • una pe care o veți rezolva după laboratorul 7, cu deadline în ziua laboratorului 8
  • una pe care o veți rezolva după laboratorul 8, cu deadline în ziua laboratorului 9
  • una pe care o veți rezolva după laboratorul 9, cu deadline în ziua laboratorului 10.

Deadline-ul depinde de semigrupa în care sunteți repartizați. Restanțierii care refac tema și nu refac laboratorul beneficiază de ultimul deadline, și anume în zilele de 26.04, 10.05, respectiv 17.05.

Rezolvările tuturor etapelor pot fi trimise până în ziua laboratorului 10 (deadline hard pentru toate etapele). Orice exercițiu trimis după un deadline soft se punctează la jumătate. Cu alte cuvinte, nota finală pe etapă se calculează conform formulei: n = (n1 + n2) / 2 (n1 = nota obținută înainte de deadline; n2 = nota obținută după deadline). Când toate submisiile preced deadline-ul, nota pe ultima submisie constituie nota finală (întrucât n1 = n2).

În fiecare etapă, veți valorifica ce ați învățat în săptămâna anterioară și veți avea la dispoziție un schelet de cod, cu toate că vor exista trimiteri la etapele anterioare. Enunțul caută să ofere o imagine de ansamblu atât la nivel conceptual, cât și în privința aspectelor care se doresc implementate, în timp ce detaliile se găsesc direct în schelet.

Etapa 1

În această etapă, ne vom concentra pe o reprezentare concretă a imaginilor din cadrul DSL-ului menționat anterior, corespunzătoare ideii de shallow embeddings. Astfel, fiecare imagine, denumită de acum încolo regiune (bidimensională), va fi reprezentată prin funcția ei caracteristică, având tipul Point → Bool. Rolul acestei funcții este de a preciza care puncte din spațiu aparțin regiunii respective (rezultat True) și care nu (rezultat False). Similar, o transformare a unei regiuni (de exemplu, translație), este reprezentată printr-o funcție care operează la nivelul punctelor, având tipul Point → Point.

Veți implementa funcții care:

  • verifică apartenența unui punct la o regiune
  • definesc regiuni simple (dreptunghi, cerc)
  • combină regiuni
  • desenează regiuni la consolă
  • definesc transformări simple (translație, scalare)
  • combină transformări
  • aplică transformări asupra regiunilor
  • pentru bonus, determină lungimea minimă a unui drum dintre două puncte, evitând o regiune dată, utilizând BFS în spațiul bidimensional.

Deși numărul funcțiilor este mai mare, majoritatea se implementează în câteva cuvinte. cicles și infiniteCircles sunt deja implementate, dar sarcina este de a înțelege utilitatea evaluării leneșe în cadrul acestora.

Construcțiile și mecanismele de limbaj pe care le veți exploata în rezolvare sunt:

  • liste
  • funcționale pe liste și nu numai, ocazie cu care veți intra în contact atât cu cele standard, care se găsesc și în Racket, cât și cu altele specifice în Haskell
  • list comprehensions, pentru descrierea concisă a unor prelucrări
  • evaluare leneșă, implicită în Haskell, pentru decuplarea conceptuală a transformărilor din cadrul unei secvențe
  • pattern matching, pentru radiografierea structurilor (perechi etc.)
  • de legare a variabilelor locale (let sau where).

Modulul de interes din schelet este Shallow, care conține reprezentarea regiunilor și a transformărilor, precum și operațiile pe care trebuie să le implementați. 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 TestShallow și evaluați main.

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 avantajulnoi 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 (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):

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

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:

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

Î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:

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

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:

Union (Transform (Combine [Translation 4 6, Scaling 48]) (Complement (Circle 5)))
      (Transform (Translation 4 6) (Rectangle 6 7))

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:

Transform (Translation 4 6)
          (Union (Transform (Scaling 48) (Complement (Circle 5)))
                 (Rectangle 6 7))

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:

  1. Fiecărui shallow embedding îi corespunde o interpretare a deep embeddings și viceversa sau relația dintre ele este mai complicată?
  2. 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:

  1. 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.
  2. 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.
  3. 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:

newtype Fix f = C (f (Fix f))
 
type TransformationAST = Fix TransformationShape
type RegionAST         = Fix RegionShape

Î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

  • Încercați să folosiți pe cât posibil funcții predefinite din modulul Data.List. Este foarte posibil ca o funcție de prelucrare de care aveți nevoie să fie deja definită aici.
  • Ca sugestie, exploatați cu încredere pattern matching, case și gărzi, în locul if-urilor imbricate.

Resurse

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ă.
pp/24/teme/haskell-imagini-functionale.txt · Last modified: 2024/05/11 22:42 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