Ohjelmoinnin peruskurssien laaja oppimäärä Luento 8: Tulkki: proseduurit, abstrakti syntaksi, quote ja cond (mm. SICP 44.1.5 osin) Riku Saikkonen 15. 11. 2011
Sisältö 1 Argumentittomat proseduurit ja käyttöliittymä 2 Abstrakti syntaksi ja env-argumentti 3 Lisäabstraktio evaliin 4 quote ja cond
Lisätään argumentittomat proseduurit ja begin 1/2 lisätään tulkkiimme proseduurit (taas Schemen näköisellä syntaksilla), mutta ei mietitä paikallisia muuttujia vielä tällä tulkilla voi teoriassa jo laskea mitä tahansa (sillä tulkattava kieli on Turing-täydellinen, jos numeroarvoilla ei ole ylärajaa), mutta pelkillä globaaleilla muuttujilla koodi on BASIC-mäistä... Testiajoja (koodi seuraavalla kalvolla) nelilaskin6.scm (eval '(define fact-iter ; vrt. factorial SICP 3.1.3:n lopussa (lambda () ; argumenttilistaa ei vielä käytetä mihinkään (if (> c n) p (begin (set! p (* c p)) (set! c (+ c 1)) (fact-iter)))))) (eval '(define p 1)) (eval '(define c 1)) (eval '(define n 5)) (eval '(fact-iter)) 120
Lisätään argumentittomat proseduurit ja begin 2/2 Muutokset edelliseen koodiin (tämä riittää!) nelilaskin6.scm (define (eval-sequence exps) ; suorittaa exps-lausekkeet järjestyksessä (cond ((null? (cdr exps)) ; onko viimeinen lauseke? (eval (car exps))) (else (eval (car exps)) (eval-sequence (cdr exps))))) (define (eval exp) (cond... ((and (pair? exp) (eq? (car exp) 'lambda)) (list 'procedure (cddr exp))) ; proseduurin ''arvo'' ((and (pair? exp) (eq? (car exp) 'begin)) (eval-sequence (cdr exp))) ((pair? exp) (let ((proc (eval (car exp)))) (if (and (pair? proc) (eq? (car proc) 'procedure)) (eval-sequence (cadr proc)) (proc (eval (cadr exp)) (eval (caddr exp))))))... ))
Lisätään vielä käyttöliittymä (SICP 4.1.4) jotta tulkkimme näyttäisi tulkilta, lisätään siihen vielä käyttöliittymä eli silmukka, joka lukee lausekkeen, suorittaa sen, ja kertoo laskennan tuloksen eli paluuarvon Schemessä (Lispissä) tällainen on nimeltään readevalprint loop tämä on helppoa, koska Schemessä on read-primitiivi Lisäyksiä edelliseen koodiin nelilaskin7.scm (define input-prompt ";;; Eval input:") (define output-prompt ";;; Eval value:") (define (driver-loop) (prompt-for-input input-prompt) (let ((input (read))) (let ((output (eval input))) (announce-output output-prompt) (display output))) (driver-loop)) (define (prompt-for-input string) (newline) (newline) (display string) (newline)) (define (announce-output string) (newline) (display string) (newline))
Sisältö 1 Argumentittomat proseduurit ja käyttöliittymä 2 Abstrakti syntaksi ja env-argumentti 3 Lisäabstraktio evaliin 4 quote ja cond
Mitä seuraavaksi? tehdään seuraavaksi edellisen luennon tulkkiin abstraktiot tulkattavan kielen syntaksin käsittelylle, jotta syntaksia olisi periaatteessa helppo muuttaa samalla koodi muuttuu lähemmäs SICP-kirjan tulkin koodia... pidetään tulkin toiminta muuten täsmälleen samana, mutta lisätään tässä samalla tuki: else-haarattomalle if:lle (palauttaa false) primitiiveille, joilla on enemmän tai vähemmän kuin kaksi argumenttia (define (f)...)-syntaksille: kuten Schemessä, se on sama kuin (define f (lambda ()...)) luovutaan myös globaaleista muuttujista variable-names ja variable-values ja kuljetetaan niitä tulkkiproseduurien uudessa argumentissa env toistaiseksi env osoittaa koko ajan samaan tietorakenteeseen (tätä muokataan myöhemmin paikallisten muuttujien tukemiseksi)
Entinen eval Koko eval ennen tätä muutosta nelilaskin7.scm (define (eval exp) (cond ((number? exp) exp) ((symbol? exp) (lookup-variable-value exp)) ((and (pair? exp) (eq? (car exp) 'set!)) (set-variable-value! (cadr exp) (eval (caddr exp)))) ((and (pair? exp) (eq? (car exp) 'define)) (define-variable! (cadr exp) (eval (caddr exp)))) ((and (pair? exp) (eq? (car exp) 'if)) (if (true? (eval (cadr exp))) (eval (caddr exp)) (eval (cadddr exp)))) ((and (pair? exp) (eq? (car exp) 'lambda)) (list 'procedure (cddr exp))) ((and (pair? exp) (eq? (car exp) 'begin)) (eval-sequence (cdr exp))) ((pair? exp) (let ((proc (eval (car exp)))) (if (and (pair? proc) (eq? (car proc) 'procedure)) (eval-sequence (cadr proc)) (proc (eval (cadr exp)) (eval (caddr exp)))))) (else (error "Invalid expression -- EVAL" exp))))
Uusi eval Koko eval tämän muutoksen jälkeen nelilaskin8.scm (define (eval exp env) (cond ((self-evaluating? exp) exp) ((variable? exp) (lookup-variable-value exp env)) ((assignment? exp) (set-variable-value! (assignment-variable exp) (eval (assignment-value exp) env) env)) ((definition? exp) (define-variable! (definition-variable exp) (eval (definition-value exp) env) env)) ((if? exp) (if (true? (eval (if-predicate exp) env)) (eval (if-consequent exp) env) (eval (if-alternative exp) env))) ((lambda? exp) (make-procedure (lambda-body exp))) ((begin? exp) (eval-sequence (begin-actions exp) env)) ((application? exp) (let ((proc (eval (operator exp) env))) (if (compound-procedure? proc) (eval-sequence (procedure-body proc) env) (apply proc (map (lambda (exp) (eval exp env)) (operands exp)))))) (else (error "Unknown expression type -- EVAL" exp))))
Syntaksiabstraktioita 1/3 (SICP 4.1.2) Evalin apuproseduureja (samoja kuin kirjassa) nelilaskin8.scm (define (self-evaluating? exp) (cond ((number? exp) true) ((string? exp) true) (else false))) (define (variable? exp) (symbol? exp)) (define (tagged-list? exp tag) (if (pair? exp) (eq? (car exp) tag) false)) (define (assignment? exp) (tagged-list? exp 'set!)) (define (assignment-variable exp) (cadr exp)) (define (assignment-value exp) (caddr exp)) (define (definition? exp) (tagged-list? exp 'define)) (define (definition-variable exp) (if (symbol? (cadr exp)) (cadr exp) ; esim. (define a 3) a (caadr exp))) ; esim. (define (f) 5) f (define (definition-value exp) (if (symbol? (cadr exp)) (caddr exp) (make-lambda (cdadr exp) (cddr exp))))
Syntaksiabstraktioita 2/3 (SICP 4.1.1, 4.1.2) Evalin apuproseduureja (samoja kuin kirjassa) nelilaskin8.scm (define (true? x) (not (eq? x false))) (define (if? exp) (tagged-list? exp 'if)) (define (if-predicate exp) (cadr exp)) (define (if-consequent exp) (caddr exp)) (define (if-alternative exp) (if (not (null? (cdddr exp))) ; onko else-haaraa? (cadddr exp) 'false)) ; 'false on tulkattavan kielen koodia! (define (begin? exp) (tagged-list? exp 'begin)) (define (begin-actions exp) (cdr exp)) (define (last-exp? seq) (null? (cdr seq))) (define (first-exp seq) (car seq)) (define (rest-exps seq) (cdr seq)) (define (eval-sequence exps env) (cond ((last-exp? exps) (eval (first-exp exps) env)) (else (eval (first-exp exps) env) (eval-sequence (rest-exps exps) env)))) (define (application? exp) (pair? exp)) (define (operator exp) (car exp)) (define (operands exp) (cdr exp))
Syntaksiabstraktioita 3/3 (SICP 4.1.2, 4.1.3) Evalin apuproseduureja (eri kuin kirjassa) nelilaskin8.scm ;; Tulkattavan kielen lambda-lauseke (define (lambda? exp) (tagged-list? exp 'lambda)) (define (lambda-body exp) (cddr exp)) (define (make-lambda parameters body) (cons 'lambda (cons parameters body))) ;; Tulkin muistiin talletettu proseduuriolio (define (compound-procedure? p) (tagged-list? p 'procedure)) (define (procedure-body p) (cadr p)) (define (make-procedure body) (list 'procedure body)) nämä ovat vielä erilaisia kuin kirjassa, koska argumentteja ja paikallisia muuttujia ei tueta niitä varten make-procedure:n pitäisi liittää proseduuriin paikallisten muuttujien arvot eli nk. ympäristö
Primitiivit ja globaalit muuttujat 1/2 siirrytään siis globaaleista muuttujista variable-names ja variable-values tietorakenteeseen env env on vielä erilainen kuin kirjassa (ei paikallisia muuttujia) toistaiseksi env-argumentti on siis koko ajan sama siirretään samalla primitiivit takaisin omaan listaansa mutta nyt ne kopioidaan sieltä env-rakenteeseen (koodi alla) Primitiivit ja uuden ympäristön alustus nelilaskin8.scm (define primitive-procedures (list (list '+ +) (list '- -) (list '* *) (list '/ /) (list '< <) (list '= =) (list '> >))) (define (primitive-procedure-names) (map car primitive-procedures)) (define (primitive-procedure-objects) (map cadr primitive-procedures)) (define (setup-environment) ; tekee uuden env-rakenteen eli ympäristön (let ((initial-env (cons (primitive-procedure-names) (primitive-procedure-objects)))) (define-variable! 'true true initial-env) (define-variable! 'false false initial-env) initial-env))
Primitiivit ja globaalit muuttujat 2/2 Muuttujan haku ja muuttaminen env:stä (define (define-variable! var val env) (set-car! env (cons var (car env))) (set-cdr! env (cons val (cdr env)))) nelilaskin8.scm (define (lookup-variable-value var env) (define (scan names values) (cond ((null? names) (error "Variable not found -- LOOKUP-VARIABLE-VALUE" var)) ((eq? (car names) var) (car values)) (else (scan (cdr names) (cdr values))))) (scan (car env) (cdr env))) (define (set-variable-value! var val env) (define (scan names values) (cond ((null? names) (error "Variable not found -- SET-VARIABLE-VALUE!" var)) ((eq? (car names) var) (set-car! values val)) (else (scan (cdr names) (cdr values))))) (scan (car env) (cdr env)))
Tulkin käyttöliittymä Käyttöliittymän koodi (melkein sama kuin ennen) nelilaskin8.scm (define input-prompt ";;; Eval input:") (define output-prompt ";;; Eval value:") ;; kirjan driver-loop käyttää tätä globaalia muuttujaa (define the-global-environment (setup-environment)) (define (driver-loop) (prompt-for-input input-prompt) (let ((input (read))) (let ((output (eval input the-global-environment))) (announce-output output-prompt) (display output))) (driver-loop)) (define (prompt-for-input string) (newline) (newline) (display string) (newline)) (define (announce-output string) (newline) (display string) (newline))
Entä nyt? edellisillä kalvoilla oli koko uuden tulkin koodi koodi on nyt muuten sama kuin SICP-kirjan kohtien 4.14.1.6 tulkki, mutta: 1 eval-proseduuri on vielä hieman vähemmän abstrakti 2 primitiiviproseduuri tulkissa on suoraan Scheme-proseduuri (kirjassa (list 'primitive Scheme-proseduuri )) 3 evalista puuttuu tuki quote:lle ja cond:lle 4 paikalliset muuttujat puuttuvat (joten muutamat niitä käsittelevät kohdat, mm. lambda, toimivat eri tavalla ja env on aina sama) 5 käyttöliittymäkoodi tulostaa proseduurit eri tavalla tehdään kohdat 13 seuraavaksi ja 45 seuraavalla luennolla
Sisältö 1 Argumentittomat proseduurit ja käyttöliittymä 2 Abstrakti syntaksi ja env-argumentti 3 Lisäabstraktio evaliin 4 quote ja cond
Vielä abstraktimpi eval 1/3 (SICP 4.1.1) Koko eval ja siitä irrotettu apply nelilaskin9.scm (define (eval exp env) (cond ((self-evaluating? exp) exp) ((variable? exp) (lookup-variable-value exp env)) ((assignment? exp) (eval-assignment exp env)) ((definition? exp) (eval-definition exp env)) ((if? exp) (eval-if exp env)) ((lambda? exp) (make-procedure (lambda-body exp))) ((begin? exp) (eval-sequence (begin-actions exp) env)) ((application? exp) (apply (eval (operator exp) env) ; ei Schemen apply! (list-of-values (operands exp) env) env)) (else (error "Unknown expression type -- EVAL" exp)))) (define apply-in-underlying-scheme apply) ; Schemen apply talteen (define (apply procedure arguments env) ; uusi määritelmä applylle (cond ((primitive-procedure? procedure) (apply-primitive-procedure procedure arguments)) ((compound-procedure? procedure) (eval-sequence (procedure-body procedure) env)) (else (error "Unknown procedure type -- APPLY" procedure))))
Vielä abstraktimpi eval 2/3 (SICP 4.1.1) Uusia evalin apuproseduureja (define (eval-assignment exp env) (set-variable-value! (assignment-variable exp) (eval (assignment-value exp) env) env) 'ok) (define (eval-definition exp env) (define-variable! (definition-variable exp) (eval (definition-value exp) env) env) 'ok) (define (eval-if exp env) (if (true? (eval (if-predicate exp) env)) (eval (if-consequent exp) env) (eval (if-alternative exp) env))) nelilaskin9.scm sama koodi oli aiemmin evalin sisällä, paitsi uusi paluuarvo 'ok
Vielä abstraktimpi eval 3/3 (SICP 4.1.1, 4.1.2) Uusia applyyn liittyviä apuproseduureja nelilaskin9.scm (define (no-operands? ops) (null? ops)) (define (first-operand ops) (car ops)) (define (rest-operands ops) (cdr ops)) ;; sama kuin (map (lambda (exp) (eval exp env)) exps) (define (list-of-values exps env) (if (no-operands? exps) '() (cons (eval (first-operand exps) env) (list-of-values (rest-operands exps) env)))) ;; uusi primitiivien esitysmuoto (define (primitive-procedure? proc) (tagged-list? proc 'primitive)) (define (primitive-implementation proc) (cadr proc)) (define (apply-primitive-procedure proc args) (apply-in-underlying-scheme (primitive-implementation proc) args)) (define (primitive-procedure-names) (map car primitive-procedures)) (define (primitive-procedure-objects) (map (lambda (proc) (list 'primitive (cadr proc))) primitive-procedures))
Miksi tämä abstraktio? nyt on tehty puutelistan kohdat 1 ja 2 (abstraktimpi eval ja uusi primiitiivien esitysmuoto) miksi teimme näin paljon abstraktioita? kaikki lausekkeen rakenteeseen liittyvä on nyt abstrahoitu koskematta evaliin ja applyyn voisi vaihtaa tulkattavan kielen syntaksia tai lausekkeiden esitysmuotoa tulkissa (nyt listarakenne, voisi olla esim. taulukko) myös suuri osa lausekkeiden käsittelystä on abstrahoitu: esim. define-lauseketta voisi käsitellä eri lailla vain muuttamalla eval-definitionia tulkin perusta on eval ja apply eli lausekkeen tulkitseminen ja proseduurin kutsu
Sisältö 1 Argumentittomat proseduurit ja käyttöliittymä 2 Abstrakti syntaksi ja env-argumentti 3 Lisäabstraktio evaliin 4 quote ja cond
Puutelistan kohta 3 lisätään vielä tuki quote:lle ja cond:lle quotessa (eli ':ssä) osa tulkattavan ohjelman koodista päätyy suoritettavassa ohjelmassa näkyväksi tietorakenteeksi tästä syystä perus-schemessä ei periaatteessa saa muuttaa quotella tehtyä listaa: toteutus saa viitata suoraan listarakenteena talletettuun koodiin toteutetaan cond muuttamalla se sisäkkäisiksi if-lausekkeiksi tämä menetelmä on nimeltään syntaksimuunnos (syntax transformation, program transformation), ja condin sanotaan olevan johdettu lauseke (derived expression) riittää määritellä uusi syntaksi olemassaolevien avulla: toteutukseen ei tarvitse lisätä muuta tukea condille kuin muunnosproseduuri joissain kielissä myös ohjelmoija itse voi tehdä syntaksimuunnoksia makroilla kielen toteutusta muokkaamatta lisätään vielä muutama primitiivi: cons, car, cdr, null?, display ja newline
Lisätään quote (SICP 4.1.2) quote on helppo lisätä: sen toteutus palauttaa palan koodista esim. '(a b) eli (quote (a b)) palauttaa osoittimen siihen koodin pariin, jossa a on car read muuntaa ' x :n (quote x )-listaksi periaatteessa tässä voisi myös tehdä listarakenteesta kopion samalla saadaan symbolit tulkattavaan kieleen (mutta ilman esim. eq?-primitiiviä niillä ei voi tehdä kovin paljon) quoten lisääminen nelilaskin10.scm (define (quoted? exp) (tagged-list? exp 'quote)) (define (text-of-quotation exp) (cadr exp)) (define (eval exp env) (cond... ((quoted? exp) (text-of-quotation exp))... ))
Syntaksimuunnos cond->if (SICP 4.1.2) mikä tahansa cond-lauseke voidaan muuttaa jonoksi if-lausekkeita koodista tulee hieman pidempää, mutta samalla tavalla toimivaa (samat ehdot pitäisi käydä läpi jos cond toteutettaisiin suoraan) muunnosta tehdessä ei tarvitse tietää muun tulkin toiminnasta vain osata muokata tulkattavia lausekkeita Esimerkki muunnoksesta (cond ((> x 0) x) ((= x 0) (display 'zero) 0) (else (- x))) muuttuu muotoon (if (> x 0) x (if (= x 0) (begin (display 'zero) 0) (- x)))
Toteutus cond->if:lle 1/2 (SICP 4.1.2) jotta muunnoksen voisi tehdä säilyttäen syntaksiabstraktiot (eikä suoraan listarakennetta tuottamalla), tarvitaan apuproseduurit if- ja begin-lausekkeiden tekemiseen evalissa vain evaluoidaan syntaksimuunnoksen lopputulos Apuproseduureja ja eval-lisäys (define (make-if predicate consequent alternative) (list 'if predicate consequent alternative)) nelilaskin10.scm (define (make-begin seq) (cons 'begin seq)) (define (sequence->exp seq) ; ''optimoitu'' make-begin (cond ((null? seq) seq) ((last-exp? seq) (first-exp seq)) (else (make-begin seq)))) (define (eval exp env) (cond... ((cond? exp) (eval (cond->if exp) env))... ))
Toteutus cond->if:lle 2/2 (SICP 4.1.2) Itse cond->if-muunnos (define (cond? exp) (tagged-list? exp 'cond)) (define (cond-clauses exp) (cdr exp)) (define (cond-else-clause? clause) (eq? (cond-predicate clause) 'else)) (define (cond-predicate clause) (car clause)) (define (cond-actions clause) (cdr clause)) nelilaskin10.scm (define (cond->if exp) (expand-clauses (cond-clauses exp))) (define (expand-clauses clauses) (if (null? clauses) 'false ; tulkattavaa koodia, joka suoritetaan kun else:ä ei ole (let ((first (car clauses)) (rest (cdr clauses))) (if (cond-else-clause? first) (if (null? rest) (sequence->exp (cond-actions first)) (error "ELSE clause isn't last -- COND->IF" clauses)) (make-if (cond-predicate first) (sequence->exp (cond-actions first)) (expand-clauses rest))))))
Primitiivien lisääminen ja ajoesimerkki Uusia primitiivejä (tämä muutos riittää) nelilaskin10.scm (define primitive-procedures (list (list 'car car) (list 'cdr cdr) (list 'cons cons) (list 'null? null?) (list '+ +) (list '- -) (list '* *) (list '/ /) (list '< <) (list '= =) (list '> >) (list 'display display) (list 'newline newline))) Tämän tulkin ajoesimerkki (define (fact-iter) ;;; Eval input: (cond ((> c n) p) (fact-iter) (else fact-iter: c = 1 (display "fact-iter: c = ") fact-iter: c = 2 (display c) fact-iter: c = 3 (newline) fact-iter: c = 4 (set! p (* c p)) fact-iter: c = 5 (set! c (+ c 1)) (fact-iter))))) ;;; Eval value: (define p 1) 120 (define c 1) (define n 5) nelilaskin10.scm