Ohjelmoinnin peruskurssien laaja oppimäärä Luento 12: Dynaaminen sidonta, Lisp-kielistä, delay Riku Saikkonen 29. 11. 2011
Sisältö 1 Dynaaminen sidonta 2 Lisp-kielistä 3 DSL-kieli: Emacs Lisp 4 Laiskaa laskentaa: delay ja force
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 (define (eval exp env) (cond... m-eval-dynamic.scm ; envistä voisi nyt tehdä globaalin muuttujan ; 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. 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ä myös tavallisen (esim. globaalin) muuttujan arvon voi vaihtaa hetkeksi fluid-let-rakenteella, esim. (fluid-let ((x 2))...)
Sisältö 1 Dynaaminen sidonta 2 Lisp-kielistä 3 DSL-kieli: Emacs Lisp 4 Laiskaa laskentaa: delay ja force
Muista Lisp-kielistä kuin Scheme Lisp-kielten yhteisiä piirteitä: sulkusyntaksi (poikkeus: esim. Dylan-kielessä tavallinen syntaksi) monia asioita voi itse määritellä uudelleen (esim. primitiivit) koodin käsittely on helppoa, esim. makroilla tai read:lla muunkin sulkusyntaksin käsittely helppoa (listarakenteena) muut Lispit eivät yritä olla yhtä minimaalisia kuin Scheme mutta useimmat Schemen rakenteet (esim. let ja cond) löytyvät samanlaisina muista: Lisp-kielet ovat keskenään melko samankaltaisia Common Lisp yhdisti oman aikansa Lispejä yhdeksi isoksi kieleksi (noin 198186) nykyään yleisimmät Lispit lienevät Scheme ja Common Lisp ja ohjelmien sisäiset Lispit, esim. Emacsissa ja Autocadissa
Scheme vs. muut Lispit Schemeen verrattuna muut Lispit: eivät yritä olla minimaalisia eivät ole niin siistejä kielinä ovat yleensä vähemmän funktionaalisia käyttävät enemmän makroja ja erikoisia kontrollirakenteita pohjalla on paljon samaa, mutta muissa Lispeissä: yleensä muuttujilla ja funktioilla on omat nimiavaruudet (ja Schemeen verrattuna ylimääräistä syntaksia niiden käsittelyyn) useimmissa ei ole häntärekursio-optimointia, vaan iso joukko silmukkarakenteita
Esimerkkejä Common Lisp -koodista ks. Maxima-matematiikkaohjelmistosta esim. src/factor.lisp src/plot.lisp 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
Sisältö 1 Dynaaminen sidonta 2 Lisp-kielistä 3 DSL-kieli: Emacs Lisp 4 Laiskaa laskentaa: delay ja force
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
Emacs Lisp Emacs-tekstieditori ja Emacs Lisp ovat melko vanhoja ensimmäinen Emacs n. 1974, Lispillä n. 1985 editoria kehitetään edelleen aktiivisesti välillä mietitään kielen vaihtamista esim. Schemeksi, mutta se lienee liian työlästä... Schemeen verrattuna Emacs Lispissä: muuttujissa dynaaminen eikä leksikaalinen sidonta eri nimiavaruudet funktioiden ja muuttujien nimille ei häntärekursio-optimointia paljon laajempi kieli, mm. paljon makroja iso valmis kirjasto erityisesti tekstieditoriin liittyvistä asioista myös mm. oma GUI-kirjasto Emacs Lispiä käytetään sekä pienten asetusten ja lisätoimintojen tekemiseen että alisoftien tekemiseen (mm. sähköpostin lukuohjema, IRC-asiakas, pieni WWW-selain) jokainen Emacsin tehokäyttäjä tekee joskus jotain omaa?
Emacsin Lisp-koodista 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 Emacsissa mukana olevien lisäksi netistä löytyy satoja irrallisia lisäpaketteja
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 (save-window-excursion koodia ): tallentaa myös ikkunoiden sijainnit ja koot (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ä 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/progmodes/gud.el (3 400 riviä): enimmäkseen tukea erilaisille debuggereille; lähettää tekstikomentoja aliprosessille lisp/font-lock.el (2 300 riviä): sekavahkoa Lispiä, säännöllisten lausekkeiden käyttöä lisp/comint.el (3 600 riviä): aliprosesseihin liittyviä apufunktioita muille, major moden runko lisp/xml.el (900 riviä): yksinkertainen recursive descent -tyyppinen XML-jäsennin
Sisältö 1 Dynaaminen sidonta 2 Lisp-kielistä 3 DSL-kieli: Emacs Lisp 4 Laiskaa laskentaa: delay ja force
Schemen delay ja force laiska laskenta (lazy evaluation tai non-strict): lauseke lasketaan vasta sitten kun (ja jos) sen arvoa tarvitaan tavallisessa Schemessä laiskaa laskentaa voi tehdä delay- ja force-primitiiveillä (delay lauseke ) palauttaa erikoisarvon, joka kuvaa vielä laskematta olevaa lausekketta (force jonkin delayn paluuarvo ) pakottaa lausekkeen laskemisen ja palauttaa sen arvon lauseke evaluoidaan vain kerran: jos forcelle antaa jo kerran lasketun lausekkeen, se palauttaa saman arvon uudelleen aiemmalla luennolla mainitut virrat on toteutettu delayn ja forcen avulla: (cons-stream a b ) (cons a (delay b )) samantapainen rakenne on muutamissa muissakin kielissä (mm. Scala ja jotkut ML-toteutukset), ja moniin se on helppo toteuttaa itse (esim. Ruby ja Common Lisp)
delayn ja forcen toteuttaminen Schemessä (SICP 3.5.1) jos Schemessä ei olisi delaytä ja forcea, ne voisi toteuttaa muun Schemen avulla: (delay a ):n sijaan sanotaan (memo-proc (lambda () a )) tällöin forcen voi toteuttaa (define (force a) (a)) delayn voi tehdä makrona alla olevalla koodilla delayn ja forcen toteuttaminen (define (memo-proc proc) (let ((already-run? false) (result false)) (lambda () (if (not already-run?) (begin (set! result (proc)) (set! already-run? true) result) result)))) (define-macro (delay a) `(memo-proc (lambda (),a))) ; Lisp-makro (define (force a) (a))
delay ja force tulkkia muokkaamalla 1/2 tehdään delay ja force kirjan kohdan 4.1 Scheme-tulkkiin lisätään force uutena primitiiviproseduurina lisätään delay erikoismuotona, joka kutsuu tulkin apuproseduuria delay-it, jolle se antaa talletettavan lausekkeen apuproseduurit seuraavalla kalvolla Muutokset tulkkiin punaisella (define (delay? exp) (tagged-list? exp 'delay)) (define (delay-expression exp) (cadr exp)) m-eval-delay.scm (define (eval-delay exp env) (delay-it exp env)) (define (eval exp env) (cond... ((delay? exp) (eval-delay (delay-expression exp) env))... )) (define primitive-procedures (list... (list 'force force-it)...))
delay ja force tulkkia muokkaamalla 2/2 (osin SICP 4.2.2) Apuproseduureja (samoja käytetään kohdassa 4.2) m-eval-delay.scm (define (delay-it exp env) (list 'thunk exp env)) ; tallettaa exp-lausekkeeen ;; thunk: talletettu lauseke; evaluated-thunk: talletettu ja jo laskettu lauseke (define (thunk? obj) (tagged-list? obj 'thunk)) (define (thunk-exp thunk) (cadr thunk)) (define (thunk-env thunk) (caddr thunk)) (define (evaluated-thunk? obj) (tagged-list? obj 'evaluated-thunk)) (define (thunk-value evaluated-thunk) (cadr evaluated-thunk)) ;; primitiiviproseduuri, joka palauttaa talletetun lausekkeen arvon (define (force-it obj) (cond ((thunk? obj) ; laskematon talletettu lauseke? (let ((result (eval (thunk-exp obj) (thunk-env obj)))) (set-car! obj 'evaluated-thunk) (set-car! (cdr obj) result) ; korvaa lauseke arvolla (set-cdr! (cdr obj) '()) ; poista thunk-env result)) ((evaluated-thunk? obj) ; jo laskettu talletettu lauseke? (thunk-value obj)) (else obj))) ; jos ei ole talletettu lauseke, palauta se