This is an old revision of the document!
Lambda Calculus Interpreter
TODO Adauga validatorul de arhive
- Temele trebuie submise pe curs.upb.ro, sub 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.
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 intuitiva de variable-capture: Variabila initial libera $ y$ a devenit legata 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. 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. 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. Implementați funcția stepN
care aplică un pas de reducere după strategia Normală.
1.4. Implementați funcțiile reduceN
și reduceAllN
care fac reducerea la forma normală folosind strategia Normală.
1.5. Implementați funcția stepA
care aplică un pas de reducere după strategia Aplicativă.
1.6. Implementați funcțiile reduceA
și reduceAllA
care fac reducerea la formă normală folosind strategia Aplicativă.
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:
<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. Implementați funcția parse_expr
care parsează un String
și returnează o expresie.
parse_expr
este la latitudinea voastră, însă noi recomandăm folosirea monadei Parser prezentată la curs. Pentru asta aveți definit în schelet o instantă Monad (unde puteți să completați funcțiile return
și »=
), alături de instanțe de Applicative și Functor, necesare pentru a defini o monadă în Haskell.
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. 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. Modificați parser-ul vostru astfel încât să parsați și expresii care conțin macro-uri.
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 = <expr>
4.1. 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. Implementați funcția parse_code
care să parsează o linie de cod.
parse_code :: String -> Code
Bonus
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. Pentru a obține acest bonus v-a trebuit realizat și acest task (care altfel nu are punctaj asociat).
Bonusul este simplu: folosind interpretorul implementat anterior, realizați un REPL prin care să fie folosit.
REPL poate să fie atât de simplu/complicat doriți, dar trebuie să prezinte cel putin următoarele caracteristici:
- să citească linii de cod (și să le evalueze, afisând forma normală)
- să păstreze un context (i.e. să pot să fac assign-uri și după să folosesc macro-urile definite)
- să ruleze continuu (după introducerea unei expresii și returnarea rezultatului, să aștepte o nouă expresie)
- să detecteze posilele erori de parsare și să nu dea eroare în cazul în care primește o expresie greșită
- Notă: nu trebuie neapărat să tratați cazul în care evaluarea efectivă a expresie dă Stack Overflow
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
Testing
Pentru testare puteți rula scriptul ./check.sh
, care v-a rula un set de teste unitare cu runhaskell checker.hs
și v-a calcula punctajul total. Puteți rula și direct runhaskell checker.hs
dar nu se va calcula punctajul total.
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.
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).