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> **TODO** Adauga schelet \\ **TODO** Adauga validatorul de arhive </note> <note important> **Deadline:** 28 mai, 23:59 * 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]]. </note> În cadrul acestei teme va trebui să realizezi un interpretor de expresii lambda în //Haskell//. Pentru asta aveți definit un **TDA**: <code haskell> data Expr = Variable String | Function String Expr | Application Expr Expr </code> Variabilele sunt declarate de tipul ''String'', considerăm variabile orice șir de caractere format numai din litere mici ale alfabetului englez. <note tip> Poți să faci referință la partea de calcul lambda din: [[pp:2023:haskell:l07|Lab 7. Lambda Calculus. Intro to Haskell]] </note> ===== 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 $math[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ă. \\ <code haskell> reduce :: Expr -> String -> Expr -> Expr reduce e_1 x 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ă. 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ă. \\ <note important> 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, ..., x_{10}, x_{11}, ... $ ș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..]**) </note> ===== 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> <expr> ::= <variable> | \.<variable> <expr> | <expr> <expr> | (<expr>) <variable> ::= 'a' | 'b' | 'c' | ... | 'z' </code> \\ 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: <code> "\x.x \y.y" </code> 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**. /* O gramatică care face progres și o puteți folosi la temă este: <code> <app> ::= [<expr>] <expr> ::= <lambda> | <variable> | '(' <app> ')' <lambda> = ::= '\' <variable> '.' <expr> <variable> ::= 'a' | 'b' | 'c' | ... | 'z' </code> */ **2.1.** Implementați funcția ''parse_expr'' care parsează un ''String'' și returnează o expresie. <note tip> Implementarea funcției ''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//. </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). 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. <note tip> funcția ''lookup'' este foarte utilă pentru lucrul cu dicționare (liste de perechi) </note> ===== 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**: <code haskell> data Code = Evaluate Expr | Assign String Expr deriving (Eq, Show) </code> 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: <code> numemacro = <expr> </code> **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. <code haskell> parse_code :: String -> Code </code> <note important> 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. </note> ===== 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// <note important> Pentru acordarea bonusului, acesta trebuie prezentat asistentului de laborator. </note> ===== 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 <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 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). <note warning> dacă implementarea unei funcții lipsește (sau apar alte erori) o să apară //„Error: ...”// în loc de **PASSED** / **FAILED**. </note>