Ohjelmointikielten periaatteiden taustaa Antti-Juhani Kaijanaho 8. tammikuuta 2007 How many times do I have to tell you? The right tool for the right job! Scotty elokuvassa Star Trek V Ohjelmointi on ongelmien ratkaisemista. Asiaa voi katsoa kahdelta kannalta: voi keskittyä ymmärtämään ongelmaa tai ratkaisumalleja. Voidaan ajatella, että maailma jakautuu kahteen osaan, ongelma-avaruuteen ja ratkaisuavaruuteen. Ongelma-avaruus koostuu kaikista mahdollisista ongelmista, ja ratkaisu-avaruus koostuu kaikista mahdollisista ratkaisuista. Ohjelmoijan tehtävänä on siirtää ongelma (joka elää ongelma-avaruudessa) ratkaisuavaruuteen, jolloin siitä tulee ratkaisu. Ohjelmointikieli on ohjelmoijan pääasiallisin työkalu, ja kielen toteutus (kääntäjä tai tulkki) auttaa ohjelmoijaa ratkaisun luomisessa: kielen ominaisuudet ratkaisevat, kuinka syvälle ratkaisuavaruuteen ohjelmoijan tulee ongelmaansa viedä suunnitteluvaiheessa ja mistä lähtien kielen toteutus tekee sen ohjelmoijan puolesta. 1 Kielten jaotteluja Ohjelmointikieliä on mielettömän paljon: HOPL 1 listaa niitä vajaat yhdeksän tuhatta. Jonkinlaista järjestystä tähän suureen joukkoon saadaan erilaisilla jaotteluilla. 1 http://hopl.murdoch.edu.au/ 1.1 Sukupolvet Eräs tunnettu mutta nykyisin vähämerkityksinen jaottelu jakaa kielet eri sukupolviin: Ensimmäinen sukupolvi koostuu konekielistä. Konekielellä tarkoitan tässä nyt ohjelman sitä muotoa, jonka kone pystyy sellaisemaan lukemaan. Seuraavassa on Hello World -ohjelma erään tietokoneen konekiellä (esitettynä 16-lukujärjestelmässä tavujonona): 980901013d6e185798020100980600036f686a656c6d612e6d6d7300 98070002f4ff00000000070100000000980400039807000748656c6c 980700076f2c2057980700076f726c6498070007210a0000980a00ff 0000000000000100980b0000203a4050104040204d20612069026e01 00812053207410100272010c82000000980c0008 Käytännössä konekieliset ohjelmat suunniteltiin kirjoittamalla ohjelmat ensin paperilla symbolisesti, käyttämällä konekäskyistä muistikasnimiä. Myös vuokaavioita saatettiin harrastaa monimutkaisten ohjelmien kirjoittamisessa. Konetta varten ohjelma käännettiin käsin konekielelle. Ensimmäinen sukupolvi oli vallalla 1940- luvun lopulta 1950-luvun alkupuolelle. Toinen sukupolvi syntyi, kun keksittiin, että voidaan kirjoittaa ohjelma suorittamaan yllämainittu käännöstyö muistikkaista konekielelle. Toinen sukupolvi koostuu siis symbolisista konekielistä. Edellä esitetty Hello World - ohjelma käännettiin seuraavasta symbolisella konekielellä kirjoitetusta ohjelmasta: argv IS $1 LOC #100 Main LDOU $255,argv,0 TRAP 0,Fputs,StdOut GETA $255,String TRAP 0,Fputs,StdOut 1
TRAP String BYTE 0,Halt,0 ", world",#a,0 Toinen sukupolvi oli vallalla 1950-luvun puoleenväliin asti. Kolmas sukupolvi syntyi vuonna 1955, kun FORTRAN kehitettiin. Lähes kaikki nykyisin ohjelmointikieliksi kutsumamme kielet ovat kolmannen sukupolven kieliä. Neljäs ja viides sukupolvi esiintyvät puheissa toisinaan, mutta niiden sisältö riippuu täysin puhujasta. Tavallisimmin neljännen tai viidennen sukupolven kielellä tarkoitetaan joko sovelluskehittimiä (Delphi ym.) tai ohjelmoitavia sovellusohjelmia (Excel ym.). 1.2 Paradigmat Edward Sapir ja Benjamin Whorf esittivät viime vuosisadan alkupuolella kielitieteellisen hypoteesin, joka tunnetaan nykyisin nimellä Sapirin Whorfin hypoteesi. Sen heikompi muoto, johon nykyisetkin kielitieteilijät enemmän tai vähemmän uskovat, kuuluu: Kieli vaikuttaa ajatteluun. Myös ohjelmoinnin yhteydessä tämä heikompi muoto on totta: se, mitä ohjelmointikieltä osaa, vaikuttaa siihen, miten ohjelmointiongelmia ryhtyy ratkaisemaan. Kaikki kielet eivät ole tässä suhteessa samanarvoisia: esimerkiksi Pascalin ja C:n tukema ajattelutapa on niin samanlainen, että kummastakin kielestä voi varsin vähällä vaivalla siirtyä toiseen. Vastaavanlainen samankaltaisuus pätee, tosin vähemmässä määrin, myös C++:aan ja Javaan. Jotkin kielet puolestaan suosivat niin erilaisia ajattelutapoja, että ne tuntuvat aluksi aivan eri maailmoilta, eikä toisen osaamisesta ole kovin suurta apua toisen osaamisessa. Ajatustavoiltaan läheisten kielten katsotaan yleensä kuuluvan samaan kieliparadigmaan, mutta niiden väliset rajapyykit eivät ole useinkaan kovin selvät. Kieliparadigmoille sukua ovat ohjelmointiparadigmat, tosin aika usein näiden välistä eroa ei nähdä (myös tämän kirjoittaja on ne monesti sekoittanut!). Kukin ohjelmointiparadigma on oikeastaan idealisoitu versio niistä ajattelutavoista, joita vastaava kieliparadigma suosii. Alan Perlis [14] kirjoitti kaksi vuosikymmentä sitten: A language that doesn t affect the way you think about programming, is not worth knowing. Minä kääntäisin tämän väitteen toisin päin: A language that affects the way you think about programming is worth knowing. Itse asiassa väittäisin, että jokaisen ohjelmointiin vakavissaan suhtautuvan tulisi tuntea ainakin pari-kolme eri kieliparadigmoihin kuuluvaa kieltä eli oikeastaan pari-kolme eri ohjelmointiparadigmaa. Ohjelmointiparadigmoiksi lasketaan tavallisimmin ainakin seuraavat (huomaa, että nämä paradigmat eivät ole toisistaan erillisiä, vaan ne menevät osittain päällekkäin): Imperatiivisen ohjelmoinnin (käskyohjelmoinnin) juuret ovat syvällä ratkaisuavaruudessa, von Neumannin arkkitehtuurin perusrakenteissa. Imperatiivinen ohjelmoija näkee maailman joukkona muistipaikkoja, joita muutellaan yksitellen von Neumannin pullonkaulaa pitkin. Imperatiiviselle ohjelmoinnille tyypillistä on tiedon muuttaminen in place (sitä kopioimatta), silmukoiden runsas käyttö, ohjelmakoodin korostuminen sekä käskymetafora. Deklaratiivisen ohjelmoinnin (kuvausohjelmoinnin) juuret ovat ongelma-avaruudessa, ongelmien täsmällisessä kuvaamisessa yleensä matematiikan keinoin. Sen taustalla on yleensä vahva matemaattinen teoria, joka on sovitettu ohjelmoinnin tarpeisiin. Deklaratiiviselle ohjelmoinnille tyypillistä on tiedon muuttumattomuus (muutos tehdään tekemällä uusi, erilainen kopio), rekursion runsas käyttö, tietorakenteiden korostuminen sekä laskentametafora. Proseduraalisen ohjelmoinnin (toimintoohjelmoinnin) perusabstraktio on aliohjelma. Proseduraalinen ohjelmoija keskittyy etsimään koodistaan tai suunnitelmastaan toimintoja, jotka toistuvat, ja tekee niistä aliohjelmia. Proseduraalinen ohjelmointi esiintyy yleensä imperatiivisen ohjelmoinnin kanssa yhdessä. Olio-ohjelmointi näkee kaiken olioina ja niiden välisenä kommunikaationa. Olio-ohjelmoija keskittyy hahmottamaan olioita ja niiden välisiä suhteita. Myös olio-ohjelmointi on yleensä imperatiivista ohjelmointia, vaikka dekla- 2
ratiivinen olio-ohjelmointi ei olekaan käsitteenä mahdoton ajatus. Funktio-ohjelmointi (funktionaalinen ohjelmointi) on eräässä mielessä proseduraalisen ohjelmoinnin yleistys. Funktio-ohjelmoija näkee ohjelman (matemaattisena) funktiona. Funktio-ohjelmalle tyypillisiä ilmaisukeinoja ovat funktionaalit (funktiot, jotka ottavat toisia funktioita parametrikseen). Funktioohjelmointi lasketaan perinteisesti deklaratiiviseksi ohjelmoinniksi, vaikka oikeastaan deklaratiivinen funktio-ohjelmointi onkin harvinaisempaa kuin imperatiivinen funktioohjelmointi, jossa funktiot ovat oikeastaan vain yleistettyjä aliohjelmia. Logiikkaohjelmointi perustuu ajatukseen, että looginen päättely voidaan mekanisoida (joka pitää paikkansa vain rajoitetusti). Logiikkaohjelmoija näkee ohjelman loogisena väitteenä, joka pitää todistaa. Logiikkaohjelma koostuu kyseisen väitteen muotoilusta sekä niiden oletusten muotoilusta, joita päättelyssä voidaan käyttää. Puhtaimmillaan logiikkaohjelmointi on täysin deklaratiivista, mutta käytännössä logiikkaohjelmointia harrastetaan myös imperatiivisesti, jossa päättelysäännöt vastaavat aliohjelmia. Olio-ohjelmoinnista ja funktio-ohjelmoinnista on kummastakin tiedekunnassamme kursseja (TJTSS33, TIEA341 ja TIES341). Ohjelmointikielet jaotellaan usein myös yllämainitttuihin paradigmalokeroihin. Tämä on kuitenkin varsin ongelmallista. Jokaisella ohjelmointikielellä on omin paradigmansa, jota se tukee parhaiten, mutta hyvin monet kielet ovat aidosti moniparadigmakieliä siinä mielessä, että niitä voidaan käyttää mainiosti useammassakin ohjelmointiparadigmassa. Seuraavassa olen luetellut tyypillisiä ohjelmointikieliparadigmojen tunnusmerkkejä ja tyyppiesimerkkejä; kannattaa kuitenkin muistaa, että minkään paradigman tyyppiesimerkit eivät rajoitu pelkästään sen paradigman sisälle. Imperatiivisen kielen (käskykielen) merkittävimmät tunnusmerkit ovat sijoituslause (tai -lauseke) ja peräkkäistäminen. Useimmissa kielissä peräkkäistäminen on implisiittistä: peräkkäin kirjoitetut lauseet suoritetaan peräkkäin. Niissäkin kielissä yleensä puolipiste on peräkkäistyksen merkkinä. Lähes kaikki kielet ovat imperatiivisia. Tyyppiesimerkkejä ovat Algol-suvun kielet (Pascal, C, C++, Java). Deklaratiivisen kielen (kuvailukielen) tunnusmerkki on se, että siitä puuttuu sijoituslause ja peräkkäistäminen. Deklaratiivisen kielen pääetu on, että siinä pätee viittausläpinäkyvyys (referential transparency), mikä helpottaa huomattavasti ohjelmien formaalia pyörittelyä käsin tai koneellisesti. Aidosti deklaratiiviset kielet ovat harvinaisia, ja niihin pätee Alan Perlisin [14] lausahdus: Purely applicative languages are poorly applicable. (Perlis käytti deklaratiivisista kielistä nimeä applikatiivinen kieli.) Esimerkkejä lienevät λ- laskenta ja John Backusin FP-kieli [1]. Viime vuosikymmenellä kehitettiin muutamia tekniikoita, joilla kielestä saatettiin tehdä eräässä mielessä sekä deklaratiivinen että imperatiivinen. Näitä tekniikoita käyttäviä kieliä ovat Clean ja Haskell. Proseduraaliset kielet (toimintokielen) tukevat aliohjelmarakennetta. Tyyppiesimerkkejä ovat C ja Pascal. Myös C++ ja Java kuuluvat tähän luokkaan, vaikka ne ovatkin ensisijaisesti oliokieliä. Oliokielet tukevat olioita, joilla on identiteetti ja toiminta (metodit) sekä toimintojen periminen oliolta toiselle (tai luokkapohjaisissa kielissä luokalta toiselle). Tyyppiesimerkkejä ovat Smalltalk, Python, Java ja C++. Funktiokielten (funktionaalisten kielten) tärkein tunnusmerkki on tuki täysivaltaisille funktioille (funktioille, joita voi palauttaa funktiosta, antaa funktiolle parametriksi ja tallettaa tietorakenteisiin). Muita tyypillisiä mutta ei universaaleja ominaisuuksia ovat tuki halvalle tiedon kopioinnille, algebralliset tietotyypit ja niiden hahmonsovitus ja tehokas rekursio. Tyyppiesimerkkejä ovat Lisp, SML, Haskell ja Clean. Myös useimmat oliokielet voidaan sijoittaa tähän kategoriaan, sillä olioilla ja täysivaltaisilla funktioilla on paljon yhteistä ainakin pellin alla. 3
1.3 Ousterhoutin jaottelu John Ousterhout [13] määrittelee järjestelmäohjelmointikielen ja juontokielen eron seuraavasti: järjestelmäohjelmointikieli Nämä kielet on tarkoitettu tietorakenteiden ja yksittäisten komponenttien toteuttamiseen siirrettävästi ja korkeammalla tasolla kuin symbolisella konekielellä kirjoitettaessa. Ne käyttävät vahvaa staattista tyypitystä. Järjestelmäohjelmointikieliä ovat esimerkiksi PL/I, Pascal, C, C++ ja Java. juontokieli Juontokielet olettavat, että on jo olemassa joukko hyödyllisiä komponentteja, jotka on kirjoitettu järjestelmäohjelmointikielillä. Juontokieliä käytetään komponenttien yhdistelemiseen, eräänlaisina liimoina (usein puhutaankin englanniksi glue languageistä). Juontokielet käyttävät tyypillisesti dynaamista tyypitystä - tyypityksen vahvuus vaihtelee. (Ousterhout puhui tyypittömyydestä mutta hän tarkoitti dynaamista tyypitystä.) Juontokieliä ovat esimerkiksi Perl, Python, Rexx, Tcl, Visual Basic ja Unixin kuoret. Ousterhoutin jaottelua on kovasti kritisoitu. 2 Historia 2.1 Kaksi ensimmäistä sukupolvea: ennen vuotta 1955 Ensimmäiset ohjelmointikielet olivat konekieliä ja niillä kirjoitetut ohjelmat olivat bittijonoja tietokoneen muistissa. Ohjelmat syötettiin koneelle kytkimiä kääntelemällä. Konekieli on ratkaisuavaruuden sydämessä: konekielellä ei ole kääntäjää tai tulkkia, joka auttaisi ihmistä ongelman tuomisessa ratkaisuavaruuteen. Ohjelmien kirjoittaminen konekielellä on erittäin virhealtista ja toisaalta hyvin mekaanista puuhaa. Jo hyvin varhaisessa vaiheessa alettiin kirjoittaa ohjelmia, jotka auttoivat tässä prosessissa. Ihmiset kirjoittivat sopivalla tavalla muotoiltua tekstiä, ja nämä ohjelmat muunsivat sen konekieleksi. Tyypillisesti tässä syötteessä yksi rivi vastaa yhtä konekielen käskyä. Tätä ohjelmointikieltä kutsutaan symboliseksi konekieleksi (engl. assembler language), ja ohjelmaa, joka kääntää tällä kirjoitetun ohjelman konekielellä, sanotaan assembleriksi. Konekielet muodostavat ohjelmointikielten ensimmäisen sukupolven ja symboliset konekielet ohjelmointikielten toisen sukupolven. Ongelmana näissä on ohjelmoinnin virhealttius ja vahva mekaanisuus sekä se, että jokaisella konetyypillä on oma konekielensä ja sitä vastaava symbolinen konekieli. Ohjelmia ei voitu siirtää konetyyppien välillä, vaan ne jouduttiin kirjoittamaan uudestaan aina kullekin uudelle konetyypille. 2.2 Automaattinen ohjelmointi ja ohjelmointikielten synty: 1955 1960 Ongelman ratkaisuksi esitettiin automaattista ohjelmointia, joksi sitä silloin 1950-luvun alkupuolella sanottiin. Kyse oli siitä, että ohjelmoija esittää tietokoneelle ongelman kuvauksen jollakin formaalilla kielellä, ja jokin erityinen ohjelma tuottaa siitä konekielisen ohjelman, joka ratkaisee ongelman. Eivät ne formaalit kielet mitenkään kummoisia olleet, mutta senaikaiseen tietämykseen nähden ne olivat tekoälytutkimuksen (josta ei tuota nimeä vielä käytetty) huippua. Nykyään näitä formaaleja kieliä sanotaan korkean tason tai kolmannen sukupolven ohjelmointikieliksi ja ohjelmia, jotka tekevät niillä kirjoitetuista ongelmakuvauksista konekielisiä ohjelmia, sanotaan kääntäjiksi (engl. compiler). Näitä varhaisia kieliä on esitelty Knuthin ja Trabb Pardon artikkelissa vuodelta 1977 [6]. Automaattiseen ohjelmointiin eivät useimmat ihmiset uskoneet, ennen kuin Fortran tuli kuvioihin 1950-luvun lopulla ensimmäinen versio julkaistiin keväällä 1957, mutta kielestä oli siinä vaiheessa puhuttu jo monta vuotta. Fortran oli ensimmäinen merkittävää kannatusta saavuttanut korkean tason ohjelmointikieli (ja kiinnostavaa kyllä, edelleen laajassa käytössä, tosin reilusti muuttuneena). Ensimmäinen Fortran-kääntäjä oli vaikea tehtävä: sitä tehtiin kolme vuotta ja siihen kului 18 ihmistyövuotta. Tätä työtä johti John Backus, joka on myöhemminkin ollut merkittävä tekijä ohjelmointikieliteknologian alalla. Fortanin merkitys ohjelmointikielten kehitykselle on kiistaton. Se oli alan pioneerityö: se sai ihmiset uskomaan korkean tason ohjelmointikieliin ylipäätään. Lisäksi monet sen ominaisuudet 4
ovat edelleen ohjelmointikielissä tärkeitä: taulukkomuuttujat, muuttujan ohjaamat silmukat (ns. for-silmukat) sekä haarautuva if-lause ovat nykykielissäkin peruskamaa. Fortran oli tarkoitettu tieteellisen laskennan ohjelmien kirjoittamiseen. John McCarthy, tekoälytutkimuksen kantaisä, halusi kielen symbolista tiedonkäsittelyä (esimerkiksi algebrallisten funktioiden symbolista derivointia) varten. McCarthy ja muutamat muut laajensivat Fortrania aluksi symbolista laskentaa varten, mutta tämän rajat tulivat pian vastaan. He aloittivat uuden kielen, Lispin, kehittämisen ensin paperilla ja käsikääntäen sillä kirjoitettuja ohjelmia konekielelle. Kun sitten huomattiin, että teoreettiseksi leluksi tarkoitettu Lisp-funktio eval toimii mainiosti Lisp-tulkkina, oli Lisp-toteutus valmis [9]. Fortranin tapaan Lispin merkitys myöhemmille ohjelmointikielille on merkittävä. Lispin mukana näkivät päivänvalon mm. rekursiiviset aliohjelmat, muistinsiivous (engl. garbage collection) ja iflauseke. Nykyisin Lispistä on käytössä kolme merkittävää murretta: Emacs Lisp, Common Lisp ja Scheme. Algoritmien julkaisemiseen kehitettiin komiteatyönä 1950-luvun lopulla uusi kieli, Algol (tämän sanotaan usein olevan lyhenne sanoista Algorithmic language, mutta se viittaa myös samannimiseen kolmoistähteen). Algol 60 [12] oli kolmas 1950-luvun loppupuolella kehitetty kieli, jolla on ollut merkittävää merkitystä ohjelmointikielten tulevalle kehitykselle. Algol 60 oli ensimmäinen lohkorakenteinen kieli. Sillä oli vapaamuotoinen, rakenteinen syntaksi. Siinä oli muuttujien tyyppien esittelyt ja arvovälitteiset (engl. call-by-value) aliohjelmaparametrit. Nämäkin kaikki ovat lähes välttämättömiä nykykielissä. Tony Hoare kirjoitti Algol 60:stä vuonna 1973 seuraavasti: [4] Tässä on kieli, joka on niin edellä aikaansa, että se oli paitsi edeltäjiään myös lähes kaikkia seuraajiaan parempi. 1950-luvun lopulla kehitettiin myös kaksi muuta suosiota saavuttanutta kieltä, Cobol ja APL. Kummankin merkitys ohjelmointikielten kehitykselle kokonaisuutena on kuitenkin vähäinen, joten niihin ei tämän enempää puututa. 2.3 Baabelin torni: 1960-luku 1960-luvulla suunniteltiin satoja uusia ohjelmointikieliä, joista hyvin harva jäi eloon. Nimet Adam, AED, Altran, CLP, Dyana ja Unicode eivät nykyään liity ohjelmointikieliin lainkaan mutta ne kaikki olivat vuonna 1967 käytössä olleita ohjelmointikieliä. Monet näistä kielistä oli tarkoitettu johonkin erityiseen tarkoitukseen. Algol-perhe sai uuden jäsenen, Algol 68:n, joka kokonsa ja vaikealukuisen määrittelynsä [19] vuoksi jäi pääosin käyttämättä. Samoihin aikoihin Niklaus Wirth ja Tony Hoare suunnittelivat oman seuraajansa Algol 60:lle, jota on kutsuttu Algol W:ksi. Norjalaiset Kristen Nygaard ja Ole-Johan Dahl kehittivät Simula-kielen simulointien kirjoittamiseen. Simulan merkitys nykykielille on ollut valtava, samaa suuruusluokkaa kuin Algol-perheen ja Lispin: Simulasta alkoi olio-ohjelmoinnin perinne jo Simula I:n yhteydessä puhuttiin olioista ja luokista. Perintä lisättiin Simula 67:ään. David Farber, Ralph Griswold ja Ivan Polonsky kehittivät Snobol-kielen merkkijonojen käsittelyä varten 1962. Snobol vaikutti suuresti tulevien kielten (esimerkiksi Perlin) merkkijonoominaisuuksiin. 1960-luvun lapsia on myös BASIC. Dartmouth Collegessa haluttiin luoda tietokonejärjestelmä ja sille ohjelmointikieli, jotka ovat niin helppoja, että niiden käyttöä voitaisiin opettaa kaikille humanisteille. BASIC sai monia vaikutteita Algol 60:stä, mutta jätti monta (näin jälkiviisaasti sanoen: hyvin tärkeätä) sen ominaisuutta pois. Esimerkiksi lohkorakenne tiputettiin pois, koska sitä ei haluttu selittää humanisteille [7]. IBM:n ja muun teollisuuden yhteistyönä kehitettiin 60-luvulla PL/I (oikeastaan PL/1, mutta ykkönen on tapana tässä tapauksessa kirjoittaa isona I-kirjaimena), joka oli yritys luoda eräänlainen universaali ohjelmointikieli. Aiemmin oli ohjelmointitehtävät ja ohjelmoijat jakautuneet kahteen selvään ryhmään: tieteellinen laskenta, jossa tarvittiin liukulukulaskentaa, taulukoita ja aliohjelmia, sekä hallinnollinen tietojenkäsittely, jossa tarvittiin kiintolukulaskentaa (engl. fixed-point arithmetic), tehokasta siirräntää (engl. input/output) ja merkkijonojen käsittelyä. 1960-luvulla tämä jako alkoi hämärtyä, ja eräs PL/I:n oleellisimmista tavoitteista oli toimia molempien ryhmien ohjel- 5
mointikielenä. Kieli oli 60- ja 70-luvuilla varsin suosittu ja IBM tukee sitä edelleen, mutta nykyisin sitä pidetään (yhdessä Algol 68:n kanssa) lähinnä varoittavana esimerkkinä laajasta ja vaikeasta kielestä. PL/I oli ensimmäinen ohjelmointikieli, joilla on muodollisesti (PL/I:n tapauksessa VDM:llä) määritelty merkitysoppi (engl. semantics). 2.4 Modernismi: 1970-luku 1970-luvulla alettiin rakentaa edellisen vuosikymmenen Baabelin tornin raunioiden päälle. Tärkeinä lähtökohtina pidettiin kielen yksinkertaisuutta ja johdonmukaisuutta. Samaan aikaan puhuttiin paljon rakenteisesta ohjelmoinnista [3] (engl. structured programming), ja monet 70-luvulla suunnitelluista kielistä oli nimenomaisesti suunniteltu rakenteista ohjelmointia ajatellen. 1970-luvun kielet ovat ensimmäisiä, joita voi kutsua moderneiksi. Edellisinä kahtena vuosikymmenenä oli kehitetty niin paljon kieliä, että niiden virheistä saattoi oppia jotain merkittävää. 1970-luvun jälkeen on myöskin ilmaantunut hyvin vähän radikaaleja, uusia ideoita ohjelmointikielten alalla kielet ovat kyllä kehittyneet mutta vähittäin. Niklaus Wirth ei todellakaan pitänyt Algol 68:sta. Hän alkoi suunnitella uutta kieltä Algol-perinteen sisällä aiemman Algol W:nsä pohjalta. Tulos oli Pascal, toisaalta opetukseen mutta toisaalta myös käytännön ohjelmointityöhön tarkoitettu kieli. Pascalin eräs varhainen, aikoinaan suosittu toteutus perustui samantyyppiselle virtuaalikoneratkaisulle kuin Java nykyisin. Toinen Algol-perinteeseen kytkeytynyt uusi kieli oli Dennis Ritchien C. C oli tarkoitettu raakaan järjestelmäohjelmointiin: sillä oli tarkoitus kirjoittaa (ja kirjoitettiinkin) UNIX kokonaan uudestaan. C:n merkitystä myöhemmille kielille on vaikea yliarvioida, vaikka se sinänsä ei sisältänytkään juuri mitään kieliteknologista uutta sen merkittävyys johtuu siitä, että se on erittäin suosittu. Olio-ohjelmointi ei vielä 1970-luvulla ollut kovin merkittävässä asemassa. Xeroxin PARClaboratorioissa kehitettiin WIMP-käyttöliittymää, ja merkittävä osa tätä kehitystyötä oli Smalltalkkielen kehitys. Smalltalk on edelleen yksi puhtaimmista oliokielistä. Itse kieli on hyvin pieni, mutta sen peruskirjasto on järkyttävän suuri. Funktio-ohjelmointi oli 1970-luvulla rajoittunut lähinnä Lisp-sukuisten kielten käyttöön. Robin Milner kehitti Edinburghin yliopiston päättelyavustimen käyttöön metakielen (kielen, jolla kuvataan kieliä), jota hän kutsui ML:ksi (lyhenne sanasta meta-language). Sen tapa ajatella ohjelmia sai paljon vaikutteita Lispistä mutta sen syntaksi on Algol-tyyppinen. ML:n ehkä suurin merkitys ohjelmointikielten kehitykselle on Hindley Milnertyyppijärjestelmä [11], jota mm. Generic Java käyttää tietyin muunnoksin. Samoihin aikoihin Guy Lewis Steele, Jr. ja Gerald Jay Sussman kehittivät uutta Lisp-varianttia, jonka he nimesivät Schemeksi. Scheme käytti Lispin omintakeista Cambridgen-puolalaista [9] syntaksia mutta otti käyttöön Algoleista tutun lohkorakenteisuuden. Schemen kehittäjät olivat vahvasti sitä mieltä, että aliohjelmakutsun ei todellakaan tarvitse olla hidas operaatio [18], ja he suunnittelivat kielensä (ja sen toteutukset) tuon käsityksen ympärille. Schemen määrittelyyn kuuluu edelleen vaatimus häntäkutsun poistamisesta, mitä juuri mikään muu kieli ei nykyisin vaadi, vaikka syytä olisi. 1970-luvun alussa syntyi ohjelmointikieli nimeltään Prolog, joka oli täysin erilainen kuin aiemmat kielet, eikä nykyisistäkään kielistä kovin monta samantyyppistä löydy. Prologin perusidea on predikaattilogiikan kaavojen tulkitseminen proseduraalisesti siten, että ohjelma koostuu kaavoista, jotka tulkitaan aliohjelmiksi, ja jonkin näistä aliohjelmista kutsuminen vastaa sitä vastaavan kaavan todistamista muiden toimiessa aksioomina. 2.5 Postmodernismi: 1980-luku 1980-lukua voi hyvällä syyllä sanoa olioohjelmoinnin popularisoinnin vuosikymmeneksi. Tällä vuosikymmenellä kehitettiin monta merkittävää olio-ohjelmointiin käytettyä kieltä. Samalla myös funktio-ohjelmoinnin tutkimus eteni, ja vaikka funktio-ohjelmointi ei saavuttanutkaan samaa suosiota kuin olio-ohjelmointi, myös sen kannalta merkittäviä kieliä kehitettiin. 1980-luvun alussa alkoi Bjarne Stroustrup kehitellä oliolaajennusta Ritchien C-kieleen. Hän kutsui tuota kieltä nimellä C with classes, mutta varsin pian nimi vaihtui C++:ksi. Vaikka C++ alkoikin oliokielenä, on siihen myöhemmin lisätty pal- 6
jon muitakin ohjelmointityylejä tukevia ominaisuuksia esimerkiksi C++:n tyyppijärjestelmästä kehittyi oma funktiopohjainen ohjelmointikielensä C++:n sisälle! Toinen tunnettu 1980-luvulla kehitetty oliokieli on Bertrand Meyerin Eiffel. Se on puhdas oliokieli ja sisältää myös tukea ohjelmien formaalille verifioinnille (esi- ja jälkiehdot sekä luokkainvariantit on mahdollista esitellä ja tarkastaa ajoaikana samoja ominaisuuksia löytyi muutamasta aiemmasta kielestä kuten Alphard ja Euclid). 1980-luvulla saatiin valmiiksi Yhdysvaltain puolustusministeriön toimeksiannosta kehitetty kieli Ada (Augusta Ada Byronin, Lovelacen kreivittären mukaan, jonka on sanottu olleen maailman ensimmäinen ohjelmoija). Adan tärkeimpiä sovelluskohteita olivat alunperin sulautetut järjestelmät ja tosiaikaohjelmistot, erityisesti asejärjestelmät. David Turner kehitti funktiokielen Miranda 1980-luvun puolessa välissä. Se muistutti monessa suhteessa ML:ää, mutta se käytti johdonmukaisesti laiskaa laskentaa (tähän palataan vielä) ja oli puhdas funktiokieli. Mirandan käyttöä rajoitti Turnerin tapa pitää kielestään voimakkaasti kiinni: hän jopa patentoi osia siitä. Samantyyppisiä omia kieliä oli lähes jokaisella funktio-ohjelmoinnista kiinnostuneella tutkijalla, joten vähän myöhemmin perustettiin komitea suunnittelemaan uutta Mirandan kaltaista ohjelmointikieltä. 1980-luvun loppupuolella syntyi kaksi merkittävää juontokieltä (engl. scripting languages): John Ousterhoutin Tcl ja Larry Wallin Perl. Tcl:n pääideana oli olla kieli, jonka voisi upottaa ohjelmistoihin, Perl syntyi tarpeesta käsitellä tekstitiedostoja mitä erilaisimmin tavoin. Larry Wall [20] kuvaa Perliä maailman ensimmäiseksi postmodernistiseksi ohjelmointikieleksi. Eräässä mielessä koko 1980-lukua voisi kuvailla postmodernismin vuosikymmeneksi. 2.6 Internetin nousu: 1990-luku Vuonna 1993 alkoi Internetin ikuinen syyskuu, kun Internet alkoi vähitellen nousta tavallisten ihmisten tietoisuuteen. Samalla ohjelmoinnin fokus siirtyi kohti verkkosovelluksia. James Gosling työryhmineen kehitti Java-kielen, vuonna 1995 julkaistun C-sukuisen oliokielen, joka on niin konservatiivisesti suunniteltu, ettei siinä ole juuri mitään uutta. Javan, kuten muidenkin C-sukuisten kielten, merkitys on sen suosikkiasemassa. Aivan 1990-luvun alussa sai edellisellä vuosikymmenellä muodostettu funktiokielikomitea työnsä päätökseen. Tulos oli Haskell, jonka tarkoitus on olla se oikea laiskasti laskeva, puhdas funktiokieli. Tämä tavoite onkin onnistunut. Vuonna 1999 valmistui kielen nykyinen määrittely, Haskell 98. Haskell näyttäisi ratkaisseen funktiokieliä vaivanneen ongelman siirrännästä varsin omintakeisella monadisen tietotyypin käsitteellään. Jo 1980-luvulla alkanut juontokielten kehitys jatkui 1990-luvulla. Guido van Rossum kehitti Pythonin vuosikymmenen alussa, Yukihiro Matsumoto kehitti Rubyn. 90-luvun lapsia ovat myös PHP ja ECMAScript (tunnetaan myös nimellä Javascript). 2.7 Sukupolvista Olen edellä viitannut ohjelmointikielten ensimmäiseen, toiseen ja kolmanteen sukupolveen. Kukaan ei näytä tietävän, mistä koko sukupolvijaottelu on peräisin, ja lähteet määrittelevät sukupolvet kukin omalla tavallaan. Edellä esittelemäni määritelmät kolmelle ensimmäiselle sukupolvelle ovat jonkinlainen lähteiden konsensus, vaikka poikkeaviakin esityksiä olen nähnyt. Useimmat lähteet puhuvat myös neljännestä ja viidennestä sukupolvesta, mutta mitään yksimielisyyttä niiden sisällöstä ei ole. 3 Kielen valinta Tyypilliset tavat valita toteutuskieli ovat me ollaan aina käytetty tätä kieltä tuo kieli on kaikkien huulilla; käyttäkäämme sitä Kumpaakin tapaa voidaan kenties perustella liiketoiminnallisilla syillä, mutta teknisesti molemmat tavat ovat huonoja. Varsinkin ison projektin suunnittelijoiden kannattaa pohtia kielen valintaa tosissaan, sillä sillä voi olla iso vaikutus projektin tuottavuuteen. Projektiorganisaatiolle uuden ja oudon 7
kielen valintaa pidetään merkittävänä riskitekijänä projektin onnistumiselle 2. Kannattaa huomata, että kielen valintaa ei voi tarkastella irrallaan kielen tukityökalujen (kääntäjä, tulkki, debuggeri yms.) tarkastelusta, mutta niitä ei silti pidä sekoittaa toisiinsa. Useimmissa kielisodissa, mitä esimerkiksi netissä käydään, kielet ja niiden tukityökalut sekoitetaan toisiinsa niin autuaasti, että vertailun mielekkyys katoaa kokonaan. Lawlis [8] on esittänyt prosessin, jolla toteutuskielen voi valita (näennäisen?) täsmällisesti vaatimusten mukaan. Burgess [2] on esittänyt kriteereitä, jota hänen mukaansa on syytä tarkastella valittaessa toteutuskieltä korkealaatuisen tuotteen tekemistä varten. Raymond [17] on käsitellyt kielen valintaa Unix-ympäristöön ohjelmaa rakentavan vapaaohjelmaprojektin näkökulmasta, mutta hänen havaintonsa ovat yleisemminkin sovellettavissa. Seuraavassa olen koonnut Lawlisin, Burgessin ja Raymondin pohjalta muutaman kysymyksen, joita voidaan käyttää kielten arvioinnissa (ei missään erityisessä järjestyksessä): 1. Tukeeko kieli hyvää ohjelmointityyliä ja tekeekö se huonon tyylin hankalaksi? 2. Onko kielestä olemassa standardi? Miten tarkasti ja selkeästi se määrittelee kielen? Tukevatko kielen toteutukset standardia ja tekevätkö ne epästandardin koodin kirjoittamisen hankalaksi? 3. Onko kielestä olemassa laadukkaita toteutuksia? 4. Onko kieli riippumaton yksittäisistä käyttöjärjestelmistä ja laitteistoista? Toisin sanoen, onko se siirrettävä myös käytännössä? 5. Tukeeko kieli sovellusalueita, joilla projekti tai organisaatio toimii? 6. Auttaako vai haittaako se turvallisuuden (safety) ja luotettavuuden tavoittelua? 7. Ovatko sen toteutukset ajanmukaisia? 8. Onko sille olemassa tarvittavat prosessia tukevat työkalut? 2 Eikö sitten olisikin parempi, että kaikki opettelisivat useita kieliä jo koulussa tai yliopistolla? 4 Ohjelmointikielten suunnitteluperiaatteita Tony Hoare kirjoitti 1970-luvun alussa poleemisen artikkelin [4], jossa hän antoi mielestään objektiiviset kriteerit ohjelmointikielen hyvyydelle. Ne ovat seuraavat. Kielen tulisi auttaa ohjelman suunnittelussa. Kielen tulisi auttaa ohjelman dokumentoinnissa. Kielen tulisi auttaa virheiden jäljittämisessä. Näiden pohjalta Hoare nimeää viisi avainlausahdusta, jotka kuvaavat hyvää ohjelmointikieltä: yksinkertaisuus Yksinkertaisuus on välttämätöntä, sillä muuten edes kielen kehittäjä ei voi arvioida tekemiensä valintojen seurauksia eikä kielen toteuttaja voi saavuttaa edes luotettavuutta saati sitten tehokkuutta ja nopeutta. Yksinkertaisuudesta hyötyy kuitenkin ensisijaisesti kielen käyttäjä. turvallisuus Turvallisuudella Hoare tarkoittaa sitä, että kielellä kirjoitettu ohjelma ei saa missään tilanteessa edes ohjelmoijan virheen seurauksena käyttäytyä hallitsemattomasti (esimerkiksi sekoittaa käyttöjärjestelmän tai muistinhallinnan tietorakenteita) tai eri järjestelmissä eri tavoilla. Lisäksi kielen syntaksin tulee olla sellainen, ettei yksinkertainen kirjoitusvirhe muuta ohjelman merkitystä merkittävästi. Nykyisinä Internet-ohjelmoinnin aikoina turvallisuus on entistä merkittävämpi, sillä juuri ohjelmointikielten turvallisuuden puute on ollut merkittävä tietomurtoja edistävä tekijä. lyhyt kääntymisaika Käännösajan (se aika, joka kestää koodin muuttamisen päättymisestä siihen, kun muutettua ohjelmaa voi ajaa) tulee olla mahdollisimman lyhyt, jotta virheenjäljitys ja -korjaus onnistuisi mahdollisimman hyvin. Tämä vaikuttaa myös kielensuunnittelijan työhön, sillä eri ominaisuudet vaativat eri määrän työtä käännösaikana. ajonaikainen tehokkuus Ohjelmointikieli on työkalu ja siksi sen tulee olla tehokas. Tässä 8
pätee sama kuin edellisessäkin kohdassa: kielensuunnittelija voi vaikuttaa paljon siihen, että onko hänen kielensä toteutettavissa tehokkaasti. Tämä ja edellinen vaatimus ovat osittain keskenään ristiriitaisia, sillä pääsääntöisesti ajonaikainen tehokkuus paranee, kun käännösaikaa kasvatetaan lisäämällä kääntäjään optimointeja. Kuitenkin kielen suunnittelija voi vaikuttaa paljon siihen, että kuinka paljon näitä optimointeja välttämättä tarvitaan. luettavuus Ohjelman luettavuuden tärkeyden pitäisi olla kaikille selvä. Hoare antaa myös artikkelissaan monia ohjeita siitä, miten hänen mielestään näihin tavoitteisiin voitaisiin päästä, mutta monet niistä ovat jo varsin kyseenalaisia. Silti kielensuunnittelusta kiinnostuneen kannattaa tutustua Hoaren artikkeliin kokonaisuudessaan. Hoaren listaan voimme lisätä vielä yhden avainlausahduksen: stabiilius. Tuotantokäytössä olevan kielen tulee olla saavuttanut tietynasteinen kypsyys. Kielen käyttäjien tulee voida luottaa siihen, että juuri se kielen versio on validi vielä kahden, viiden ja aika usein vielä kymmenenkin vuoden kuluttua sekä ei pelkästään Windowsissa vaan myös Unixissa, Macissa ja vaikkapa GNU:ssa. Tässä auttaa voimakkaasti kielen standardointi, sillä standardi antaa stabiiliutta ja antaa yhteisen pohjan kielen eri toteutuksille. Tehtävä 1 Ovatko nämä kriteerit tarkoituksenmukaisia? Ovatko ne kattavia? Mitä sinä pidät tärkeänä ohjelmointikielessä? Tehtävä 2 Löydätkö vastaavuuksia ohjelmointikielen ja ohjelman hyvyyden arviointiin käytettävien kriteereiden väliltä? 5 Kielen määrittely Kielestä on aina syytä kirjoittaa määrittelydokumentti (specification) samaan aikaan kun sitä ensimmäistä kertaa toteutetaan tai heti sen jälkeen. Määrittelydokumentti kertoo ohjelmoijalle, mitä kieli tarjoaa, ja toteuttajalle, mitä hänen ohjelmansa tulee tehdä. Tämän dokumentin lisäksi on toki hyvä olla ohjelmoijan opas erikseen, koska määrittelydokumentti on ollessaan hyödyllinen tavallisesti suhteellisen vaikealukuinen. Määrittelydokumentissa kuvataan kaikki oleellinen kielestä riittävällä tarkkuudella. Tässä on valittavissa monta tarkkuustasoa: voidaan esittää asia epämuodollisesti tai sitten hieman muodollisemmin. Kielioppi esitetään nykyisin lähes aina täysin muodollisesti käyttäen tiettyjä formalismeja (tavallisimmin Backus-Naur Form (BNF) tai sen muunnokset EBNF ja ABNF). Merkitysoppi on tapana ilmaista sanallisesti, koska olemassaolevat merkitysopin muodolliset kuvausmenetelmät (aksiomaattinen, denotationaalinen ja operationaalinen) ovat varsin vaikeita kirjoittaa ja lukea. Myös mahdollinen tyyppijärjestelmä voidaan kuvata muodollisesti esittelemällä se päättelysääntöjoukkona. Palaamme näihin kaikkiin formalismeihin myöhemmin. Uuden ohjelmointikielen määrittelydokumenttia levitetään tavallisimmin kielen alkuperäisen toteutuksen mukana. Toisinaan erityisesti yliopistoissa ja tutkimuslaitoksissa luotujen kielten määrittelydokumentti julkaistaan laitosraporttina tai jopa kansainvälisenä tutkimusartikkelina, ja monien määrittelyjen nimi onkin muotoa Report on the X Language Y, missä X on jokin kieltä Y kuvaava lausahdus. Tavallista on myös julkaista kielten paranneltuja versioita, jolloin saattaa tulla kyseeseen julkaista myös Revised Report on the X Language Y. Scheme on tästä mainio esimerkki: Schemeraportista on nyt voimassa viides revisio [5]. Kun kieli on saavuttanut tietyn kypsyystason ja käyttäjäjoukon, aletaan tavallisesti puuhata sen standardointia. Standardointia harrastavat monet järjestöt, tärkeimpinä ISO (kansainvälinen standardointijärjestö, http://www.iso.ch/), usein yhteistyössä IEC:n (kansainvälinen sähköteknillinen standardointijärjestö, http://www.iec.ch/) kanssa, ECMA (informaatio- ja kommunikaatojärjestelmiä standardoiva eurooppalainen järjestö, http: //www.ecma.ch/) sekä IEEE (Institute of Electrical and Electronics Engineers, http://www.ieee. org/). Takavuosina merkittävässä asemassa olivat myös Yhdysvaltain kansallisen standardointijärjestön ANSI:n standardit, mutta nykyisin lähes kaikki ohjelmointikieliin liittyvät ANSI-standardit ovat myös kansainvälisiä ISO/IEC-standardeja. Usein tosin kielten standardointi tehdään näiden virallisten tahojen ulkopuolella (esimerkik- 9
si Haskell-kielen standardointi tehtiin käyttäjä- ja toteuttajayhteisön sisällä, ja kielen määrittely julkaistiin kirjana [15] ja lehtiartikkelina [16]). Seuraavassa on listattu joidenkin tunnettujen kielten standardit: C-kielestä on kolme standardia: ANSI X3.159-1989, ISO/IEC 9899:1990 ja ISO/IEC 9899:1999. Kaksi ensimmäistä ovat oleellisesti sama standardi ja käsittelevät sitä C:n versiota, jota on tapana kutsua ANSI C:ksi, C89:ksi tai C90:ksi. Kolmas standardi käsittelee C:n uutta versiota C99:ää. C++-kielen standardi on ISO/IEC 14882:1998. Scheme-kielen standardi on IEEE Std 1178-1990. C#-kielen standardi on ECMA-334. BASIC-kielestä on neljä standardia: ANSI X3.60-1978, ECMA-116, ANSI X3.113-1987 (R1998) ja ISO/IEC 10279:1991 (E). Ne kaikki käsittelevät hieman eri versiota kielestä. Java-kielestä ei ole standardia. Viitteet [1] John Backus. Can programming be liberated from the von Neumann style? A functional style and its algebra of programs. Communications of the ACM, 21(8), elokuu 1978. [2] C. J. Burgess. Software quality issues when choosing a programming language. Kirjassa Software Quality Management III Vol. 2, ss. 25 31. Computational Mechanics Publications, 1995. [3] O.-J. Dahl, E. W. Dijkstra, ja C. A. R. Hoare. Structured Programming. Academic Press, Lontoo, 1972. [4] C. A. R. Hoare. Hints on programming language design. Kirjassa C. A. R. Hoare ja C. B. Jones, toim., Essays in Computer Science. Prentice-Hall, New York, 1989. [5] Richard Kelsey, William Klinger, Jonathan Rees, et al. Revised 5 report on the algorithmic language Scheme. ACM SIGPLAN Notices, 33(9):26 76, syyskuu 1998. [6] Donald E. Knuth ja Luis Trabb Pardo. The early development of programming languages. Kirjassa J. Belzer, A. G. Holzman, ja A. Kent, toim., Encyclopedia of Computer Science and Technology, osa 6. Dekker, New York, 1977. Julkaistu myös kirjassa [10]. [7] Thomas E. Kurtz. BASIC. ACM SIGPLAN Notices, 13(8):103 118, elokuu 1978. Preprints, First ACM SIGPLAN History of Programming Languages Conference. [8] Patricia K. Lawlis. Guidelines for choosing a computer language: Support for the visionary organization, elokuu 1997. [9] John McCarthy. History of LISP. ACM SIGPLAN Notices, 13(8):217 223, elokuu 1978. Preprints, First ACM SIGPLAN History of Programming Languages Conference. [10] N. Metropolis, J. Howlett, ja Gian-Carlo Rota, toim. A History of Computing in the Twentieth Century. Academic Press, New York, 1980. [11] Robin Milner. A theory of type polymorphism in programming. Journal of Computer and System Sciences, 17(3):348 375, 1978. [12] Peter Naur et al. Revised report on the algorithmic language ALGOL 60. Communications of the ACM, 6(1):1 17, tammikuu 1963. [13] John K. Ousterhout. Scripting: Higher-level programming for the 21st century. Computer, 31(3):23 30, maaliskuu 1998. [14] Alan J. Perlis. Epigrams on programming. ACM SIGPLAN Notices, 17(9), syyskuu 1982. [15] Simon Peyton Jones, toim. Haskell 98 Language and Libraries: The Revised Report. Cambridge University Press, huhtikuu 2003. [16] Simon Peyton Jones. Haskell 98 language and libraries: The revised report. Journal of Functional Programming, 13(1), tammikuu 2003. [17] Eric S. Raymond. The Art of Unix Programming. Addison-Wesley, Boston, 2003. [18] Guy Lewis Steele, Jr. Debunking the expensive procedure call myth or procedure call implementations considered harmful or, 10
LAMBDA: The ultimate GOTO. Kirjassa James K. Ketchel et al., toim., Proceedings of the 1977 annual conference, ss. 153 162. ACM Press, 1977. [19] A. van Wijngaarden et al., toim. Revised Report on the Algorithmic Language Algol 68. Springer, Berliini, 1976. [20] Larry Wall. Perl, the first postmodern computer language. http://www.wall.org/~larry/pm. html, 1999. A talk given in Linux World Spring 1999. 11