Tyyppijärjestelmä

Ohjelmointikielen tyyppijärjestelmä määrittää, kuinka ohjelmointikieli luokittelee ohjelmassa esiintyvät entiteetit tyyppeihin. Tyyppejä ovat lausekkeet, muuttujat ja niin edelleen. Tyyppijärjestelmä vaikuttaa kuinka kielessä käsitellään tyyppejä ja tyyppien välisiä toimintoja.

Tyyppi tai tietotyyppi on numeeristen arvojen tulkintatapa. Tyyppi voi koostua joukosta sallittuja arvoja (enumeraatio). Tyyppi voi olla komposiittityyppi, joka sisältää useita arvoja (kuten taulukko, luokka ja niin edelleen).

Tyyppijärjestelmä voi olla laajennettava: ohjelmoija voi määrittää uusia tyyppejä kuten luokkia.

Merkitys 

Tietotyypit antavat tulkinnan tietokoneen muistissa oleville bittijonoille. Tavanomaisten tietokoneiden muisti koostuu järjestyksessä olevista biteistä, joiden tulkinta on vapaa. Niinpä sama osa muistin sisältöä voidaan tulkita vaikkapa merkiksi, kokonaisluvuksi, liukuluvuksi tai osoitteeksi toiseen muistipaikkaan. Tyypit kertovat ohjelmille ja ohjelmoijille, kuinka nämä bitit tulkitaan.

Muuttujan tai arvon tyyppi määrittelee, millä tavoin se voi toimia muiden muuttujien tai arvojen kanssa. Tyyppitarkastuksessa varmistutaan siitä, että tyypit toimivat näiden rajoitusten mukaan, eli tarkastetaan osa ohjelman semantiikasta. Tyyppitarkastus voidaan suorittaa ohjelman käännösaikana (staattinen tyyppitarkastus) tai ajoaikana (dynaaminen tyyppitarkastus).

Ohjelmointikielissä voidaan antaa lause:
int abc = 42;
.. jossa int on muuttujan abc tyyppi ja literaali 42 on muuttujaan sijoitettava arvo.
Kääntäjä antaa virheen, jos muuttujaan yritetään sijoittaa jotain mitä tyyppijärjestelmän mukaan siihen ei voida sijoittaa kuten:
vector<int> v;
abc = v; // VIRHE

Tyyppijärjestelmä havaitsee jos tyypillä yritetään tehdä toimintoja, joita sillä ei voida tehdä:
cout << abc + abc; // ok
cout << *abc; // VIRHE

Tyyppijärjestelmä antaa seuraavia merkittäviä hyötyjä:

 Tyyppiturvallisuus - Tyyppien ansiosta ohjelmointikielen kääntäjä tai tulkki voi tunnistaa virheitä ohjelmakoodissa. Esimerkiksi lauseke 
 "Hello, World" / 3
voidaan tunnistaa virheelliseksi, koska merkkijonon jakaminen kokonaisluvulla ei ole mielekäs operaatio.
 Optimointi - Jos kääntäjä esimerkiksi tietää, että jokin muuttuja on aina 8-bittinen kokonaisluku, sen ei tarvitse varata muistista arvoa varten kahdeksaa bittiä enempää tilaa. Kääntäjä voi myös käyttää 8-bittisille kokonaisluvuille parhaiten soveltuvia konekielen käskyjä muuttujan käsittelyyn.
 Dokumentointi - Ohjelmoija voi tyyppien huolellisella määrittelyllä ja käytöllä selventää ohjelmakoodin tarkoitusta. Tyyppijärjestelmä voi esimerkiksi mahdollistaa rahamäärää kuvaavien arvojen erottamisen paljaista reaaliluvuista. Kirjoittamalla esimerkiksi funktion paluutyypiksi rahamäärän eikä reaalilukua ohjelmoija dokumentoi osan funktion merkityksestä.
 Abstraktointi - Tyyppien ansiosta ohjelmaa on helpompi tarkastella tietokoneen toimintojen tasoa korkeammalla abstraktiotasolla. Esimerkiksi pankkisovelluksen parissa työskentelevä ohjelmoija voi käyttää ohjelmoinnissaan ja ajattelussaan enemmän pankkialan termistöä ja vähemmän tietokoneen teknisten yksityiskohtien termistöä.

Ensimmäisen luokan tyyppejä ovat helposti käytettävät muuttujatyypit kuten int C++-kielessä. Ensimmäisen luokan tyypeillä:
 voidaan ajonaikana luoda helposti uusia arvoja olemassa olevista arvoista
 arvoja voidaan välittää argumentteina funktioille tai palautettua funktioista
 arvoja voidaan tallettaa säiliöluokkiin

Eräät kielet tukevat funktioita ensimmäisen luokan tyyppeinä: näitä ovat muun muassa Haskell, Scheme, Python ja Lua.

Yksittäisen rajapinnan tarjoamista erityyppisille entiteeteille kutsutaan polymorfismiksi.

Staattinen vs. dynaaminen tyypitys 

Tyyppijärjestelmä voi olla staattinen tai dynaaminen: staattisessa tyypityksessä tyyppisidos päätetään käännösvaiheessa kun taas dynaamisessa tyypityksessä tyyppi tarkistetaan ja päätetään ajonaikaisesti.

Esimerkiksi C-kielen tyyppijärjestelmä on staattinen, koska muuttujien tyypit on pakko antaa muuttuja- ja funktioesittelyiden yhteydessä. Sen sijaan useimmat tulkattavat (eivät kuitenkaan kaikki) kielet ovat dynaamisesti tyypittäviä kieliä, kuten Perl ja Python. Java käyttää yhdistelmää käännösaikaisia ja ajonaikaisia tarkistuksia.

Esimerkiksi seuraava ohjelmakoodinpätkä on laillista Pythonia:

 def parse_arg(index):
     count, filename = ARGV[0], ARGV[1]
     count = int(count)
     return count, filename

Aluksi count on tässä merkkijonotyyppinen, koska taulukon ARGV (komentoriviargumentit) alkiot ovat merkkijonoja. Seuraavalla rivillä count muunnetaan kokonaislukutyyppiseksi ennen return -lausetta. Muuttujan count tyyppi siis vaihtuu ajonaikana, mutta siitä ei aiheudu virhettä.

Dynaaminen tyypitys tarjoaa joustavuutta, mutta se ei ole automaattisesti staattista tyypitystä parempi ratkaisu. Staattisen tyyppijärjestelmän avulla monet virheet voidaan havaita käännösaikana, kun taas dynaamisesti tyypitetyssä kielessä vastaava virhe havaitaan vasta ajon aikana.

Joissakin kielissä tyyppi voidaan päätellä, jolloin se näyttää dynaamiselta mutta on todellisuudessa staattisesti tyypitetty:
var x = 10; // C#
auto x = 10; // C++

// molemmissa vastaa samaa kuin:
int x = 10;

OCaml käyttää implisiittistä tyypitystä, joka kuitenkin on staattisesti tyypitetty.

Manifesti vs. implisiittinen tyypitys 
Kielissä, joissa tyyppi määritetään eksplisiittisesti käytetään manifestityypitystä ja tyyppiannotaatiota. Esimerkiksi:
double sq47(double n)
{
    double result = 4.7 * n * n;
    return result;
}

Muutoin käytössä on implisiittinen tyypitys, jolloin tyyppi voidaan päätellä. Dynaamisesti tyypitetyt kielet käyttävät yleensä implisiittistä tyypistystä.
Esimerkki implisiittisestä tyypityksestä Haskell-kielellä, joka käyttää staattista tyypitystä:
sq47 n = result where
    result = 4.7 * n * n

Joissakin tapauksissa dynaaminen ja implisiittinen voivat näyttää samoilta, mutta käytännössä ovat eri asioita.

Nimellinen vs. rakenteellinen 
Kielet voivat käyttää nimettyä tyyppiä huolimatta sen rakenteesta, jolloin tyyppi tarkistetaan vain sille annetun nimen perusteella. Esimerkiksi ohjelmoijan luomat tyypit:
struct A {
    int h;
    int m;
};

struct B {
    int h;
    int m;
};
.. jossa A ja B ovat eri tyyppejä.

Nimelliseen tyyppiin on löyhempiä tapauksia, kuten perityn luokan välittäminen sen kantaluokan mukaan.

Rakenteellisessa tyypityksessä (kutsutaan myös ankkatyypitykseksi) tyyppejä voidaan käsitellä keskenään vaihdettavina mikäli niillä on sama rakenne ja ne tukevat samoja toimintoja.

Esimerkiksi C++-kielellä:
template <typename T, typename U>
T addEm(T a, U b)
{
    return a + b;
}

Python-kielellä:
def addEm(a, b):
    return a + b

Go-kielellä:
type Shape interface {
        Area() float64
}

Latentti tyypitys 
Latentti tyypitys on ohjelmointikielien kuten Schemen tyypitys, jossa yleisesti ottaen et voi ohjelman lähdekoodia lukemalla päätellä mitä tyyppiä tiettyyn muuttujaan tai lausekkeeseen liittyvä data on.

Vahva ja heikko tyypitys 

Vahva ja heikko tyypitys on tyyppijärjestelmän ominaisuus, joka määrittelee miten erityyppisten muuttujien väliset muunnokset käsitellään keskenään. Kieli voi käyttää joko vahvaa tai heikkoa tyypitystä.

Tyyppijärjestelmättömät ohjelmointikielet 
Eräissä ohjelmointikielissä ei ole tyyppijärjestelmää: esimerkkejä ovat BCPL ja B-kielet.

Forth-kieli erottelee kokonaisluvut ja liukuluvut, joten sillä on tyypin käsite, mutta näitä käsitellään eri syntaksilla ja sillä ei ole tyyppitarkistusta.

Katso myös 
 Polymorphismi
 Myöhäinen ja varhainen sidonta

Lähteet

Aiheesta muualla 
 Type Theory

Kirjallisuutta 
 

Tietojenkäsittelyteoria
Ohjelmointi