Ohjelmoinnin peruskurssien laaja oppimäärä Luento 13: Scheme-tulkki Pythonilla, datan serialisointi, keväästä Riku Saikkonen 11. 12. 2012
Sisältö 1 Scheme-tulkki Pythonilla 2 Datan serialisointi 3 Suunnitelmia kevään kursseista
Tulkin tekeminen muilla kielillä kuin Schemellä edellä tehtiin Scheme-tulkkia Schemellä entä jos tekisi kielen x tulkin kielellä y (esim. Scheme Pythonilla)? melkein aina pitää tehdä itse lausekkeiden esitysmuoto tulkissa ja esikäsittelyvaihe, joka lukee ohjelman merkkijonona ja jäsentää (parse) sen tähän omaan esitysmuotoon (edellä Schemen sulkulausekkeet ja read tekivät nämä) sen sijaan varsinaisen tulkin perusrakenne on yleensä sama jos kieli y ei tue mutta x:n pitää tukea: häntärekursiota eval-funktio pitää muuttaa silmukaksi poikkeuksia kutsupino pitää itse tallettaa tulkin sisälle sen sijaan, että se kutsuisi itseään rekursiivisesti automaattista muistinhallintaa oma roskankeruualgoritmi jne. muille vastaaville rakenteellisille ominaisuuksille seuraavaksi esimerkkinä osa Scheme-tulkista Pythonilla
Scheme-lausekkeiden esitysmuoto Pythonissa on suoraviivaista esittää Scheme-lauseke (define fact (lambda (n) (if (= n 0) 1 (* n (fact (- n 1)))))) sulkulauseketta vastaavana Python-listana: ['define', 'fact', ['lambda', ['n'], ['if', ['=', 'n', 0], 1, ['*', 'n', ['fact', ['-', 'n', 1]]]]]] tulkki siis voisi tallettaa lausekkeensa tässä muodossa muoto toimii millä tahansa sulkulausekkeilla joten esim. quote ja makrot toimisivat myös tosin oikeastaan pitäisi vielä mm. erottaa symbolit ja merkkijonot toisistaan sekä tukea mm. #t:tä ja Schemen erikoisia numerotyyppejä ja keksiä esitysmuoto pareille, jotka eivät pääty tyhjään listaan
Tulkki tälle esitysmuodolle Osa tulkin evalin koodista def tagged_list(exp, tag): return instanceof(exp, list) and exp[0] == tag def istrue(val): return val!= False def seval(exp, env): if... elif tagged_list(exp, 'if'): if istrue(seval(exp[1], env)): return seval(exp[2], env) else: return seval(exp[3], env) else: # proseduurikutsu proc = seval(exp[0], env) args = [seval(e, env) for e in exp[1:]] return sapply(proc, args) eli SICP-kirjan tulkin voisi kääntää suoraviivaisesti Pythoniksi tämä tulkin rakenne toimisi, mutta se ei ole tyypillinen tapa tehdä tulkkeja olio-ohjelmointikielellä
Lausekkeet olioina tyypillisempi tapa tehdä tulkkeja on esittää lauseke oliona, joka kuvaa sen ulointa osaa ja sisältää viittauksia alilausekkeisiin esim. if-lauseketta vastaisi luokka If, jonka kentissä olisi tallessa ehtoa, then- ja else-haaraa vastaavat lausekkeet eli tulkin abstrakti syntaksi olisi näitä luokkia tällaiselle lausekeoliolle tarvitaan: konstruktori, joka ottaa alilausekkeet talteen eval-metodi, joka suorittaa lausekkeen ja palauttaa sen arvon, kutsuen alilausekkeiden eval-metodeja (lisäksi oliossa voisi olla metodeja lausekkeen tulostamiseen, sen käyttämien muuttujien luettelemiseen optimointeja varten, jne.) lausekkeen suorittaminen vaatii pääsyä muuttujien arvoihin joten evalille annetaan argumentiksi ympäristö tulkki voi siis suorittaa minkä tahansa lausekkeen kutsumalla sen eval-metodia
Lausekkeet olioina: esimerkki Scheme-lauseke (define fact (lambda (n) (if (= n 0) 1 (* n (fact (- n 1)))))) voidaan esittää Pythonilla olioina esimerkiksi näin: Define('fact', Lambda(['n'], [If(Call(Var('='), Var('n'), SelfEval(0)), SelfEval(1), Call(Var('*'), Var('n'), Call(Var('fact'), Call(Var('-'), Var('n'), SelfEval(1)))))])) em. Python-koodi on siis sisäkkäisiä kutsuja eri lausekeluokkien konstruktoreihin kukin konstruktori ottaa argumentiksi lausekeluokkien osat, esim. Lambda ottaa parametrilistan ['n'] ja proseduurin koodin (lista lausekeolioita, tässä vain yksi If) SelfEval(1) tarvitaan, jotta myös sille voisi kutsua eval-metodia
Lausekkeet olioina: lausekeluokan toteutus tässä esimerkkinä If:n toteutus; muut ovat samankaltaisia Osa tulkin koodista (koko If) class Expr(object): # po. abstrakti luokka def eval(self, env): raise NotImplementedError schemeeval.py class If(Expr): def init (self, condition_expr, then_expr, else_expr): self.condition_expr = condition_expr self.then_expr = then_expr self.else_expr = else_expr def eval(self, env): if self.istrue(self.condition_expr.eval(env)): return self.then_expr.eval(env) else: return self.else_expr.eval(env) def istrue(self, val): return (val!= False)
Ympäristöt mitä muuta tulkkiin tarvitaan kuin kaikkien eri lauseketyyppien toteutus? 1 ympäristöt (eli paikka, jossa muuttujat ovat tallessa) 2 proseduurit ja niiden kutsuminen (eli SICP-tulkin apply) 3 muut arvot, joita ei ole samanlaisina alla olevassa kielessä (jos sellaisia on; tässä esim. Schemen symbolit) 4 ehkä käyttöliittymä (riippuen tulkin käyttötarkoituksesta) ympäristöissä voi käyttää Pythonin valmiita tietorakenteita: esim. kehys on dict muuttujannimistä arvoihin, ja ympäristö on lista näitä kehyksiä paikallisia muuttujia tehdessä (proseduurikutsussa) voi tehdä uuden ympäristöolion, jossa on yksi kehys lisää
Proseduurien toteutus: lausekkeet Lambda-lauseke ja proseduurikutsulauseke class Lambda(Expr): def init (self, params, body): self.params = params self.body = body def eval(self, env): return Procedure(self.params, self.body, env) schemeeval.py class Call(Expr): def init (self, procexpr, *argexprs): self.procexpr = procexpr self.argexprs = argexprs def eval(self, env): proc = self.procexpr.eval(env) argvals = [expr.eval(env) for expr in self.argexprs] return proc.apply(argvals) koodi on samaa kuin SICPin tulkissa (paitsi siirrettynä luokkiin): proseduurikutsussa esim. (fact (+ 1 2)) kutsutaan evalia kaikille lausekkeen osille ja applyä saadulle proseduurioliolle
Proseduurien toteutus: proseduurioliot Proseduurioliot (omat proseduurit ja primitiivit) class Callable(object): # po. abstrakti luokka def apply(self, argvalues): raise NotImplementedError schemeeval.py class Procedure(Callable): def init (self, params, body, env): self.params = params self.body = body self.env = env def apply(self, argvalues): # uusi ympäristö, jossa on uusi kehys ja sen jälkeen env applyenv = self.env.extended(self.params, argvalues) r = None for expr in self.body: r = expr.eval(applyenv) return r class Primitive(Callable): def init (self, python_func): self.python_func = python_func def apply(self, argvalues): return apply(self.python_func, argvalues) # Pythonin apply
Entä jonkin muun kielen tulkki? esimerkkikoodissa schemeeval.py on toimiva tulkki, jossa on useimmat SICP-kirjan tulkin ominaisuudet mikä muuttuisi, jos tehtäisiin tulkki muulle kuin Schemelle? suurin osa lausekeolioista olisi samoja (lähes kaikille kielille yhteisiä) paljon monimutkaisempi jäsennin kielen syntaksille Definen sijaan monessa kielessä olisi erilliset lausekkeet proseduurien ja muiden muuttujien määrittelyyn (eikä Lambdaa) muitakin tapoja tehdä paikallisia muuttujia kuin proseduurikutsu (esim. oma lauseketyyppi joka vastaisi Schemen letiä) silmukkarakenteita operaattorit ja vastaavat voisivat olla Call-lausekkeita, paitsi jos primitiiviä ei haluta hakea nimen perusteella ympäristöstä isommassa ohjelmointikielessä voisi olla myös oliot, moduulijärjestelmä, poikkeukset, staattiset tyypit, jne. eli lähinnä lisää ominaisuuksia samaan runkoon (ja jäsennin) poikkeus: runko muuttuisi jonkin verran, jos pitäisi pitää itse yllä kutsupinoa (esim. poikkeusten tekemistä varten)
Sisältö 1 Scheme-tulkki Pythonilla 2 Datan serialisointi 3 Suunnitelmia kevään kursseista
Yleisiä tapoja datan siirtoon useimmat ohjelmat tallettavat ja lataavat dataa tiedostoista tai esim. verkon yli toiselta ohjelmalta esim. lautapeli voisi tallentaa pelin (eli laudan sisällön) tai verkkopelinä toimiessaan lähettää sen toiselle pelaajalle myös tulkin lausekkeet ovat tällaista dataa miten tällainen data kannattaa esittää? itse keksityllä binääri- tai tekstitiedostolla? jollain puolivalmiilla formaatilla kuten XML:nä? jonkin muun ohjelman ennestään käyttämällä formaatilla? omia binääriformaatteja on helppo tuottaa joistain ohjelmointikielistä (muistin sisältö levylle), mutta vaikea käsitellä muilla kielillä tai eri versioilla seuraavilla kalvoilla on muutama yleinen puolivalmis formaatti
Datan serialisointi serialisointi (serialization, marshalling) tarkoittaa ohjelmassa olevan datan muuttamista merkkijonoksi ja takaisin erityisesti ohjelmointikielen tietorakenteiden automaattista tallentamista ja lataamista Schemessä read ja write toteuttavat Scheme-datan serialisoinnin mm. Pythonissa ja Javassa on valmiit kirjastot olioiden serialisointiin kielen omaan (binääri)formaattiin, jota on tarkoitus käsitellä vain näillä kirjastoilla itsellään joten se ei toimi kunnolla kielten välillä eikä tiedostoformaattia usein voi luotettavasti dokumentoida kaikkea ei voi serialisoida (esim. useimmiten funktioita) käytännössä serialisointi tehdään usein kuitenkin osittain käsin esimerkiksi jotta ohjelman eri versiot toimisivat ristiin, vaikka olioiden rakenne muuttuu tai jotta ylimääräistä esim. väliaikaista dataa ei menisi talteen
Sulkulausekkeet Esimerkki datasta sulkulausekkeina (osm ((version "0.6") (generator "CGImap 0.0.2")) (bounds 60.1711000 24.8552600 60.1760400 24.8672200) (way 26026161 (user "alv") (uid 4660) (visible #t) (version 2) (changeset 139696) (timestamp "2008-08-08T08:00:57Z") (nodes 284132199 284132200 286097549) (tags ("highway" "footway") ("surface" "unpaved")))) Schemen read- ja write-primitiivit käyttävät Scheme-koodin näköistä sulkulauseke-esitysmuotoa esitysmuoto on nimeltään S-expression (symbolic expression) sitä on kohtalaisen helppo kirjoittaa ja lukea automaattisesti ilmankin ohjelmointikielen tukea
JSON (JavaScript Object Notation) Esimerkki datasta JSON-muodossa { "osm": { "version": "0.6", "generator": "CGImap 0.0.2" }, "bounds": { "minlat": 60.1711000, "minlon": 24.8552600, "maxlat": 60.1760400, "maxlon": 24.8672200 }, "ways": [ { "id": 26026161, "user": "alv", "uid": 4660, "visible": true, "version": 2, "changeset": 139696, "timestamp": "2008-08-08T08:00:57Z", "nodes": [ { "ref": 284132199 }, { "ref": 284132200 }, { "ref": 286097549 } ], "tags": [ { "k": "highway", "v": "footway" }, { "k": "surface", "v": "unpaved" } ] } ] } JSON-formaatti on tehty JavaScript-kielen pohjalta periaatteessa se on koodia (olion ja/tai taulukon alustus), jonka voisi suoraan antaa JavaScript-tulkille käytännössä sitä useimmiten luetaan kirjaston avulla käytetään varsinkin webbisovelluksissa selaimessa pyörivän JavaScript-koodin ja palvelimen väliseen tiedonsiirtoon
XML Esimerkki XML-datasta (pala OpenStreetMap-dataa) <?xml version="1.0" encoding="utf-8"?> <osm version="0.6" generator="cgimap 0.0.2"> <bounds minlat="60.1711000" minlon="24.8552600" maxlat="60.1760400" maxlon="24.8672200"/> <way id="26026161" user="alv" uid="4660" visible="true" version="2" changeset="139696" timestamp="2008-08-08t08:00:57z"> <nd ref="284132199"/> <nd ref="284132200"/> <nd ref="286097549"/> <tag k="highway" v="footway"/> <tag k="surface" v="unpaved"/> </way> </osm> XML-esitysmuotoon kuuluu yleensä skeema (schema), joka määrittelee, mitä alikohtia missäkin saa olla ja valmiita työkaluja, jotka tarkistavat skeemanmukaisuuden, editoivat XML:ää, hakevat XML:stä osia tietyillä hakuehdoilla, konvertoivat XML:ää muunlaisiin formaatteihin, jne. XML:n käsittelyyn on kirjastoja useimmissa ohjelmointikielissä
Itse tehdyt formaatit hyvin usein ohjelmissa käytetään edellä lueteltujen formaattien sijaan omia itse suunniteltuja formaatteja etuja: joustavampia ja tilanteeseen sopivampia, joskus yksinkertaisempia ymmärtää, usein helpompia kirjoittaa käsin haittoja: itse tehty jäsennin ei yleensä ole yhtä monipuolinen; omaa formaattia käsitteleviä muita työkaluja ei ole valmiina etu ja haitta: ei tarvitse käyttää valmiita kirjastoja se, että jokin data esitetään sulkulausekkeina, JSONina tai XML:nä ei useinkaan kerro vielä kovin paljon pitää vielä määritellä, miten niitä käytetään nämä valmiit formaatit helpottavat suunnittelua, mutta eivät tee sitä ohjelmoijan puolesta usein näiden valmiiden formaattien sisälle (esim. tiettyihin merkkijonoihin) määritellään vielä itse jokin oma formaatti
Mitä Pythonista löytyy? Pythonin pickle-paketti serialisoi Python-olioita automaattisesti formaatti on Pythonille sisäinen: ei ole tarkoitus käsitellä muuten kuin pickle:llä Pythonin repr-funktio tuottaa tekstimuotoisen esityksen argumentistaan (esim. "['abc', 3, [1, 2, 3]]") kuten Schemen write, paitsi että repr:n tulostetta ei ole tarkoitus lukea takaisin Pythoniin Pythonille on kirjastot mm. XML:n ja JSONin käsittelyyn pitääkö kirjastolta saaduille olioille tehdä vielä itse jotain? yleensä ne pitää käydä läpi ja tarkistaa, varsinkin jos data tulee ohjelman tai sen käyttäjän ulkopuolelta kirjastot siis auttavat mutta eivät korvaa kokonaan esim. datatiedoston tai verkkopaketin lukevaa funktiota samoin kielen oma serialisointi (esim. pickle)
Sisältö 1 Scheme-tulkki Pythonilla 2 Datan serialisointi 3 Suunnitelmia kevään kursseista