SEMINAARI ESSEE: DYLAN TIE-20306 Principles of Programming Languages Tiivistelmä Tässä esseessä käydään läpi Dylan ohjelmointikielen perus piirteet ja ominaisuudet. Alussa myös lyhyt historia kielen synnystä ja sen kehitysvaiheista. Mukana myös koodi esimerkkejä muun muassa mergesortista toteutettuna Dylanilla. Jaakko Laitinen & Shayan Shajarian Ryhmä 27
1 Johdanto Dylan nimi tulee englannin sanoista dynamic ja language, eli dynaaminen kieli, mikä kuvastaa jo hyvin kielen luonnetta ja myös sen tavoitteita ja tarkoitusta. Dylan sai alkunsa 1990-luvulla Applen sisäisenä projektina. Alun perin Dylanin oli tarkoitus tulla käyttöön Applen omaan kehitystyöhön, mutta Dylanin kehitys ei edennyt tarpeeksi nopeasti ja lopulta projekti hylättiin Applen osalta. Dylanin kehitystä jatkoi kuitenkin Applen ulkopuoliset tahot ja Dylanin kehitys keskittyi pääasiassa Windows alustalle. Lopputuloksena Dylan siirtyi avoimen lähdekoodin projektiksi, joka tunnetaan nykyään Open Dylanina. Dylan kääntäjästä on unix alustalle suunnattu versio nimeltä Gwydion Dylan, joka tosin vain luo C-koodia Dylanin lähdekoodista. Dylan on saanut vaikutteita muun muassa Lispistä(CLOS) ja Shcemasta. Alun perin Dylanin syntaksi suunniteltiin olemaan Lispin kaltainen prefix-syntaksi. Tämän prefix-syntaksin pelättiin kuitenkin olevan liian vieras kohderyhmälle, joten lopulta Dylanin kehittäjät päätyivät tutumpaan C:n kaltaiseen infix-syntaksiin. Luvussa 2 käydään läpi Dylanin ominaisuuksia kurssilla mietittyjen aiheiden näkökulmasta. Luku 3 taas esittelee muita kielen mielenkiintoisia ominaisuuksia. Lopuksi vielä lyhyt yhteenveto, käytetyt lähteet ja koodiesimerkit. 2 Pääpiirteet Dylanin pääasiallinen pyrkimys on ollut luoda kieli, joka toisaalta abstrahoi turhan laitteistoläheisyyden pois tarjoten nopean tavan rakentaa prototyyppejä, mutta mahdollistaen prototyyppien jatkojalostuksen ohjelmiksi, joidenka suoritus olisi verrattavissa laitteistoläheisemmin kehitettyihin vastaaviin ohjelmiin. Tämä kehityskaari näkyy esimerkki koodissa (Koodi 3 ja Koodi 2), jossa on esitelty kaksi kielen hyväksymää syntaktista ääripäätä yksityiskohtien suhteen. Kun esimerkiksi muuttujien ja parametrien tyypit on eksplisiittisesti ilmoitettu, kääntäjän pitäisi pystyä generoimaan tehokkaampaa koodia kuin, jos tyypit jätetään auki. 2.1 Paradigmat Dylan on yhdistelmä funktionaalista- ja olio-paradigmaa. Kaikki Dylanissa ovat olioita ja niitä voidaan käsitellä saman arvoisesti. Vaikka Dylanissa ohjelman rakenne voi muistuttaa imperatiivisen kielen rakennetta (kuten C) peräkkäin suoritettavien lausekkeiden takia, on se kuitenkin suurelta osin funktionaalinen kieli. Funktioita käsitellään kuten mitä tahansa muuta luokkaa ja yleensä keskitytään prosessoimaan annettua sisääntuloa sen muuttamisen sijaan. Dylanin olio-luonne tarjoaa kuitenkin myös niin sanotut mutable-luokat joiden tilaa voi muuttaa vasten funktionaalisuuden periaatetta. Dylan tarjoaa kaikki yleisimmät olio-ohjelmoinnin ominaisuudet kuten käyttäjän määrittämät luokat, (moni- )periyttämisen ja polymorfismin; ainakin omalla tavallaan. Sen sijaan, että luokalle määritettäisiin suoraan metodeja, luokka määrittelyssä spesifioidaan ainoastaan luokan tilamuuttujat. Näille luokan muuttujille luodaan automaattisesti myös funktiot arvojen asettamiseen ja palauttamiseen. Dylan käyttää niin sanottua multiple dispatch mekanismia hoitamaan tiettyyn luokkaan liittyvät funktiot ja niiden kutsumisen. Tästä tarkemmin luvussa 3.1. 2.2 Tyyppi järjestelmä Dylanissa pääasiassa käytetyt tyypit ovat luokat (classes), singletonit (singletons), unionit (unions) ja rajatut (limited). Kaikki oliot ovat siis esiintymiä jostain näistä. Singletonit tekevät ikään kuin oliosta itsestään luokan/tyypin. Tämä tarkoittaa esimerkiksi funktiokutsun parametrissa, että funktiota pitää kutsua tietyllä 1
oliolla, johon singleton-tyyppi perustuu. Union kokoaa useamman luokan/tyypin yhdeksi tyypiksi ja parametrina se siis hyväksyy minkä tahansa olion, joka kuuluu unionin luokkiin/tyyppeihin. Rajatut tyypit taas ottavat jonkin luokan ja muodostaa sille ali-tyypin, jolla on myös muita rajoitteita. Esimerkiksi <integer>luokasta voi määrittää version, jonka arvo alue on rajattu tietylle välille. Koko tyyppi hierarkian huipulla on <object>-luokka ja kaikki Dylanissa on joko tämän esiintymä ("olio") tai alityyppi tästä (edellä mainitut tyypit). Luokkien yhteydessä periyttäminen määrittelee luokka hierarkian ja aliluokka suhteet. 2.3 Muuttujat ja parametrit Dylanissa kaikki ovat olioita, jopa numeeriset vakiot eli numerot. Näin ollen kaikki muuttujat ja parametrit ovat vain viitteitä tiettyyn olioon ja muuttujat vain sidotaan tiettyyn olioon. Dylanissa "="-merkki sitoo muuttujaan olion muuttujan määrittelyn yhteydessä, mutta pitää käyttää ":="-merkkiä kun muuttuja halutaan sitoa uuteen olioon. Dylanissa siis kaikki muuttuja ja funktiot käsitellään niin sanotusti sidontoina (bindings). Tämä tuo muun muassa vertailuun eri tasoja. Kun kahta oliota vertaillaan "="-merkillä, testataan yleensä, onko viitatut oliot samaa luokkaa ja onko niillä sama rakenne (käyttäjä voi määritellä oman funktion "="-operaattorille, kun sitä käytetään tiettyjen luokkien kanssa). Kun taas vertaillaan "=="-merkillä, testataan viittaavatko muuttujat samaan olioon. Funktiolle voidaan antaa C/C++:n jne. tyyliin (pakollisia) parametreja, jotka on annettava tietyssä järjestyksessä. Tämän lisäksi avainsanalla #key voidaan määrittää vapaaehtoisia avain-parametreja, jotka annetaan pakollisten parametrien jälkeen ensiksi antamalla avain-parametrin nimi ja sitten arvo (järjestyksellä ei väliä). Avain-parametreille annetaan oletus arvo (tai false). Dylanissa on myös mahdollista antaa parametrilistoja avain sanalla #rest. Tämä järjestää loput parametrit listaksi. Monipuolisen funktio parametri järjestelmän lisäksi funktiot voivat palauttaa useamman kuin yhden arvon kerrallaan. 2.4 Dynaaminen muistinhallinta Dylanissa käytetään automaattista roskien keruuta eli eksplisiittistä muistin varausta ja vapautusta ei tarvita. Tämä voi aiheuttaa pientä hidastumista manuaaliseen muistin hallintaan verrattuna, mutta roskien keruulla voidaan välttää yleisimpiä muistinhallintaan liittyviä virheitä. Hyvin toteutettu kääntäjä voi myös päätellä milloin olio luodaan vain pinoon, jolloin sen vapauttaminen on helppoa. 2.5 Enkapsulointi Enkapsulointi menetelmistä luokat ovatkin jo mainittu edellä, mutta niiden lisäksi Dylan tarjoaa niin sanotut moduulit (modules) ja kirjastot (libraries) hallitsemaan ohjelman rakennetta. Yksinkertainen moduuli/kirjasto esimerkki on esitetty dokumentin lopussa (Koodi 1). Moduuli määrittää, mitä muita moduuleita käytetään (moduulin ympäristössä) ja se myös määrittää, mitkä sidonnat paljastetaan (export) eli mitkä luokat/muuttujat/funktiot ovat käytettävissä moduuleissa, jotka käyttävät määriteltävää moduulia. Kirjastot taas määrittelevät, mitä muita kirjastoja tullaan käyttämään. Kirjastot vastaavasti taas paljastavat moduulit, joita kyseiset kirjastot haluavat tarjota muille. Näitä yhdistämällä käyttäjällä on hyvin hienojakoinen tapa hallita, mitä rajapintoja mikin kirjasto paljastaa. 3 Muita ominaisuuksia Tässä luvussa esitellään vielä muita Dylanin ominaisuuksia, joita ei aikaisemmissa luvuissa tullut esille. Dylanista löytyy melko paljon erilaisia ominaisuuksia, joten tässä käydään läpi vain pintapuolisesti mielenkiintoisimpia ominaisuuksia. 2
3.1 Multiple dispatch ja geneeriset funktiot Dylanissa funktiot tukevat niin sanottuja multimetodeja (multimethods/multiple dispatch). Tämä tarkoittaa, että funktiokutsun yhteydessä kutsuttava funktio valitaan jokaisen parametrin tyypin/luokan perusteella. Prosessi menee korkealla tasolla niin, että aluksi jokaista parametria kohden etsitään kaikki funktion versiot, jotka sopivat kyseiseen parametriin eli funktiot, joiden parametrin luokkaan kyseinen parametrin luokka on ali-luokkana. Löydetyt funktiot järjestetään niiden spesifiyden mukaan. Spesifiys tässä tarkoittaa luokkaa, joka on perintä hierarkiassa mahdollisimman lähellä parametrina annetun olion varsinaista luokkaa. Lopuksi valitaan mahdollisimman spesifi funktio, joka löytyy kaikkien parametrien listasta. Luokkien funktiot voidaankin määrittää vain kirjoittamalla funktio, joka ottaa kyseisen luokan olion parametrina. Tämä mahdollistaa niin sanottujen geneeristen funktioiden määrittelyn. Geneerinen funktio ei vastaa mitään varsinaista toteutusta, mutta ohjelmoija voi määritellä samalla nimellä metodeja, joilla on jokin varsinainen toteutus ja parametreilla luokka, joille funktio halutaan määritellä. Parametrien luokka määrittää varsinaisesti kutsutun funktio yllä esitetyn mukaisesti. Periyttämisen yhteydessä periytetty luokka voi hyödyntää ylä-luokan funktioita, mutta se voi myös laajentaa tai määrittää uudestaan kyseiset funktiot. Laajentaminen onnistuu kutsumalla next-method()-funktiota, joka kutsuu seuraavaksi spesifeintä funktiota (yleensä siis ylä-luokan vastaavaa funktiota). 3.2 Poikkeuskäsittely ja lohkot Dylanissa tarjotaan poikkeuskäsittelyyn hieman muista kielistä poikkeava mekanismi. Dylan tarjoaa niin sanotun signaloinnin (signaling). Koodiin voi lisätä signal-kutsun, jolla voidaan käsitellä poikkeukselliset tilanteet. Toisin kuin esimerkiksi C/C++:ssa, signalointi ei keskeytä ohjelman suoritusta vaan se on kuin funktiokutsu ja se etsii viimeisimmän käsittely-funktion (handle-funktion), joka sitten suoritetaan. Käsittely funktio asetetaan 'let handler <error-class> = error-handler-func'-komennolla. Käsittely funktio voidaan siis asettaa missä vain ennen signalointia ja sitä kutsutaan sitten signaloidussa kohdassa. Kuten muutkin funktiot, myös signalointi-kutsu voi palauttaa jonkin arvon poikkeuksesta palautumiseksi. Edellä mainitulla mekanismilla ei kuitenkaan ole mahdollista poiketa ohjelman normaalista suorituksesta. Ohjelman normaalin suorituksen muuttamiseksi Dylanissa on niin sanotut lohkot (blocks)(katso Koodi 2 ja Koodi 3), jotka muistuttavat esimerkiksi C/C++:n goto-lausetta tai break-lausetta. Lohkot voivat palauttaa arvoja ja ne määritetään 'block(block_id)...end'-rakenteella. Nyt kutsumalla block_id(ret_val)-funktiota keskeytyy normaali suoritus ja kyseisestä lohkosta poistutaan. Lohkoihin voi myös määrittää vapaaehtoiset afterwards- ja cleanup-osiot. Afterwards-osio suoritetaan vain normaalin suorituksen jälkeen. Cleanup-osio suoritetaan aina lohkon jälkeen (vaikka olisi poistuttu kesken). Lohkoihin voidaan lisäksi vielä yhdistää signalointi, jolloin saadaan C++:n try-catch-lohkon kaltainen rakenne lisäämällä exception-osio. 4 Yhteenveto Imperatiivisia/olio kieliä käyttäneelle Dylan voi aluksi vaikuttaa melko tutulta, mutta se silti sisältää vahvan funktionaalisen pohjan. Dylan tarjoaa paljon ominaisuuksia, joita käytetyimmissä kielissä harvemmin tapaa. Jää tosin ohjelmoijan vastuulle hyödyntää niitä. Toisaalta ominaisuuksien moninaisuus voi hankaloittaa kielen omaksumista, joten ohjelmoija voi helposti tukeutua vanhoihin tapoihinsa, vaikka Dylan tarjoaisikin paremman tavan saavuttaa sama asia. 3
5 Lähteet Dylan Hackers, "An Introduction to Dylan", saatavilla: http://opendylan.org/documentation/intro-dylan/index.html Open Dylan -> History, saatavilla: http://opendylan.org/history/index.html Neal Feinberg et al., "Dylan Programming", saatavilla: http://opendylan.org/books/dpg/index.html Andrew Shalit, "The Dylan Reference Manual", saatavilla: http://opendylan.org/books/drm/title Wikipedia, "History of the Dylan programming language", saatavilla: https://en.wikipedia.org/wiki/history_of_the_dylan_programming_language define library sort-lib use dylan; use mergesort-lib; use quicksort-lib; use bucketsort-lib; define library mergesort-lib use dylan; export mergesort; define module mergesort use dylan; export mergesort!, mergesort; Koodi 1: Esimerkki kuvitteellisesta sort-kirjastosta. 4
define method mergesort! ( lst ) block( skip ) if ( ~lst size(lst)==1 ) skip(); local method split-lst () values( copy-sequence (lst, end: size(lst)/2), copy-sequence (lst, start: size(lst)/2 + 1) ) let (l1,l2) = split-lst(lst); mergesort!(l1); mergesort!(l2); for (i from 0 to size(lst)-1) let (a,b) = values(head(l1),head(l2)); if ( ~b ( a & (a < b)) ) lst[i]:= a; l1:=tail(l1); else lst[i]:=b; l2:=tail(l2); Koodi 3: Esimerkki toteutus mergesortista. (Toimivuutta ei testattu käännösympäristön pystytyksessä ilmenneiden ongelmien takia) define method mergesort! ( lst :: <collection> ) => () block( skip ) if ( ~lst size(lst)==1 ) skip(); end if; local method split-lst () => (lst1 :: <collection>, lst2 :: <collection>) let (lst1,lst2) = values( copy-sequence (lst, end: size(lst)/2), copy-sequence (lst, start: size(lst)/2 + 1)) end method split-lst; let (l1,l2) = split-lst(lst); mergesort!(l1); mergesort!(l2); for (i from 0 to size(lst)-1) let (a,b) = values(head(l1),head(l2)); if ( ~b ( a & (a < b)) ) lst[i]:= a; l1:=tail(l1); else lst[i]:=b; l2:=tail(l2); end if; end for; end block; end method mergesort!; Koodi 2: Mergesort lisätyillä yksityiskohdilla. 5