This is an old revision of the document!
Lambda Calculus Interpreter
- 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.
În cadrul acestei teme va trebui să realizezi un interpretor de expresii lambda în Haskell.
O sa definim o expresie lambda cu ajutorul urmatorului TDA:
data Lambda = Var String | App Lambda Lambda | Abs String Lambda
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 )$.
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ă $ 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)$.
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ă.
reduce :: String -> Lambda -> Lambda -> Lambda reduce x e_1 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ă.
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:
<lambda> ::= <variable> | '\' <variable> '.' <lambda> | (<lambda> <lambda>) <variable> ::= 'a' | 'b' | 'c' | ... | 'z'
2.1. (50p) Implementați funcția parse_lambda
care parsează un String
și returnează o expresie SAU o eroare (sub forma de String
).
Parser
din schelet, nu o să fie punctată pentru cerințele de parsare.
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).
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).
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.
lookup
este foarte utilă pentru lucrul cu dicționare (liste de perechi)
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:
data Code = Code Lambda | Assign String Lambda
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.
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).
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 <archive_name>
Numele arhivelor trebuie sa fie de forma <Nume>_<Prenume>_<Grupa>_T3.zip (daca aveti mai multe prenume sau nume, le puteti separa prin '-').