Ohjelmoinnin peruskurssien laaja oppimäärä Luento 9: Makrot ja dynaaminen sidonta Riku Saikkonen 7. 12. 2010
Sisältö 1 Makrot 2 Pieni esimerkki abstraktion tekemisestä 3 Dynaaminen sidonta
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ä 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) 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 kääntäjän pitää pystyä itse suorittamaan 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 Makrot 2 Pieni esimerkki abstraktion tekemisestä 3 Dynaaminen sidonta
Tulkin ympäristökoodin abstrahointia kirjan kohdan 4.1 tulkin ympäristöjä käsittelevässä koodissa oli kolme rakenteeltaan melkein samanlaista proseduuria: define-variable, lookup-variable-value ja set-variable-value! kaikki etsivät ympäristöstä muuttujasidontaa erot: tekevät eri asian kun sidonta löytyy tai ei löydy, ja define-variable tutkii vain ensimmäistä kehystä kokeillaan siirtää niiden yhteinen koodi omaan apuproseduuriinsa keksitään apuproseduuri: (env-traverse var env only-rst-frame not-found found ) hakee varia envistä kutsuu (not-found), jos ei löytänyt sidontaa kutsuu (found vals ), jos sidonta löytyi; vals on lista, jonka car on muuttujan arvo jos only-first-frame on tosi, tutkii vain ensimmäistä kehystä
Proseduuriabstraktio Apuproseduurin koodi (käyttää kirjan tulkin koodia) abs-env.scm (define (env-traverse var env only-first-frame not-found-proc found-proc) (define (env-loop env) (define (scan vars vals) (cond ((null? vars) (if only-first-frame (not-found-proc) (env-loop (enclosing-environment env)))) ((eq? var (car vars)) (found-proc vals)) (else (scan (cdr vars) (cdr vals))))) (if (eq? env the-empty-environment) (not-found-proc) (let ((frame (first-frame env))) (scan (frame-variables frame) (frame-values frame))))) (env-loop env)) sinisellä kirjan tulkin proseduureista lainattu yhteinen koodi
Proseduuriabstraktion käyttö Ympäristöproseduurit abstraktion avulla abs-env.scm (define (lookup-variable-value var env) (env-traverse var env #f (lambda () (error "Unbound variable" var)) (lambda (vals) (car vals)))) (define (set-variable-value! var val env) (env-traverse var env #f (lambda () (error "Unbound variable -- SET!" var)) (lambda (vals) (set-car! vals val)))) (define (define-variable! var val env) (env-traverse var env #t (lambda () (add-binding-to-frame! var val (first-frame env))) (lambda (vals) (set-car! vals val))))
Voisiko makroista olla tässä apua? 1/2 kokeillaan tehdä sama abstraktio makrolla alla oleva makro tuottaa ympäristörakenteen läpikäyvän koodin Apumakro ympäristöproseduureille abs-env.scm (define-macro (env-traverse only-first-frame not-found found) (define (make-scan not-found-case found-case) `(let ((frame (first-frame env))) (define (scan vars vals) (cond ((null? vars),not-found-case) ((eq? var (car vars)),found-case) (else (scan (cdr vars) (cdr vals))))) (scan (frame-variables frame) (frame-values frame)))) (if only-first-frame (make-scan not-found found) `(begin (define (env-loop env) (if (eq? env the-empty-environment),not-found,(make-scan '(env-loop (enclosing-environment env)) found))) (env-loop env))))
Voisiko makroista olla tässä apua? 2/2 Ympäristöproseduurit apumakron avulla abs-env.scm (define (lookup-variable-value var env) (env-traverse #f (error "Unbound variable" var) (car vals))) (define (set-variable-value! var val env) (env-traverse #f (error "Unbound variable -- SET!" var) (set-car! vals val))) (define (define-variable! var val env) (env-traverse #t (add-binding-to-frame! var val (first-frame env)) (set-car! vals val))) erona proseduuriabstraktioon lambdoja ei nyt tarvita found- ja not-found-tapausten koodin ympärille, ja tietynnimisiä muuttujia (esim. var ja env) käytetään makrokoodissa suoraan
Pohdintaa: kannattiko tämä tehdä? molemmissa versiossa näkyy kirjan koodia selkeämmin, miten nämä kolme ympäristöproseduuria eroavat sen sijaan yhteinen koodi on nyt monimutkaisempaa luettavaa proseduuriabstraktiossa on kohtuullisen selkeä rajapinta ympäristön esitysmuodon voisi vaihtaa (esim. harjoitustehtävänä olleeseen) vain apuproseduuria muuttamalla toisaalta abstraktio on monimutkainen muttei kovin monikäyttöinen: sillä voi edelleen vain hakea yhtä muuttujaa makroabstraktio ei ole kovin selkeä rajapinta on monimutkainen, varsinkin kun se luottaa siihen, että muuttujien nimet ovat var ja env tällainen ei ole kovin tyypillinen Lisp-makro makron etu: käyttävä koodi on lyhyempää, eivätkä lambdat haittaa koodin lukemista kuten proseduurissa proseduuriabstraktiota ehkä kannattaisi käyttää, makroa tuskin tilanne muuttuisi, jos käyttötapauksia olisi enemmän kuin kolme
Sisältö 1 Makrot 2 Pieni esimerkki abstraktion tekemisestä 3 Dynaaminen sidonta
Mikä dynaaminen sidonta? Scheme ja kirjan tulkit, kuten useimmat ohjelmointikielet, ovat leksikaalisesti sidottuja (lexical/static scoping/binding) eli saatavilla olevat muuttujat näkee lauseketta ympäröivästä koodista (staattisesti tai leksikaalisesti) harvinaisempi vaihtoehto on dynaaminen sidonta: tietynnimisen muuttujan arvo on se arvo, joka tälle nimelle viimeksi ajon aikana asetettiin siis muuttujasidonta on dynaaminen eli ajon aikainen käytössä kokonaan lähinnä joissain vanhoissa kielissä, mm. Emacs Lisp, Logo ja APL vähän useammin dynaamista sidontaa käytetään osittain (tietynlaisille muuttujille tai tietyllä tavalla tehdylle koodille), mm. Perlissä, Common Lispissä ja monissa Scheme-toteutuksissa
Eräs tapa ymmärtää dynaamista sidontaa jos kaikki muuttujat ovat dynaamisesti sidottuja, kieli käyttäytyy kuten se toimisi näin: kaikki muuttujat ovat globaaleja muuttujia let-lauseke (tai vastaava) ottaa globaalin muuttujan arvon talteen, asettaa sille uuden arvon ja palauttaa talletetun arvon, kun let-lausekkeen suoritus loppuu samoin esimerkiksi proseduurikutsu asettaa argumenttimuuttujalle uuden arvon kutsun ajaksi ja palauttaa entisen arvon kutsun jälkeen todellisuudessa dynaaminen sidonta toteutetaan usein enemmän leksikaalista muistuttavalla tavalla (seuraava kalvo) dynaaminen sidonta on siis leksikaalista helpompi toteuttaa: ympäristö-tietorakenne voi olla yksinkertaisempi talletetun proseduurin ei tarvitse sisältää ympäristöä (joten proseduurista on tallessa pelkkä koodi eli ajon aikana ei tarvitse varata muistia sitä varten)
Dynaamisen sidonnan toteutus kirjan tulkkiin Muutokset tulkkiin dynaamista sidontaa varten m-eval-dynamic.scm (define (eval exp env) ; envistä voisi nyt tehdä globaalin muuttujan (cond... ; sen sijaan että sitä kuljettaa mukana evalissa ((lambda? exp) (make-procedure (lambda-parameters exp) (lambda-body exp) env)) ; envin voisi nyt jättää tästä pois... ((application? exp) (apply (eval (operator exp) env) (list-of-values (operands exp) env) env))... )) (define (apply procedure arguments env) (cond... ((compound-procedure? procedure) (eval-sequence (procedure-body procedure) (extend-environment (procedure-parameters procedure) arguments env))) ; oli (procedure-environment...)... ))
Esimerkki dynaamisesta sidonnasta Pieni koodiesimerkki (define (f a) (define (add x) (+ x a)) (let ((a 5)) (* a (add 2)))) tavallisella Schemellä eli leksikaalisella sidonnalla (f 3) 25 dynaamisella sidonnalla (f 3) 35 Hyödyllisempi (osittainen) esimerkki show-list.scm (define sl-columns 5) ; näin monta arvoa samalle riville (define sl-column-width 8) ; varataan tilaa näin monta merkkiä/arvo (define (show-list l) ; tulostaa listan l alkiot em. asetusten mukaan... ) (define (show-data)... (show-list foo)... ) (define (display-report) ; tulostaa tietoja kapeaan ikkunaan... (show-data)...) (define (print-report) ; tulostaa samoja tietoja leveälle paperille... (let ((sl-columns 10)) (show-data))...)
Makrot ovat dynaamisesti sidottuja! jos Lisp- tai C-makrosta laventuvassa koodissa on muuttujannimiä, joita ei ole siinä määritelty, ne käyttäytyvät kuin sidonta olisi dynaamista esim. jos makro lavenee koodiksi, joka käyttää muuttujaa x, tämä x viittaa siihen muuttujaan, joka makron käyttöpaikassa on vrt. jos proseduurin koodissa käyttää muuttujaa x, se haetaan leksikaalisen sidonnan mukaan proseduurin määritelmää ympäröivästä koodista Schemen hygieeniset makrot ovat kuitenkin leksikaalisesti sidottuja myös poikkeuksien käsittelijät ovat dynaamisesti sidottuja esim. Javassa poikkeuksen käsittelijä valitaan niistä try-lauseista, joiden sisällä ollaan poikkeuksen aiheuttavaa koodia suoritettaessa ei siis niistä, jotka ovat staattisesti koodin ympärillä
Dynaaminen sidonta Schemessä: parametrit Schemen with-output-to-file vaihtaa sen tiedoston, johon mm. display kirjoittaa: (define (myprint) (display "The answer is: ") (display 42) (newline)) (myprint) (with-output-to-file "foo.txt" myprint) eli nykyinen tiedosto on kuin dynaamisesti sidottu muuttuja, jota display käyttää monessa Scheme-toteutuksessa tällaiset asetukset on abstrahoitu parametreiksi, jotka ovat dynaamisesti sidottuja muuttujia, joilla voi muokata primitiivien toimintaa niiden arvoja voi asettaa letiä muistuttavalla lauseella, esim. (parameterize ((radix 2))...) ohjelmoija voi tehdä myös uusia parametreja ja käyttää niitä itse joissain Schemeissä ja Lispeissä tavallisen (esim. globaalin) muuttujan arvon voi vaihtaa hetkeksi fluid-let-rakenteella, esim. (fluid-let ((x 2))...)