====== Lambda Calculus Interpreter ====== Schelet: {{:pp:2023:tema3.zip|}} **Deadline:** 28 mai, 23:59 * Temele trebuie submise pe [[curs.upb.ro]], în assignment-ul ''Tema 3''. * Temele care nu sunt acceptate de validatorul de arhive **NU** vor fi punctate. * Pentru întrebări folosiți forum-ul dedicat de pe [[curs.upb.ro]]. În cadrul acestei teme va trebui să realizezi un interpretor de expresii lambda în //Haskell//. Pentru asta aveți definit un **TDA**: data Expr = Variable String | Function String Expr | Application Expr Expr Variabilele sunt declarate de tipul ''String'', considerăm variabile orice șir de caractere format numai din litere mici ale alfabetului englez. Poți să faci referință la partea de calcul lambda din: [[pp:2023:haskell:l07|Lab 7. Lambda Calculus. Intro to Haskell]] ===== 1. Evaluation ===== Glosar de termeni: * //$\beta$-reducere// - //reducere// a unei lambda-expresii, in sensul in care a fost prezentat la curs. * **redex** - lambda-expresie de forma $( \lambda x.e_1 \ e_2 )$. Înainte de a reduce o expresie (realizarea $\beta$-reducerii), trebuie să rezolvăm //**coliziunile de nume**//. \\ Dacă am încerca să reducem un **redex** fără a face substituții textuale (un **redex** e o expresie reductibilă, **i.e.** are forma $( \lambda x.e_1 \ e_2 $) există riscul de a pierde întelesul original al expresiei. \\ Spre exemplu **redex**-ul: $(\lambda x.\lambda y.(x \ y) \ \lambda x.y)$, ar fi redus la: $\lambda y.(\lambda x.y \ y)$. Acest efect nedorit are denumirea intuitivă de //variable-capture//: Variabila inițial liberă $math[y] a devenit legată dupa reducere. \\ Puteți observa că expresia și-a pierdut sensul original, pentru că **y**-ul liber din $\lambda x.y$ e acum //bound// de $\lambda y.$ din expresia în care a fost înlocuit. \\ Astfel, expresia corecta ar fi: $\lambda a.(\lambda x.y \ a)$. \\ \\ **1.1.** (//10p//) Implementați funcția auxiliară ''free_vars'' care returnează o listă cu toate ''String''-urile care reprezintă variabile libere într-o expresie. (**notă**: dacă o variabilă este liberă în expresie în mai multe contexte, o să apară o singură dată în listă). \\ **1.2.** (//30p//) Implementați funcția ''reduce'' care reduce un **redex** luând în considerare și //**coliziunile de nume**//. Funcția primește **redex**-ul 'deconstruit' și returnează expresia rezultată. \\ reduce :: Expr -> String -> Expr -> Expr reduce e_1 x e_2 = undefined -- oriunde apare variabile x in e_1, este inlocuita cu e_2 \\ Acum că putem reduce un **redex**, vrem să reducem o expresie la forma ei normală. Pentru asta trebuie să implementăm o strategie de alegere a **redex**-ului care urmează să fie redus, și să o aplicăm până nu mai există niciun **redex**. În această temă o să implementăm 2 strategii: **Normală** și **Aplicativă** (studiate la curs). * **Normală**: se alege cel mai exterior, cel mai din stânga **redex** * **Aplicativă**: se alege cel mai interior, cel mai din stânga **redex** O să facem reducerea „step by step”, implementăm o funcție care reduce doar următorul **redex** comform unei strategii. Apoi aplicăm acesți pași până expresia rămasă este în formă normală. Pentru asta o să implementați 2 funcții, una care returnează doar forma normală a expresiei, și una care returnează o listă cu toate formele intermediare (rezultatul după fiecare pas de reducere). \\ \\ **1.3.** (//5p//) Implementați funcția ''stepN'' care aplică un pas de reducere după strategia Normală. \\ **1.4.** (//10p//) Implementați funcțiile ''reduceN'' și ''reduceAllN'' care fac reducerea la forma normală folosind strategia Normală. \\ **1.5.** (//5p//) Implementați funcția ''stepA'' care aplică un pas de reducere după strategia Aplicativă. \\ **1.6.** (//10p//) Implementați funcțiile ''reduceA'' și ''reduceAllA'' care fac reducerea la formă normală folosind strategia Aplicativă. \\ Când faceți substituția textuală, trebuie să vă asigurați că noile denumiri nu există deja în expresie, pentru asta puteți folosi denumiri imposibile de parsat (care conțin numere de exemplu), dar tot trebuie să verificați că nu ați folosit aceași denumire pentru substituție în trecut. O soluție ar fi să folosiți: $ x_1, x_2, x_3, ..., x_{10}, x_{11}, ... $ și să vericați să nu apară denumirea în corpul sau parametrul funcției. Recomandăm numere doar pentru că sunt mai ușor de generat ca un stream inifinit ca șirurile de caractere. (folosind **[1..]**) ===== 2. Parsing ===== Momentan putem să evaluăm expresii care le definim tot noi sub formă de cod, pentru a avea un interpretor funcțional, trebuie să putem lua expresii sub forma de șiruri de caractere și să le transformăm în **TDA**-uri (acest proces se numește **parsare**). \\ O gramatică pentru expresii lambda ar putea fi: \\ ::= | '\' '.' | | '(' ')' ::= 'a' | 'b' | 'c' | ... | 'z' \\ Această gramatică exprimă corect structura expresiilor lambda, dar nu este practică pentru o implementare, pentru că procesul de parsare se poate bloca intr-o bucla infinita. Sa presupunem ca expresia noastra '''' este: "\x.x \y.y" Urmarind regulile de parsare de mai sus, un parser ar putea sa incerce sa aplice regula descrisa de '' '', caz in care un nou parser de expresii de tip '''' (pentru sub-expresia din stanga) va fi invocat. Absenta **progresului** (nici un sir de la input nu a fost consumat), va conduce procesul de parsare intr-o bucla infinita. Pentru o parsare corectă și eficientă trebuie să definești o gramatică care face progres la fiecare pas. \\ ** Alege o organizare a gramaticii pentru lambda-expresii de asa maniera ca la fiecare pas, o portiune din sir sa fie consumata**. /* O gramatică care face progres și o puteți folosi la temă este: ::= [] ::= | | '(' ')' = ::= '\' '.' ::= 'a' | 'b' | 'c' | ... | 'z' */ **2.1.** (//50p//) Implementați funcția ''parse_expr'' care parsează un ''String'' și returnează o expresie. **NU** aveți voie să schimbați structura parserului, o soluție care nu se folosește de tipul de date ''Parser'' din schelet, nu o să fie punctată pentru cerințele de parsare Parserul care trebuie să îl implementați are definiția: newtype Parser a = Parser { parse :: String -> Maybe(a, String) } Obervați că tipul care îl întoarce funcția de parsare este ''Maybe(a, String)'', el întoarce ''Nothing'' dacă nu a putut parsa expresia sau ''Just (x, s)'' dacă a parsat ''x'', iar din String-ul original a rămas sufix-ul ''s''. ===== 3. Steps towards a programming language ===== Teoretic, folosind parserul și evaluatorul anterior, putem să evaluăm orice rezultat computabil, expresiile lambda fiind suficient de expresive, însă este foarte greu să scrii astfel de expresii. Pentru a fi mai ușor de folosit, vrem să introducem noțiunea de variabile. Pentru asta o să folosim conceptul de **macro**. Primul pas ar fi să extindem definiția unei expresii cu un constructor ''Macro'' care acceptă un ''String'' ca parametru (denumirea macro-ului). Odată cu extinderea definiției va trebui să completăm (în fișierul ''Expr.hs'') și definițiile pentru shorthand și instanțele ''Show'' și ''Eq''. Pentru a folosi un macro, introducem sintaxa: orice nume de variabilă (șir de litere mici ale alfabetului) precedat de un '$' e considerat un macro. Câteva exemple de expresii cu macro-uri sunt: $ \$ macro $ \\ $\lambda x.\$ macro $ \\ $ \lambda x.(\$ a \ \lambda y.\$ b) $ \\ Pentru a evalua o expresie cu macro-uri, introducem noțiunea de //**context computațional**//. Contextul în care evaluăm o expresie este pur și simplu un dicționar de nume de macro-uri și expresii pe care aceste nume le înlocuiesc. Astfel când evaluăm un macro, facem pur și simpu substituție textuală cu expresia găsită în dicționar. **3.1.** (//10p//) Implementați funcția ''evalMacros'' care ia un context și o expresie care poate să conțină macro-uri, și întoarce expresia după evaluarea tuturor macro-urilor. **3.2.** (//5p//) Modificați parser-ul vostru astfel încât să parsați și expresii care conțin macro-uri. funcția ''lookup'' este foarte utilă pentru lucrul cu dicționare (liste de perechi) ===== 4.Code ===== Acum că avem macro-uri, trebuie să avem o modalitatea de a le defini. Pentru asta introducem un nou concept, cel de linie de cod. Definim un nou **TDA**: data Code = Evaluate Expr | Assign String Expr deriving (Eq, Show) O linie de cod are 2 posibili constructori: ''Evaluate'', care e doar o expresie pe care o evaluează și printează rezultatul (similar cu un limbaj ca **MATLAB** în care expresiile care nu se termină cu ; sunt afișate în consolă) și ''Assign'' care definește un macro. Sintaxa pentru ''Evaluate'' este exact aceași ca pentru o expresie, iar pentru ''Assign'' este: numemacro = Un exemplu de cod care l-am putea scrie ar fi: $ true = \lambda x.\lambda y.x $ $ false = \lambda x.\lambda y.y $ $ and = \lambda x.\lambda y.(x \ y \ x) $ $ \$ and \ \$ true \ \$ false $ Care ar rezulta doar în afișarea rezultatului ultimei expresii: $ \lambda x.\lambda y.y $ **4.1.** (//10p//) Implementați funcția ''evalCode'' care primește o strategie de evaluare și o lista de linii de cod, și întoarce o listă cu rezultatele tuturor liniilor de tipul ''Evaluate''. **4.2.** (//5p//) Implementați funcția ''parse_code'' care să parsează o linie de cod. parse_code :: String -> Code Spre deosebire de o expresie lambda, unde un spațiu alb se află doar între 2 expresii și desemnează aplicația, între numele macro-ului, egal și expresie pot exista oricâte spații albe. ===== REPL ===== La finalul temei, o să puteți să rulați ''runhaskell main.hs'' pentru a porni un **REPL**, care se folosește de funcțiile și parserul făcute de voi. În acesta puteți să evaluați diverse expresii lambda, cum a fost prezentat anterior. Pentru a fi mai ușor de utilizat, există un context default în care pornește, cu macro-uri deja definite pentru câteva expresii uzuale (expresii boolene, combinatori). ===== Punctare ===== Tema are un punctaj total de **1.5p** din nota finală, împărțit pe subpuncte: - Evaluation * 10p - **1.1.** - aflarea variabilelor libere * 30p - **1.2.** - reducerea unui redex * 15p - **1.3.** + **1.4.** - reducere normala * 15p - **1.5.** + **1.6.** - reduce aplicativa - Parsing * 50p - **2.1.** parsare - Steps towards a programming language * 10p - **3.1.** evaluarea unui macro * 5p - **3.2.** parsarea expresiilor cu macro-uri - Code * 10p - **4.1.** evaluarea unei secvente de cod * 5p - **4.2.** parsarea de linii de cod După cum s-a anunțat la începutul semestrului, pentru studenții care au punctaj maxim pe toate 3 temele de pe parcursul semestrului, o să se echivaleze examenul din sesiune cu punctaj maxim. Punctele pentru o anumită cerință le primești doar dacă trec **TOATE** testele pentru acea cerință, nu se punctează parțial. Pot să existe depunctări de până la **1.5p** pentru implementări hardcodate sau plagiat. ===== Testing ===== Pentru testare puteți rula un set de teste unitare cu ''runhaskell test.hs''. Pentru a testa doar o cerință, puteți da unul din argumentele [lambda | parser | macro | code] pentru a rula cerințele 1, 2, 3 sau 4. Pentru fiecare test v-a aparea **PASSED** / **FAILED**, și în caz de **FAILED**, diferențele între rezultatul vostru și cel dorit. Dacă trec toate testele pentru o cerință, o să apară punctajul (**+X points**), suma tuturor astfel de display-uri este punctajul final. Aveți la dispoziție și un script, ''check.sh'', care vă calculează punctajul final (din 150). Pentru testarea manuală, puteți să folosiți: '':l all.hs'' din **ghci**, care v-a încărca toate fișierele necesare (''Expr.hs'', ''Lambda.hs'', ''Parser.hs'') plus un fișier ajutător (''Tests/Examples.hs''), în care se află diverse expresii deja declarate (care au fost folosite și in teste). Dacă implementarea unei funcții lipsește (sau apar alte erori) o să apară //„Error: ...”// în loc de **PASSED** / **FAILED**. ===== Trimitere ===== Temele trebuie submise pe curs.upb.ro, în assignment-ul ''Tema 3''. În arhivă trebuie să se regăsească doar: * Expr.hs * Lambda.hs * Parser.hs * main.hs * ID.txt - acest fisier va contine o singura linie, formata din ID-ul unic al fiecarui student Pentru a verifica format-ul arhivei, aveți în schelet un script în python care face asta: python3 archive_validator.py Numele arhivelor trebuie sa fie de forma **___T3.zip** (daca aveti mai multe prenume sau nume, le puteti separa prin '-'). Doar temele care trec de validatorul de arhive o să fie notate.