Ohjelmoinnin peruskurssien laaja oppimäärä Luento 12: Ohjelmamuunnokset, dynaaminen sidonta, Lisp-kielistä Riku Saikkonen 4. 12. 2012
Sisältö 1 Ohjelmia muokkaavat ohjelmat 2 Dynaaminen sidonta 3 Lisp-kielistä ja DSL-kielistä
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 kääntämisen aikana varsinkin kääntäjissä tekniikka on nimeltään ohjelmamuunnos (program transformation)
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 Ohjelmia muokkaavat ohjelmat 2 Dynaaminen sidonta 3 Lisp-kielistä ja DSL-kielistä
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ää eikä kutsua ympäröivästä koodista Schemen omat hygieeniset makrot (define-syntax) ovat kuitenkin leksikaalisesti sidottuja myös poikkeuksien käsittelijät ovat dynaamisesti sidottuja esim. Pythonissa 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: ne 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ä myös tavallisen (esim. globaalin) muuttujan arvon voi vaihtaa hetkeksi fluid-let-rakenteella, esim. (fluid-let ((x 2))...)
Sisältö 1 Ohjelmia muokkaavat ohjelmat 2 Dynaaminen sidonta 3 Lisp-kielistä ja DSL-kielistä
Muista Lisp-kielistä kuin Scheme Lisp-kielet ovat keskenään melko samankaltaisia; yhteisiä piirteitä: sulkusyntaksi (poikkeus: Dylan-kielessä tavallinen syntaksi) monia asioita voi itse määritellä uudelleen (esim. primitiivit) koodin käsittely on helppoa, esim. makroilla tai read:lla monet rakenteista, esim. let ja cond Schemeen verrattuna muut Lispit: eivät yritä olla minimaalisia (eivätkä ole niin siistejä kielinä) ovat yleensä vähemmän funktionaalisia käyttävät silmukoita häntärekursion sijaan käyttävät enemmän makroja ja erikoisia kontrollirakenteita yleensä muuttujilla ja funktioilla on omat nimiavaruudet (ja Schemeen verrattuna ylimääräistä syntaksia niiden käsittelyyn) Common Lisp yhdisti oman aikansa Lispejä yhdeksi isoksi kieleksi (noin 198186) nykyään yleisimmät Lispit lienevät Scheme, Common Lisp ja ohjelmien sisäiset Lispit esim. Emacsissa ja Autocadissa
Esimerkkejä Common Lisp -koodista muutama Common Lispin rakenne: declare: kertoo muuttujien tyypeistä yms. kääntäjälle (eli koodissa voi antaa vinkkejä optimointia varten) tapa dokumentoida proseduurit koodin sisällä (docstring) on lainattu Lispistä suoraan mm. Pythoniin funktioilla voi olla monta paluuarvoa (myös Schemessä) Common Lispin oliojärjestelmä CLOS on monipuolinen ja melko erilainen kuin muissa kielissä (opettavaisella tavalla erilainen?) loop: hyvin monipuolinen silmukkarakenne ( yhdistelmä silmukoista ja list comprehensionista) tagbody ja prog: yhdistelmiä Schemen letistä ja gotosta koodiesimerkkejä Maxima-matematiikkaohjelmistosta: src/factor.lisp src/plot.lisp (selaile esim. näitä jos haluat nähdä miltä Common Lisp näyttää)
DSL-kielistä DSL-kieli (domain specic language) on tiettyyn sovellusalueeseen tehty ohjelmointikieli usein se on periaatteessa yleiskäyttöinen ohjelmointikieli seuraavassa esimerkkinä DSL-kielestä Emacs-tekstieditorin oma Lispin murre se tehtiin Emacsin kongurointiin ja laajentamiseen (muuten pitäisi esim. muuttaa Emacsin C-koodia ja kääntää se uudelleen) kielessä on valmiina paljon tekstieditoriin liittyvää toiminnallisuutta monissa muissakin ohjelmissa on sisällä jonkin kielen tulkki nykyään se on usein tavallinen (skripti)kieli, johon on valmiiksi ladattu ohjelmaan liittyviä funktioita, makroja tms. esim. Python, Lua, Perl, Scheme tai Javascript usein sillä voi laajentaa ohjelmaa vain tietystä kohdasta (esim. Gnomen Hearts-korttipeliin voi tehdä tekoälyjä Pythonilla) yksinkertaisempi variaatio on ohjelmaa varten tehty kieli esim. konguraatiotiedostoille, jossa on jotain ohjelmointikielen ominaisuuksia mutta ei välttämättä esim. silmukoita tai funktioita
Emacsin Lisp-koodista Emacs Lisp on melko iso kieli (yleensä DSL on paljon pienempi) monet Emacsin lisätoiminnot on tehty sillä iso valmis kirjasto erityisesti tekstieditoriin liittyvistä asioista myös mm. oma GUI-kirjasto koodia on paljon: noin 1,2 miljoonaa koodiriviä (GNU Emacs 22.2) hyvin Lisp-mäistä koodia (paljon vanhoja muista Lispeistä lainattuja ohjelmointitapoja) koodi ei ole kovin monimutkaista, abstraktiot yksinkertaisia yleensä hyvin dokumentoitua (docstring-ominaisuudella) kielessä ei ole näkyvyyksiä, ja monia sisäisiäkin funktioita voi käyttää suoraan editorista ja käytetään muualta laajennettavuuden kannalta vähän haurasta: rakenteellisissa muutoksissa pitäisi muuttaa paljon koodia
Muutamia Emacs Lispin kontrollirakenteita (save-excursion koodia ): tallentaa kursorin yms. paikan ja palauttaa sen koodin suorituksen jälkeen imperatiivisempi rakenne olisi tallettaa vanhat arvot paikalliseen muuttujaan ja asettaa ne takaisin lopuksi (with-current-buffer puskuri koodia ) ajaa koodin niin, että sen ajaksi nykyinen editoitava puskuri vaihdetaan annetuksi muitakin with--alkuisia funktioita tai makroja on paljon myös Schemessä on esim. with-output-to-file defadvice: tapa lisätä olemassaolevan funktion alkuun ja/tai loppuun koodia pienissä paloissa, joita voi myöhemmin yksitellen kytkeä päälle ja pois
Emacs Lisp -koodiesimerkkejä katso vaikka näitä jos haluat nähdä Emacs Lisp -koodia: lisp/paren.el (260 riviä): show-paren-mode: yksi suoraviivainen funktio ja asetuksia lisp/calendar/cal-mayan.el (380 riviä): monta pientä funktiota, jotka käyttävät calendarin kirjastoa lisp/xml.el (900 riviä): yksinkertainen recursive descent -tyyppinen XML-jäsennin lisp/font-lock.el (2 300 riviä): sekavahkoa Lispiä, säännöllisten lausekkeiden käyttöä lisp/progmodes/gud.el (3 400 riviä): enimmäkseen tukea erilaisille debuggereille; lähettää tekstikomentoja aliprosessille tai sano Emacsissa C-h k ja joku käyttämäsi näppäinyhdistelmä: ohjetekstin ensimmäinen linkki on lähdekoodiin