Lambda Calculus Interpreter

Schelet: 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: Lab 7. Lambda Calculus. Intro to Haskell

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ă $ 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, \ldots, x_{10}, x_{11}, \ldots $ ș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..])

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:

<expr> ::= <variable> | '\' <variable> '.' <expr> | <expr> <expr> | '(' <expr> ')'
<variable> ::= '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 <expr> este:

 "\x.x \y.y" 

Urmarind regulile de parsare de mai sus, un parser ar putea sa incerce sa aplice regula descrisa de <expr> <expr>, caz in care un nou parser de expresii de tip <expr> (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.

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.

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)

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 = <expr>

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.

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

Tema are un punctaj total de 1.5p din nota finală, împărțit pe subpuncte:

  1. 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
  2. Parsing
    • 50p - 2.1. parsare
  3. Steps towards a programming language
    • 10p - 3.1. evaluarea unui macro
    • 5p - 3.2. parsarea expresiilor cu macro-uri
  4. 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.

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.

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

Doar temele care trec de validatorul de arhive o să fie notate.