Ohjelmoinnin peruskurssien laaja oppimäärä Luento 10: Tulkin muokkaus, makrot, ohjelmia muokkaavat ohjelmat (mm. SICP 3.2.4, 4-4.1.6) Riku Saikkonen 22. 11. 2011
Sisältö 1 Kirjan tulkin muokkaaminen 2 Yksityiskohta: sisäiset määrittelyt (SICP 3.2.4 ja 4.1.6) 3 Makrot 4 Ohjelmia muokkaavat ohjelmat 5 Kierroksen 3 tehtävien ratkaisut
Tulkin muokkaaminen tulkkiin lisätään ominaisuuksia yleensä muokkaamalla sitä jollain näistä tavoista: 1 lisätään uusia primitiiviproseduureja 2 lisätään uusia johdettuja lausekkeita (kuten cond tehtiin) 3 lisätään uusia erikoismuotoja (kuten esim. if tehtiin) tässä(kään) ei ole mitään erityisen Scheme-spesistä: samat kolme vaihtoehtoa olisi esim. Python-tulkin muokkaamisessa primitiiviproseduuri riittää, jos uusi lauseke voi evaluoida kaikki argumenttinsa ennen kuin proseduuriin päästään (esim. if ei voi) uusia johdettuja lausekkeita voi tehdä myös tulkkia muuttamatta, jos kieli tukee makroja (lähinnä Lisp-kielet tukevat) ihan kaikkia ominaisuuksia ei voi toteuttaa näin: joskus tulkin rakennetta pitää muuttaa enemmän esim. poikkeusten (try, catch ja throw) toteuttaminen vaatisi enemmän muutoksia, jos alla olevassa kielessä ei olisi vastaavaa ominaisuutta (Schemessä on)
Tulkin muokkaaminen: uusi primitiivi primitiiviproseduureja on helppo lisätä: lisätään vain niiden toteutus primitive-procedures-listaan hankalampaa, jos primiivi ottaa argumentiksi tai palauttaa proseduureja (kuten map) tai uuden primitiivin voisi määritellä tulkin sisällä: esim. kutsua setup-environmentista evalia omalla define-lausekkeella Esimerkki uusien primitiivien lisäämisestä (muutokset punaisella) (define (average x y) (/ (+ x y) 2)) (define primitive-procedures (list... (list 'length length) (list 'average average) (list 'print display)...)) ; Schemen primitiivi ; itse toteutettu primitiivi ; uudelleennimetty Schemen primitiivi
Tulkin muokkaaminen: uusi johdettu lauseke 1 tehdään apuproseduurit, jotka tunnistavat uuden lausekkeen ja jakavat sen osiin 2 tehdään muunnosproseduuri, joka muuntaa uuden lausekkeen koodiksi, jota tulkki ennestään tukee 3 lisätään evaliin haara, joka kutsuu muunnosproseduuria Esimerkki uuden johdetun lausekkeen lisäämisestä ;; käyttöesimerkki: (let ((a 2)) (unless (= a 0) (/ 1 a) 0)) 1/2 (define (unless? exp) (tagged-list? exp 'unless)) (define (unless-predicate exp) (cadr exp)) (define (unless-alternative exp) (caddr exp)) (define (unless-consequent exp) (cadddr exp)) (define (unless->if exp) ; muunnos (unless a b c) (if a c b) (make-if (unless-predicate exp) (unless-consequent exp) (unless-alternative exp))) (define (eval exp env) (cond... ((unless? exp) (eval (unless->if exp) env))... ))
Tulkin muokkaaminen: uusi erikoismuoto 1 tehdään apuproseduurit, jotka tunnistavat uuden lausekkeen ja jakavat sen osiin 2 tehdään proseduuri, joka suorittaa (tulkitsee) uuden lausekkeen 3 lisätään evaliin haara uudelle lausekkeelle Esimerkki uuden erikoismuodon lisäämisestä m-eval-prog.scm ;; (prog n lauseke...): kuten begin, mutta saa valita minkä lausekkeen ;; arvo palautetaan; esim. (let ((x 5)) (prog 1 x (set! x (+ x 1))) 5 (define (prog? exp) (tagged-list? exp 'prog)) (define (prog-n exp) (cadr exp)) (define (prog-exps exp) (cddr exp)) (define (eval-prog n exps env saved) ; ei säilytä häntärekursiota! (if (null? exps) saved (let ((result (eval (car exps) env))) (eval-prog (- n 1) (cdr exps) env (if (= n 1) result saved))))) (define (eval exp env) (cond... ((prog? exp) (eval-prog (prog-n exp) (prog-exps exp) env #f))... ))
Sisältö 1 Kirjan tulkin muokkaaminen 2 Yksityiskohta: sisäiset määrittelyt (SICP 3.2.4 ja 4.1.6) 3 Makrot 4 Ohjelmia muokkaavat ohjelmat 5 Kierroksen 3 tehtävien ratkaisut
Sisäiset määrittelyt (SICP 3.2.4, 4.1.6) tulkkien ja ympäristöjen yhteydessä ei vielä käsitelty pientä yksityiskohtaa: paikallisten (eli toisen proseduurin sisäisten) proseduurien määrittelyä miten paikallisten proseduurien määrittely definellä vaikuttaa ympäristöihin? define lisää muuttujan sen evaluointiympäristön sisimpään kehykseen (ei siis tee uutta kehystä kuten let) joten sisäiset määrittelyt lisätään proseduuria kutsuttessa luotuun kehykseen (samaan kuin proseduurin argumentit) myös kohdan 4.1 tulkki toimii näin (define-variablessa) entä jos paikalliset proseduurit viittaavat toisiinsa? define lisää ne samaan kehykseen, joten kaikki samalla tasolla määritellyt näkevät toisensa kaikki siis toimii loogisesti, jos defineä käyttää kuten kirjassa on tapana (ensin definet, sitten muu koodi)
Sisäiset määrittelyt: define ja letrec (SICP 3.2.4, 4.1.6) sisäisellä definellä määriteltyjen muuttujien arvoja saa käyttää vasta kun kaikki definet on suoritettu käytännössä proseduurin rungossa on ensin definet paikallisille proseduureille ja sitten vasta muu koodi definellä ei pitäisi määritellä tavallisia muuttujia (vaan letillä) toinen tapa paikallisten proseduurien tekemiseen on letrec kuten let, mutta määritelmien sisällä voi viitata samassa letrecissä määriteltäviin muuttujiin käsitteellisesti siistimpi kuin sisäinen define: paikalliset määrittelyt ja proseduurin varsinainen koodi erottuvat toisistaan define- ja letrec-esimerkki (define (my-odd-1? x) (define (even? n) (if (= n 0) true (odd? (- n 1)))) (define (odd? n) (if (= n 0) false (even? (- n 1)))) (odd? x)) (define (my-odd-2? x) ; sama letrecillä (letrec ((even? (lambda (n) (if (= n 0) true (odd? (- n 1))))) (odd? (lambda (n) (if (= n 0) false (even? (- n 1)))))) (odd? x)))
Sisältö 1 Kirjan tulkin muokkaaminen 2 Yksityiskohta: sisäiset määrittelyt (SICP 3.2.4 ja 4.1.6) 3 Makrot 4 Ohjelmia muokkaavat ohjelmat 5 Kierroksen 3 tehtävien ratkaisut
Mikä on makro? makroilla ohjelmoija voi määritellä kieleen uutta syntaksia (uusia johdettuja lausekkeita) makron määritelmä kertoo, miten makroja sisältävää ohjelmakoodia muokataan ennen kuin se suoritetaan koodia muutetaan makron käyttöpaikan eli kutsun kohdalta Lispissä makro on käännösaikainen proseduuri, joka saa argumenteiksi lausekkeita ja palauttaa uuden lausekkeen, joka varsinaisesti ajon aikana suoritetaan esim. kirjan tulkin cond->if-muunnosproseduuri toimisi sellaisenaan makrona: cond->if saa argumentiksi listan lausekkeita (condin ehdot ja niiden koodin) ja tuottaa lausekkeen, jossa on sisäkkäisiä iffejä lauseke muunnetaan eli makro lavennetaan joko käännösaikana tai (tulkissa) juuri ennen koodin suoritusta makroja käytetään varsinkin sellaisten abstraktioiden tekemiseen, joihin proseduurit eivät riitä
Makrot Lisp-kielissä: define-macro useimmille Lisp-kielille yhteinen tapa määritellä makroja on define-macro (joissain kielissä nimenä on defmacro) (define-macro ( makron nimi argumentit... ) koodi ) määrittelee uuden makron koodi saa argumenteikseen makron kutsussa olevat lausekkeet, ja sen pitäisi palauttaa lauseke, johon makro lavenee koodi on siis tavallista kielen koodia, joka käsittelee kielen lausekkeita (ja joka voidaan suorittaa käännösaikana) Scheme-esimerkkejä (define-macro (unless predicate alternative consequent) (list 'if predicate consequent alternative)) (unless (< 1 2) 3 4) 4 (define-macro (ind n. exprs) (if (null? exprs) 0 (list 'if (car exprs) n (cons 'ind (cons (+ n 1) (cdr exprs)))))) (ind 1 (< 2 1) (< 3 4)) (if (< 2 1) 1 (if (< 3 4) 2 0)) 2
quasiquote quasiquote eli ` on apusyntaksi sellaisten listojen rakentamiseen, joista osa pysyy vakiona ja osa lasketaan ajon aikana quasiquotea voi käyttää ilmankin makroja sen voisi aina kirjoittaa auki mm. quoten ja consin avulla quasiquote toimii kuten quote, paitsi: jos listassa on, eli unquote, sen jälkeen oleva lauseke evaluoidaan normaalisti ja sen paluuarvo tulee listan alkioksi,@ eli unquote-splicing on kuten,, mutta evaluoitavan lausekkeen palauttaman listan alkiot liitetään listan tähän kohtaan sisäkkäiset quasiquotet ovat vähän monimutkaisempia Esimerkkejä quasiquotesta `(a (+ 5 2) c) (a (+ 5 2) c) `(a,(+ 5 2) c) (a 7 c) `(a,(filter odd '(1 2 3)) c) (a (1 3) c) `(a,@(filter odd '(1 2 3)) c) (a 1 3 c) (define-macro (ind n. exprs) (if (null? exprs) 0 `(if,(car exprs),n (ind,(+ n 1),@(cdr exprs)))))
Tapa tehdä uusia muuttujannimiä: gensym Viallinen esimerkki or-makrosta or-macro.scm (define-macro (my-or. exprs) (cond ((null? exprs) #f) ((null? (cdr exprs)) (car exprs)) (else `(let ((x,(car exprs))) (if x x (my-or,@(cdr exprs))))))) (my-or (= 1 2) (= 3 4)) #f ; tämä vielä toimii (let ((x 2)) (my-or (= x 3) (= x 2))) ; tästä tulee virheilmoitus! vika tulee siitä, että makro määrittelee muuttujan x, jota käytetään sen argumentissa eri merkityksessä korjaus: primitiivi (gensym) tuottaa nimeksi uuden symbolin Korjattu esimerkki or-macro.scm (define-macro (my-or. exprs) (cond ((null? exprs) #f) ((null? (cdr exprs)) (car exprs)) (else (let ((name (gensym))) `(let ((,name,(car exprs))) (if,name,name (my-or,@(cdr exprs))))))))
R 5 RS-Schemen hygieeniset makrot standardi-schemessä ei ole define-macroa vaan oma makrojärjestelmä Schemen makrot ovat hygieenisiä eli nimeävät makron paikalliset muuttujat uudelleen automaattisesti koodi ei ole Scheme-koodia, vaan vain lauseke, johon makro laventuu (kylläkin rekursiivisesti) R 6 RS-Schemessä monipuolisempi makrokielessä on hahmonsovitus, jota ei kuitenkaan voi käyttää muussa Schemessä käytännössä Scheme-makroja käytetään melko vähän Scheme-makroesimerkkejä ;; or Scheme-standardista ;;... on makrokielen koodia! (define-syntax or (syntax-rules () ((or) #f) ((or test) test) ((or test1 test2...) (let ((x test1)) (if x x (or test2...)))))) ;; let Scheme-standardista ;; (osa: nimetty let puuttuu) (define-syntax let (syntax-rules () ((let ((name val)...) body1 body2...) ((lambda (name...) body1 body2...) val...))))
Entä C:n makrot? C- ja C++-kielissä makrot muokkaavat merkkijonoja eivätkä varsinaisia kielen lausekkeita merkkijonosta tehdään lauseke (eli se jäsennetään, parse) vasta makrolavennuksen jälkeen poikkeuksena makroja määrittelevä yms. koodi (# rivin alussa) C:n makroilla voi tehdä lähinnä vakiolavennuksia ja yksinkertaisia ehtolauseita (#if), Lispin/Schemen makroissa voi käyttää koko ohjelmointikieltä joten Lisp-kääntäjän pitää itse suorittaa kääntämäänsä kieltä yleensä Lispeissä on erillinen tapa suorittaa koodia käännösaikana muutenkin kuin makron sisällä näistä syistä Lispin makroilla tehdään monimutkaisempia asioita kuin esim. C:n makroilla mm. Common Lispin oliojärjestelmän voi toteuttaa makroilla
Sisältö 1 Kirjan tulkin muokkaaminen 2 Yksityiskohta: sisäiset määrittelyt (SICP 3.2.4 ja 4.1.6) 3 Makrot 4 Ohjelmia muokkaavat ohjelmat 5 Kierroksen 3 tehtävien ratkaisut
Ohjelmia muokkaavat ohjelmat kirjan tulkit suorittavat annettua Scheme- tms. ohjelmaa samaan tapaan voi myös muuttaa annettua ohjelmaa muunnosproseduuri ottaa ohjelman eli lausekkeen argumentiksi ja palauttaa muokatun version siitä jaetaan käsittely osiin lauseketyypin (set!, if jne.) mukaan samoin kuin tulkissa mutta lausekkeen suorittamisen sijaan palautetaan lauseke joko sellaisenaan tai muunnettuna alilausekkeet muunnetaan rekursiivisesti tai tutkia sitä: esimerkiksi tarkistaa osia koodin toiminnasta staattisesti eli suorittamatta sitä esim. Scheme-koodista voi tarkistaa, käytetäänkö siinä muuttujia, joita ei ole määritelty tai koodista voisi optimointeja varten etsiä muuttujia, joiden arvoja ei koskaan muuteta, tai sivuvaikutuksettomia proseduureja tai kääntää sen toiselle (yleensä matalamman tason) kielelle
Ohjelman muokkauksen käyttötarkoituksia mitä ohjelmaa muokkaamalla sitten voi tehdä? kääntää koodia yksinkertaisempaan muotoon, esim. poistaa cond-lausekkeet erilaisia optimointeja lisätä koodiin dynaamisia eli ajonaikaisia tarkistuksia muuttaa koodin toimintaa (esim. tehdä kaikkien tulostuskomentojen tilalla jotain muuta) lisätä toiminnallisuutta, esim. kerätä tietoja siitä, mitä koodia suoritetaan eniten kääntäjät tekevät usein tällaisia muunnoksia ennen varsinaista kääntämistä, joskus sen jälkeenkin
Scheme-ohjelman muokkausproseduurin (eräs) runko Kopioi lausekkeen ja cond->if-muuntaa sen syntax-process.scm ;; käyttää kirjan tulkin apuproseduureja tagged-list? ja cond->if (define (process exp) (cond ((tagged-list? exp 'quote) exp) ((tagged-list? exp 'set!) (list 'set! (cadr exp) (process (caddr exp)))) ((tagged-list? exp 'define) (cons 'define (cons (cadr exp) (map process (cddr exp))))) ((tagged-list? exp 'if) (cons 'if (map process (cdr exp)))) ((tagged-list? exp 'lambda) (cons 'lambda (cons (cadr exp) (map process (cddr exp))))) ((tagged-list? exp 'begin) (cons 'begin (map process (cdr exp)))) ((tagged-list? exp 'cond) (process (cond->if exp))) ((tagged-list? exp 'let) (cons 'let (cons (map (lambda (clause) (list (car clause) (process (cadr clause)))) (cadr exp)) (map process (cddr exp))))) ((pair? exp) (map process exp)) (else exp)))
Muunnosesimerkki: tilastoja if:ien ehdoista Muutokset edelliseen punaisella syntax-process.scm (define predicate-id 0) (define (process exp) (cond... ((tagged-list? exp 'if) (set! predicate-id (+ predicate-id 1)) (cons 'if (cons (list 'predicate-stat predicate-id (process (cadr exp))) (map process (cddr exp)))))...)) lisää jokaisen if-lausekkeen ehtoon tilastointiproseduurin kutsun esim. (if (< x 0) a b) (if (predicate-stat 1 (< x 0)) a b) muunnettua koodia ajettaessa predicate-stat voisi esim. kerätä tilastoja siitä, kuinka usein mitäkin haaraa suoritetaan (define (predicate-stat n branch) (add-to-statistics! n branch) branch)
Sisältö 1 Kirjan tulkin muokkaaminen 2 Yksityiskohta: sisäiset määrittelyt (SICP 3.2.4 ja 4.1.6) 3 Makrot 4 Ohjelmia muokkaavat ohjelmat 5 Kierroksen 3 tehtävien ratkaisut