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.Default Library
Acum ca avem un interpretor functional pentru calcul lambda, hai sa definim si cateva expresii uzuale, ca sa le putem folosi ca un context default pentru interpretorul nostru.
In fisierul Default.hs
sunt deja definiti cativa combinatori. Hai sa definim si alte lucruri extra.
4.1. (5p) Definiti ca expresii Lambda cateva macro-uri utile pentru lucrul cu Booleene (TRUE
, FALSE
, AND
, OR
, NOT
, XOR
).
4.2. (5p) Definiti ca expresii Lambda cateva macro-uri utile pentru lucrul cu perechi (PAIR
, FIRST
, SECOND
).
4.3. (10p) Definiti ca expresii Lambda cateva macro-uri utile pentru lucrul cu numere naturale (N0
, N1
, N2
, SUCC
, PRED
, ADD
, SUB
, MULT
).
simplify
implementat de voi sa testam expresiile, deoarece vrem sa testam comportamentul lor, nu structura.
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.
Punctare
Tema are un punctaj total de 1.5p din nota finală, împărțit pe subpuncte:
- Evaluation
- 5p - 1.1. - aflarea variabilelor
- 5p - 1.2. - aflarea variabilelor libere
- 5p - 1.3. - generarea unei noi variabile
- 20p - 1.4. - reducerea unui redex
- 10p - 1.5. - step normal
- 10p - 1.6. - step aplicativ
- 5p - 1.7. - verificarea formei normale
- 10p - 1.8. - reducerea unei expresii step by step
- Parsing
- 50p - 2.1. parsare
- Steps towards a programming language
- 10p - 3.1. evaluarea unui macro
- 5p - 3.2. parsarea expresiilor cu macro-uri
- 5p - 3.3. parsarea liniilor de cod
- Code
- 5p - 4.1. expresii pt boolene
- 5p - 4.2. expresii perechi
- 10p - 4.2. expresii numere naturale
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 | code | default] 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.
Trimitere
Temele trebuie submise pe curs.upb.ro, în assignment-ul Tema 3
.
În arhivă trebuie să se regăsească:
- Code.hs
- Lambda.hs
- Parser.hs
- <alte fisiere>.hs
- ID.txt - acest fisier va contine o singura linie, formata din ID-ul unic al fiecarui student