3.5. TYYPIT 43 teydestä. On myös mahdollista ilmaista parametrin alue tyyliin λx : S.E, joka tarkoittaa, että kyseinen funktio kuuluu alueeseen S T jollakin T (T on yleensä johdettavissa E:n rakenteesta). Helposti nähdään, että S T on vakiofunktio λx : S. T. Funktiot jaetaan kahteen luokkaan sen mukaan, miten ne käyttäytyvät silloin, kun argumenttina on. Monet funktiot ovat tiukkoja (strict), mutta jotkut ovat väljiä (nonstrict): funktio f S T on tarkka, mikäli pätee f( S ) = T, muuten se on väljä. Tässä esitetyllä tavalla käsiteltynä kaikki funktiot ovat yksipaikkaisia. Monipaikkaisia funktioita voidaan simuloida kahdella tavalla: funktio voi kuulua alueeseen (S 1 S 2 ) T, jolloin se on yksipaikkainen funktio, joka ottaa parin parametrinaan; toisaalta funktio voi kuulua alueeseen S 1 (S 2 T), jolloin se on yksipaikkainen funktio, joka palauttaa toisen yksipaikkaisen funktion. Jälkimmäistä tapaa sanotaan currymaiseksi (curried) loogikko Haskell B. Curryn (1900 1982) mukaan, edellistä voisi kutsua vastaavasti epäcurrymaiseksi (uncurried). Sulut on edellä esitetyistä kaavoista tapana jättää pois: S 1 S 2 T ja S 1 S 2 T. Tuloalueen koostin siis sitoo tiukemmin kuin funktioalueen koostin, joka sitoo oikealle (associates to the right). Currymaisen tavan kautta tulkittuna myös monipaikkaiset funktiot voidaan luokitella kunkin parametrinsa osalta tiukaksi tai väljäksi. Esimerkiksi monissa kielissä looginen ja -operaattori käyttää ns. oikosulkulaskentaa eli on toisen parametrinsa suhteen väljä: jos ensimmäinen argumentti on epätosi, operaation tuloksena on epätosi riippumatta toisesta argumentista. Funktioille on määritelty yhdistämisfunktiot ( ): (T U) (S T) (S U) ( ) = λf.λg.λx.f(g(x)) ( ): (S T) (S T) (S T) { f(x) mikäli f(x) T, ( ) = λf.λg.λx. g(x) muuten. Matemaattisesti suuntautuneelle opiskelijalle jääköön harjoitustehtäväksi todistaa, että tämä funktio todella kuuluu funktioalueeseen. Funktioita on käytännöllistä määritellä paloittain ja yhdistellä määritelmät käyttäen ( )-funktiota. Varsin usein määritelmässä käytetään hyväksi hahmonsovitusta (pattern matching): parametrissa nimen sijasta annetaan jokin pari, jonka alkiona voi olla jokin toinen samanlainen pari tai sitten vakiolauseke
44 LUKU 3. MUUTTUJAT, ARVOT, OLIOT JA TYYPIT tai nimi. Operatiivisena tulkintana on se, että jos argumentin rakenne on sama kuin parametrin, parametrissa esiintyvien nimien arvoksi sidotaan vastaavassa paikassa argumenttia olevat arvot. Puoliformaalisti tämä määritellään rekursiona seuraavasti: ((λx.((λy.e)(prj 2 z)))(prj 1 z)) jos z S T λ(x, y): e = λz. ((λy.e)(sel x z)) jos z S + T ja tst x z = True muuten. missä z on nimi, joka ei esiinny vapaana e:ssä. Esimerkki 5 Lisp-kielestä tuttu funktio cadr voidaan määritellä seuraavasti: cadr: S S cadr = λ(2, (x, (2, (y, z)))).y Olisi sen myös voinut määritellä lausekkeella car cdr. 3.5.2 Oliotason tyypit Jokaiseen olioon liittyy tyyppi. Oliotason tyyppejä ei aina tosin sanota tyypeiksi, vaan tyyppikäsite varataan toisinaan pelkästään kielitason tyyppejä tarkoittavaksi. Esimerkiksi Java-kielen virallisessa terminologiassa oliotason tyyppejä sanotaan luokiksi (class). Oliotasolla tyypillä on kaksi tärkeää tehtävää. Tyypin ensimmäinen tehtävä on tietää olion koko. Toinen tehtävä on tarjota tulkinta olion osoitteesta alkavasta, olion koon pituisesta tavujonosta olion arvoksi. Matemaattisesti oliotason tyyppi on kuvaus kokonaislukujen (jotka on rajattu tavun lukualueelle) jonojen joukolta kaikkien arvotason tyyppien summaalueelle. Tyyppivirheet ja dynaaminen tyyppitarkastus Olioihin kohdistuu operaatioita. Operaatiot olettavat, että kohteena oleva olio on jotain tiettyä tyyppiä. Tyyppivirheellä (type error) tarkoitetaan sitä, että jonkin suoritettavan operaation kohteena on olio, jonka tyyppi ei ole se, mitä operaatio olettaa. Huomaamatta jäävät tyyppivirheet johtavat ohjelman sekoamiseen: koska olio on eri tyyppiä kuin oletetaan, sen tulkinta arvoksi on täysin päätön ja tuloksena on roskaa. Mikäli ohjelmointikieli (eli oikeasti sen määrittely) vaatii toteutukseltaan, että se diagnosoi (ilmoittaa käyttäjälle) kaikki
3.5. TYYPIT 45 tyyppivirheet, kieli on vahvasti tyypitetty (strongly typed). Muussa tapauksessa kieli on heikoisti tyypitetty (weakly typed). Joitakin kieliä voidaan myös verrata keskenään sen mukaan, mitä tyyppivirheitä ne vaativat diagnosoitavaksi. Esimerkiksi C ja C++ ovat heikosti tyypitettyjä (molemmissa on mahdollista kirjoittaa tyyppivirheellinen ohjelma, jonka tyyppivirhettä ei toteutus huomaa), mutta C++ on paljon vahvemmin tyypitetty kuin C. Java, Haskell ja Scheme ovat esimerkkejä vahvasti tyypitetyistä kielistä. Kielen toteutus voi diagnosoida tyyppivirheen joko ennen suoritusta tai suoritusaikana. Ensin mainitussa tapauksessa on kyse staattisesta tyyppitarkastuksesta (static typechecking), jälkimmäisessä dynaamisesta tyyppitarkastuksesta (dynamic typechecking). Staattinen tyyppitarkastus on kielitason ongelma; nyt tarkastellaan dynaamista tyyppitarkastusta. Mikäli dynaamista tyyppitarkastusta halutaan, tulee olion sisällä olla tieto siitä, mitä tyyppiä se on. Oliokielissä (joissa tämä tieto tarvitaan jo metodien dynaamisen sidonnan toteuttamiseen) tämä toteutetaan jo aiemmin mainitulla luokkaoliolla, jonka osoite tallennetaan jokaisen tätä tyyppiä olevan olion alkuun. Tyyppitarkastus voidaan tehdä tarkastamalla, mihin luokkaolioon olion alussa on osoitin. Vastaava tekniikka toimii myös muissa kielissä. Dynaamisissa funktiokielissä (mm. Lisp), joissa tyyppejä on varsin vähän, on tapana käyttää oliona konesanaa, josta varataan muutama bitti tyyppitunnisteeksi (isommat oliot laatikoidaan). Perustyypit Vaikka tyyppijärjestelmän kiinnostavimmat ominaisuudet liittyvät rakenteisiin tyyppeihin, jokainen ohjelmointikieli tarvitsee pohjalleen koko joukon perustyyppejä, jotka edustavat lukuja (ja joitakin muita asioita, joita ajatellaan lukuina). Oleellisesti oliotason perustyypit vastaavat arvotason yksinkertaisia alueita. Joissakin ohjelmointikielissä (esimerkiksi C) on perustyyppinä ainakin yksi rajallisen lukualueen luonnollisten lukujen tyyppi (C:ssä mm. unsigned int). Tämän tyypin olio on aina saman kokoinen (tyypillisesti 2 n tavua pitkä jollakin n, yleensä konesanan pituinen), ja sen lukualue on näin rajoitettu (yleensä nollasta 2 kn 1:een, missä n on olion tavumäärä ja k on koneen tavupituus). Mikäli näitä tyyppejä on useita, ne eroavat toisistaan vain kokonsa (ja siis lukualueensa) puolesta. Kielissä, joissa on vahva staattinen tyypitys, sekä joissakin heikosti tyypitetyissä kielissä koko olio tulkitaan yhdeksi kaksikantaiseksi luonnolliseksi luvuksi. Muissa kielissä osa oliosta joudutaan varaamaan tyyppi-informaatiota varten, muu osa oliosta tulkitaan edellä mainittuun tapaan yhdeksi kaksikantaiseksi luonnolliseksi luvuksi.
46 LUKU 3. MUUTTUJAT, ARVOT, OLIOT JA TYYPIT Jos kielen toteutus ottaa konesanasta käyttöönsä muutaman bitin, ne ovat yleensä sanan vähiten merkitsevät bitit. Luonnollisen luvun tyyppiä olevan olion arvo saadaan sitten jakamalla koko olion lukutulkinta sopivalla kahden potenssilla. Mikäli tämän tyypin tyyppibitit ovat kaikki nollia, yhteen- ja vähennyslasku ei vaadi mitään varotoimenpiteitä ja kerto- ja jakolasku vaativat vain tuloksen skaalauksen. Luonnollisten lukujen tyypeille on yleensä määritelty yhteen-, vähennys- ja kertolasku niin kuin ne toimivat kokonaislukujen jäännösluokkarenkaassa kn (missä n ja k ovat kuten edellä). Toisin sanoen, mikäli tulosluku ylittää tai alittaa lukualueen rajat, tulos siirretään takaisin lukualueelle lisäämällä tai vähentämällä siitä sopiva kn:n monikerta. Käytännössä tämä tapahtuu siten, että ylivuotobitit jätetään huomiotta. Myös jakolasku, jonka tulos katkaistaan, on yleensä käytettävissä, sekä operaatio, joka antaa jakolaskun jakojäännöksen. Tavallista on myös, että näillä tyypeillä on määritelty operaatioita, jotka käsittelevät olion arvoa bittijonona: biteittäiset ja, tai sekä ei -operaatiot sekä monesti myös biteittäinen poissulkeva tai sekä bittien siirto vasemmalle tai oikealle (olion arvon kertominen ja jakaminen kahden potenssilla). Lähes kaikissa ohjelmointikielissä on perustyyppinä ainakin yksi rajallisen lukualueen kokonaislukutyyppi (C:ssä ja Javassa mm. int, Haskellissa Int). Tälle tyypille pätee kaikki edellä mainittu kahta poikkeusta lukuunottamatta: olion ne bitit, joita käytetään olion arvon esittämiseen, tulkitaan kokonaisluvun kahden komplementti -esitykseksi (jolloin olion lukualue on tyypillisesti luvusta 2 kn 1 lukuun 2 kn 1 1), ja lukualueen ylitykset (overflow) ja alitukset (underflow) ovat virhetilanteita. Läheskään aina tällaisia virhetilanteita ei diagnosoida. Lähes kaikissa nykykielissä on perustyyppinä myös ainakin yksi liukulukutyyppi, useimmissa kielissä kaksi ( lyhyt ja pitkä liukuluku, esimerikiksi C:n float ja double). Tyypillisesti niiden esitystapa määritellään uusissa kielissä IEEE 754 -standardin 6 mukaiseksi. Tuo standardi vaatii kaikilta toteutuksilta kaksi liukulukutyyppiä: yksinkertaisen tarkkuuden (single format) ja kaksinkertaisen tarkkuuden (double format) liukulukutyypin. Edellämainittu on 32-bittinen, jälkimmäinen 64-bittinen esitys. Aiemmin tyydyttiin laitteiston tukemiin liukulukuesityksiin. IEEE-liukuluvuilla on se puoli, että nykyisillä 32-bittisillä laitteistoilla ne vievät yhden tai kaksi konesanaa kokonaan. Se on hyvä kielissä, jotka eivät tarvitse suoritusaikaista tyyppi-informaatiota. Toisaalta erityisesti niissä kielten toteutuksissa, joissa käytetään konesanan bittejä tyyppilappuina, kaikki liukuluvut on käytännössä pakko laatikoida. 6. IEEE Standard for Binary Floating-Point Arithmetic, An American National Standard. IEEE Std 754-1985.
3.5. TYYPIT 47 Liukuluvuille on määritelty yleensä kaikki neljä peruslaskutoimitusta sekä (yleensä kirjastoissa) iso joukko matemaattisia funktioita, kuten trigonometriset funktiot ja logaritmi. Toisin kuin luonnollisten lukujen tyypin ja kokonaislukutyypin tapauksessa liukulukujen laskutoimituksilla ei ole aivan suoraviivaista tulkintaa reaalilukujen laskutoimituksina, sillä liukulukulaskuihin liittyy pieniä laskuvirheitä (lisää tästä numeerisen laskennan kursseilla), mutta ne kyllä ovat kohtuuhyviä reaalilukulaskutoimitusten approksimaatioita. Kaikilla edellämainituilla tyypeillä on kaikissa laitteistoissa (mikäli laitteisto kyseistä tyyppiä tukee) tuki arvojen vertailuun ja ohjelman suorituksen ohjailuun vertailun tuloksen perusteella. Tämän johdosta useimmissa kielissä on kielitasolla totuusarvotyyppi, joka oliotasolla käyttäytyy kuten rajallisen lukualueen luonnollisten lukujen tyyppi. Perustyyppeihin lasketaan usein myös merkkityypit, mutta oliotasolla ne näyttäytyvät rajallisen lukualueen luonnollisten lukujen tyyppeinä. Joissakin kielissä on perustyyppinä myös kompleksilukutyyppi ja rajattoman lukualueen kokonaislukutyyppi. Monissa muissa kielissä ne on määritelty kirjastoissa rakenteisina tyyppeinä. Taulukkotyypit Ensimmäisenä rakenteisten tyyppien tapauksena käsittelemme taulukkotyyppejä. Taulukot vastaavat arvotasolla tilanteesta riippuen joko tuloalueita (joissa tekijäalueina on aina sama tyyppi) S n = S S }{{} n tai lista-alueita. Taulukko on siis tietynmittainen jono samantyyppisiä olioita. Taulukon sanotaan olevan homogeeninen tyyppi, koska sen alkiot ovat kaikki samaa tyyppiä. Taulukon perusoperaatio on sen indeksointi. Taulukon alkiot on numeroitu nollasta 7 alkaen. Kuhunkin taulukon alkioon liittyy siis luonnollinen luku, sen indeksi, joka erottaa sen muista kyseisen taulukon alkioista. Indeksointioperaatio ottaa luonnollisen luvun parametrikseen ja tuottaa ulos viittauksen siihen olioon, jonka indeksi kyseinen luku on. Tähän liittyy mahdollisuus virheeseen: mikäli indeksointioperaatiolle annetaan parametriksi taulukon koko tai 7. Tässä oliotason tarkastelussa voidaan todella olettaa, että taulukon indeksit alkavat nollasta, sillä se on tässä luonnollinen valinta. Kielitasolla on sitten mahdollisuus siirtää indeksit mielivaltaiselle kokonaislukuvälille.
48 LUKU 3. MUUTTUJAT, ARVOT, OLIOT JA TYYPIT sitä suurempi luku, ei ole olemassa oliota, jonka indeksi tuo luku olisi. Tämä virhe, vaikka se saattaisi tuntua triviaalilta, on itse asiassa yksi vakavimmista ohjelmointivirhetyypeistä. Kaikki ohjelmoijat syyllistyvät siihen jatkuvasti (elleivät ole erityisen tarkkana, mitä yleensä ei olla), ja se on Internetissä (ja muuallakin) havaittujen turva-aukkojen yleisin syy. Siksi olisi toivottavaa, että kaikki indeksointivirheet tulisivat diagnosoiduiksi mielellään jo ennen ohjelman suorituksen alkamista, mutta tämä on jo utopiaa. Yksinkertaisin taulukkotyyppi on kiinteäkokoisten taulukoiden tyyppi, joissa taulukon koko on määrätty staattisesti eli ennen suorituksen alkamista. Tällöin taulukon koko sisältyy tyyppin olemukseen. Tätä tyyppiä oleva olio on oleellisesti alkio-olioidensa jono, eikä siinä ole mitään ylimääräistä. Koska taulukon koko on tiedossa ennen suorituksen alkua, kääntäjän on helppo sijoittaa käännettyyn ohjelmakoodiin varmistuksia indeksivirheiden varalle. Valitettavan usein näin ei tehdä, tai varmistukset otetaan pois tuotantokäyttöä varten, koska nämä tarkistukset koetaan suoritusta liiallisesti hidastavina. Mutta mitä hyötyä on turvavyöstä, jos sitä pitää vain autokoulussa, ja kortin saatuaan lakkaa sitä käyttämästä? Esimerkiksi C:ssä ja C++:ssa näitä varmistuksia ei ole lainkaan, Javassa tätä taulukkotyyppiä ei esiinny lainkaan. Useissa muissa kielissä varmistukset ovat pakollisia. Seuraavaksi yksinkertaisin taulukkotyyppi on sellaisten kiinteäkokoisten taulukoiden tyyppi, jossa taulukon koko määräytyy suoritusaikana. Tilanne on samanlainen kuin edellä muuten, mutta mikäli halutaan varmistaa indeksointivirheiden diagnosointi, pitää olioon liittää tieto taulukon alkioiden lukumäärästä. Sama pitää tehdä, mikäli halutaan, että taulukon koko on saatavissa selville missä tahansa, missä sitä käytetään. Esimerkiksi C:ssä ja C++:ssa tätä tietoa ei tallenneta taulukko-olioon, joten kaikille taulukkoa käsitteleville aliohjelmille tulee viedä parametrina tieto taulukon koosta, mikä on riskaabelia. Javassa ja Schemessä tieto taulukon koosta sisältyy taulukko-olioon ja indeksivirhevarmistukset ovat aina käytössä. Harvempi kieli sisältää valmista tukea kokoaan muuttaville taulukoille jotkin kielet kyllä mahdollistavat sellaisen rakentamisen itse ja yleensä myös sisältävät varuskirjastossaan (standard library) tällaisen otuksen. Tässäkin taulukkotyyppitapauksessa on useampi muunnelma mahdollinen. Taulukolla voi olla maksimikoko, joka on tiedossa joko ennen suorituksen alkua tai vasta suoritusaikana, ja taulukko voi myös olla kokorajaton. Jos muuttuvakokoisella taulukolla on maksimikoko, se muistuttaa päällisin puolin kiinteäkokoista taulukkoa. Taulukko-olion kooksi voidaan ottaa taulukon maksimikoko, ja näin olion koko ei muutu olion elinaikana. Siksi tällainen olio voidaan luoda hyvin pinodynaamisena. Taulukon kunkinhetkinen koko
3.5. TYYPIT 49 joudutaan luonnollisesti tallettamaan olioon. Muuttavakokoinen taulukko, jolla ei ole kokorajaa, joudutaan aina toteuttamaan laatikoimalla kokorajoitettu taulukko. Jos taulukkoa haluaan kasvattaa yli sen hetkisen maksimikoon, joudutaan luomaan uusi kokorajoitettu taulukko isommalla maksimikoolla ja kopioimaan sinne oliot vanhasta taulukosta. Kaksiulotteiset taulukot voidaan toteuttaa taulukkona taulukoita. Tällöin kaksiulotteisen taulukon rivit (tai sarakkeet) ovat alkioita itse taulukossa. Toisaalta kaksiulotteista voidaan myös simuloida: jos ensimmäisen dimension koko on k 1 ja toisen k 2, saadaan kaksiulotteinen indeksi (i 1, i 2 ) muutettua yksiulotteiseksi kaavalla i 1 k 1 + i 2 (rivipohjainen simulointi) tai kaavalla i 1 + i 2 k 2 (sarakepohjainen simulointi). Useampiulotteiset tapaukset toimivat vastaavasti. Merkkijonot toteutetaan lähes kaikissa käskykielissä taulukkoina. 3.5.3 Monikkotyypit Monikkotyypit (tuple types) vastaavat arvotasolla tuloalueita. Kielitasolla niitä vastaavat paitsi kielitason monikot (tuple) myös tietueet (record, structure) ja oliokielten oliot. Myös algebralliset tietotyypit käyttävät monikkotyyppejä oliotasolla hyväkseen. Monikkotyypit vastaavat monissa suhteissa kiinteäkokoisia taulukoita, joiden koko tiedetään ennen suorituksen alkamista. Ainoa merkittävä ero on, että siinä missä taulukot ovat homogeenisiä, monikot ovat heterogeenisiä eli voivat tallettaa eri alkioissaan erityyppisiä olioita. Hieman vähemmän huomiota herättävä ero on se, että siinä missä taulukkoa indeksoidaan, monikosta valitaan. Kullakin monikkotyypillä on omat valintaoperaationsa, joita tyypillä on yhtä monta kuin sen oliolla alkioita. Kukin valintaoperaatio vastaa yhtä alkiota; se ottaa tämän tyypin olion ja antaa tulokseksi viittauksen siihen alkioon (olioon), jota valintaoperaatio vastaa. Monikkotyypin alkioilla on tietty järjestys, jossa ne sijaitsevat monikko-oliossa, mutta ne eivät välttämättä sijaitse tiiviisti toistensa perässä. Useimmilla prosessorityypeillä tiettyjä tasaussuosituksia tai -vaatimuksia (alignment recommendations or requirements), joiden noudattaminen joko on pakollista tai nopeuttaa ohjelman suoritusta. Tämä tarkoittaa, että tietynkokoisia olioita on nopeampi käsitellä tietynlaisista osoitteista tai se ei olisi edes mahdollista muuten. Esimerkiksi 32-bittisiä olioita on hyvä tallettaa nykyprosessoreilla vain kahdeksalla jaollisiin osoitteisiin. Tämän vuoksi monikkotyypin alkioiden väliin jää toisinaan tyhjää tilaa, jolla ei tehdä mitään. Onkin tärkeää huomata, että monikko-olion koko ei ole useinkaan sen alkio-olioiden kokojen summa.