Edit this page Backlinks This page is read only. You can view the source, but not change it. Ask your administrator if you think this is wrong. ====== Lambda Calculus Interpreter ====== <note> Schelet: TODO </note> <note warning> **Deadline:** TODO * Temele trebuie submise pe [[curs.upb.ro]], în assignment-ul ''Tema 3''. * Pentru întrebări folosiți forum-ul dedicat de pe [[curs.upb.ro]]. </note> În cadrul acestei teme va trebui să realizezi un interpretor de expresii lambda în //Haskell//. <note tip> Remember: [[pp:2024:l07|Lab 7. Lambda Calculus.]] </note> O sa definim o expresie lambda cu ajutorul urmatorului **TDA**: <code haskell> data Lambda = Var String | App Lambda Lambda | Abs String Lambda </code> <note important> In schelet, definitia **TDA**-ului contine si un constructor pentru un macro, pentru cerintele **1** si **2** il puteti ignora, o sa fie introdus in cadrul cerintei **3**. </note> Variabilele sunt declarate de tipul ''String'', pentru simplitate o sa considerăm variabile orice șir de caractere format numai din litere mici ale alfabetului englez. ===== 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 )$. <note> Î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)$. \\ </note> Pentru a detecta si rezolva //variable capture//, o sa pregatim cateva functii ajutatoare: \\ **1.1.** (//5p//) Implementați funcția auxiliară ''vars'' care returnează o listă cu toate ''String''-urile care reprezintă variabile într-o expresie. \\ **1.2.** (//5p//) 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.3.** (//10p//) Implementați funcția auxiliară ''new_vars'' care primeste o lista de ''String''-uri si intoarce cel mai mic ''String'' lexicografic care nu apare in lista (**e.g.** ''new_vars ["a", "b", "c"]'' o sa intoarca ''"d"''). ---- **1.4.** (//20p//) 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ă. \\ <code haskell> reduce :: String -> Lambda -> Lambda -> Lambda reduce x e_1 e_2 = undefined -- oriunde apare variabile x in e_1, este inlocuita cu e_2 </code> \\ 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ă. \\ **1.5.** (//10p//) Implementați funcția ''normal_step'' care aplică un pas de reducere după strategia Normală. \\ **1.6.** (//10p//) Implementați funcția ''applicative_step'' care aplică un pas de reducere după strategia Aplicativă. \\ **1.7.** (//5p//) Implementați funcția ''is_normal_form'' care verifica daca o expresie este în formă normală. \\ **1.8.** (//5p//) Implementați funcția ''simplify'', care primeste o functie de step si o aplica pana expresie ramane in forma normala, si intoarce o lista cu toti pasi intermediari ai reduceri. \\ ===== 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: \\ <code> <lambda> ::= <variable> | '\' <variable> '.' <lambda> | (<lambda> <lambda>) <variable> ::= 'a' | 'b' | 'c' | ... | 'z' </code> \\ **2.1.** (//50p//) Implementați funcția ''parse_lambda'' care parsează un ''String'' și returnează o expresie SAU o eroare (sub forma de ''String''). <note important> **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. </note> <note warning> Parserul care trebuie să îl implementați are definiția: <code haskell> newtype Parser a = Parser { parse :: String -> Maybe(a, String) } </code> 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''. </note> ===== 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). Pentru a folosi un macro, introducem sintaxa: orice șir de caractere format numai din litere mari ale alfabetului englez si cifre e considerat un macro. Câteva exemple de expresii cu macro-uri sunt: $ TRUE $ \\ $\lambda x.FALSE $ \\ $ \lambda x.(NOT \ \lambda y.AND) $ \\ 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 ''replace_macros'' care ia un context și o expresie care poate să conțină macro-uri, și întoarce expresia după evaluarea tuturor macro-urilor SAU o eroare in cazul in care o variabila nu a fost gasita (expresia returnata ar trebui sa nu mai contina macro-uri, ca sa putem folosi ''simplify'' implementat anterior). <note info> Codul atunci cand lucrezi cu ''Maybe'' sau ''Either'' poata sa devina complicat atunci cand faci ''case'' pe fiecare variabila sa verifici erorile, de asta exista o monada definita atat peste tipul de date ''Maybe'' cat si peste ''Either'', foloseste ''do'' notation sa iti usurezi viata. </note> <note tip> funcția ''lookup'' este foarte utilă pentru lucrul cu dicționare (liste de perechi) </note> Ca sa ne folosim de macro-uri ne trebuie si o metoda de a le defini. Pentru asta o sa definim conceptul de linie de cod: <code haskell> data Code = Code Lambda | Assign String Lambda </code> O linie de cod poate sa fie ori o expresie lambda, ori o definitie de macro. Astfel daca o sa evaluam mai multe linii de cod, in expresii o sa ne putem folosi de macro-urile definite anterior. **3.2.** (//5p//) Modificați parser-ul vostru astfel încât să parsați și expresii care conțin macro-uri. **3.3.** (//5p//) Implementați funcția ''parse_code'' care să parseze o linie de cod, daca gaseste erori o sa intoarca o eroare (sub forma de ''String''). ===== 4.Code ===== ===== 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 (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. <note important> Punctele pentru o anumită cerință le primești doar dacă trec **TOATE** testele pentru acea cerință, nu se punctează parțial. </note> <note warning> Pot să existe depunctări de până la **1.5p** pentru implementări hardcodate sau plagiat. </note> ===== 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). <note warning> Dacă implementarea unei funcții lipsește (sau apar alte erori) o să apară //„Error: ...”// în loc de **PASSED** / **FAILED**. </note> ===== 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: <code> python3 archive_validator.py <archive_name> </code> Numele arhivelor trebuie sa fie de forma **<Nume>_<Prenume>_<Grupa>_T3.zip** (daca aveti mai multe prenume sau nume, le puteti separa prin '-'). <note important> Doar temele care trec de validatorul de arhive o să fie notate. </note>