Konekielinen ohjelmointi (yleensä ns. "assemblerilla") on hyvin kömpelöä ja virhealtista. Käytännössä nykyään lähes aina ohjelmoidaan ns. lausekielillä. Niitä käyttäen ohjelmoija välttyy koneen sisäisen rakenteen ajattelemiselta. Ohjelmista saadaan lausekielellä ohjelmoiden myös siirrettäviä: ne ovat käytettävissä kaikissa tietokoneissa, joille ko. lausekieli on toteutettu.
Lausekielten ajattelutapa on konekieltä paljon ihmisläheisempi: jo ensimmäiset lausekielet mahdollistivat matematiikasta tuttujen laskutoimitusten kirjoittamisen; voitiin kirjoittaa vaikkapa A+B*C sen sijaan, että olisi itse kerrottu, mistä koneen osasta minne bittijonoja siirrellään ja minkä bittijonojen välillä käynnistetään peruslaskutoimituksia. Sittemmin lausekieliin on kehitelty monenlaisia ajatusten järjestämisen välineitä: nimettyjä aliohjelmia ja aliohjelmakirjastoja, lohkorakennetta, olioita ja luokkia, ...
Jotta tietokone saataisiin suorittamaan lausekielellä laadittuja ohjelmia, lausekieliset ohjelmat on tavalla tai toisella kuitenkin muokattava konekielisiksi - tietokonehan ei muuta "osaa" kuin vain konemaisesti suorittaa muistissaan olevaa konekielisten operaatioiden jonoa. Tuollaista muokkaamista kieleltä toiselle kutsutaan kääntämiseksi.
Lausekielisen ohjelman kääntäminen konekielelle on aika työlästä ja mekaanistakin puuhaa, joten se sopii mainiosti tietokoneelle: ohjelmaa, jonka syöttötietoina on algoritmi lausekielisenä ja tulostietoina vastaava algoritmi vaikkapa konekielisenä, kutsutaan kääntäjäksi (compiler).
syöttötiedot
omalle algoritmille
|
V
oma algoritmi KÄÄNTÄJÄ OMA ALGORITMI
lausekielellä --> KONEKIELELLÄ --> KONEKIELELLÄ
|
V
oman algoritmin
tulostiedot
Kääntäjä usein ohjelmoidaan itsekin lausekielellä. Miten se itse saadaan
käännettyä konekieliseksi? Tietokonehan ei muunlaisia algoritmeja osaa
suorittaa!
Ohjelmointikieli voidaan toteuttaa myös tulkin (interpreter) avulla. Tällöin konekielinen tulkkiohjelma suorittaa lausekielistä algoritmia "enemmän tai vähemmän sellaisenaan":
syöttötiedot
omalle algoritmille
|
V
oma algoritmi TULKKI
lausekielellä --> KONEKIELELLÄ
|
V
oman algoritmin
tulostiedot
Lausekielisen ohjelman tulkkaaminen "enemmän tai vähemmän sellaisenaan" on joustavaa, mutta se saattaa olla hidasta monestakin syystä. Yksi mahdollisuus tehostaa tulkintaa on ns. välikielen käyttäminen. Välikieli voi olla käsitetasoltaan konekielen kaltaista, mutta todellisia konekieliä yksinkertaisempaa. Jos välikielen operaatiot esitetetään tietokoneen muistin tavuina (yleensä 8 bittiä), kieltä kutsutaan tavukoodiksi. Java on toteutettu juuri näin. Javan tavukoodin nimi on Bytecode.
syöttötiedot
omalle algoritmille
|
V
oma algoritmi VÄLIKIELIKÄÄNTÄJÄ OMA ALGORITMI VÄLIKIELITULKKI
lausekielellä --> KONEKIELELLÄ TAI --> VÄLIKIELELLÄ --> KONEKIELELLÄ
VÄLIKIELELLÄ
|
V
oman algoritmin
tulostiedot
Lausekieltä välikielelle kääntävä ohjelma voi tietenkin olla konekielinen,
mutta se voi olla myös välikielellä ilmaistu! Miten kääntäjä tässä
tapauksessa saadaan suoritettua tietokoneessa, joka tietenkin
osaa vain omaa konekieltään?
Muuttuja tarkoittaa matematiikassa jotakin melko syvällistä: esimerkiksi ilmauksessa a+b=b+a väitetään vaikkapa minkä tahansa lukuparin summan olevan riippumaton laskentajärjestyksestä. Tilastotieteessä muuttuja voi tarkoittaa "selittävää tekijää"; esimerkiksi myytyjen jäätelöiden määrä voisi olla muuttujana hukkumiskuolemien määrää selitettäessä.
Algoritmisessa ohjelmoinnissa muuttuja on paljon konkreettisempi asia: muuttuja on arvon säilytyspaikka, "laatikko, jonka kyljessä on nimi". Laatikon sisältöa voi tutkia ja sisältöä voi muuttaa. Uuden sisällön asettamista laatikkoon kutsutaan sijoittamiseksi, sijoitusoperaatioksi tai sijoituslauseeksi.
Sijoitus korvaa muuttujan vanhan sisällön uudella. Uusi arvo voi olla jonkin laskutoimituksen, ns. lausekkeen (expression) arvo. Tuo lauseke voi sisältää vakioita, muuttujien arvoja ja laskutoimituksia.
Sijoitusoperaation ilmauksena algoritmeja kirjoitettaessa käytetään usein merkkijonoa ":=". Myös ilmausta "<-" näkee joskus. Valitettavasti eräissä ohjelmointikielissä (mm. Javassa!) sijoitusoperaation merkkinä käytetään yhtäsuuruusmerkkiä "=". Näissä kielissä matematiikasta tuttuun yhtäsuuruuden vertailuun joudutaan siten käyttämään matematiikan kielestä poikkeavaa ilmausta, esimerkiksi kahta yhtäsuuruusmerkkiä peräkkäin "==". Tässä oppimateriaalissa käytetään Javan tapaan sijoitusoperaation ilmaisemiseen yhtäsuuruusmerkkiä. :-(
Huom: Algoritmiesimerkeissä käytetään Scala-ohjelmointikieltä toukokuusta 2008 lähtien! Tämän Java-pitoisen kurssin materiaalia Scala-kielellä (ja muuta Scala-tietoa) löytyy alkaen sivultani: Scala.
Esimerkki:
(Ilmaus var eka tarkoittaa, että määritellään
muuttuja ("variable") nimeltä eka.)
var eka = 7 var toka = 9 * eka var kolm = (eka + toka) * 100 eka = eka + 1Huom: Sijoitusoperaatio kannattaa heti alussa opetella lukemaan "saa arvokseen", ei "on", vaikka ohjelmointikielessä Javan tapaan käytettäisiin yhtäsuuruusmerkkiä sijoitusoperaation ilmauksena! Erityisen hyvin tuo viimeinen esimerkki osoittaa, miksi.
eka = eka + 1
Ilmaus "eka on eka plus yksi" on looginen epätotuus. Sitävastoin
"eka saa arvokseen eka plus yksi" ilmaisee, mistä on kysymys:
ekan vanhaan arvoon lisätään yksi ja saatu summa sijoitetaan
muuttujan eka uudeksi arvoksi.
Algoritmia siis suoritetaan ajassa: "ensin tehdään sitä, sitten tätä". Algoritmin muuttujien arvot kullakin ajan hetkellä muodostavat algoritmin ns. tilan (state). Ja oikeastaan kaikki tietokoneohjelmat ovat lopulta vain algoritmeja, jotka etenevät tilasta toiseen. Jos ohjelmassa näyttäisi olevan jotakin "viisautta", kyse on siitä, että ohjelman laatija on osannut rakentaa algoritmilleen sellaisen tilojen ketjun, joka ohjelman käyttäjästä vaikuttaa viisaalta.
Edellisen esimerkin tilat:
tila: |__?____| |__?____| |__?____|
eka toka kolm
eka = 7
tila: |__7____| |__?____| |__?____|
eka toka kolm
toka = 9 * eka
tila: |__7____| |__63___| |__?____|
eka toka kolm
kolm = (eka + toka) * 100
tila: |__7____| |__63___| |__7000_|
eka toka kolm
eka = eka + 1
tila: |__8____| |__63___| |__7000_|
eka toka kolm
Algoritmi liitetään ympäristöönsä ns. syöttö- ja
tulostusoperaatioin. Syöttöoperaation eli lukemisen
seurauksena jokin muuttuja saa arvon ohjelman käyttäjältä tai
vaikkapa jostakin tiedostosta. Lukeminen siis muuttaa ohjelman
tilaa. Tulostusoperaatio, kirjoittaminen puolestaan laskee
jonkin lausekkeen arvon ja siirtää tuloksen jollekin tulostuslaitteelle.
Tällöin ohjelman tila ei muutu.
Syöttötietojen ajatellaan olevan syöttöjonossa, josta ne yksi kerrallaan luetaan. Tulostietojen ajatellaan puolestaan olevan tulosjonossa, johon ne yksi kerrallaan kirjoitetaan. Nämä jonot voivat ihan konkreettisestikin olla "jonoja" jossakin tiedostossa, mutta esimerkiksi vuorovaikutteisen ohjelman syötteiden ja tulosteiden "jonot" ovat pikemminkin jonoja ajassa.
Esimerkki:
println("Anna kaksi lukua")
var eka = readInt
var toka = readInt
var kolmas = eka + toka
println("Niiden summa on")
println(kolmas)
Jos tämän ohjelman syöttöjonossa on luvut 3 ja 4, muuttujaan eka luetaan arvo 3, muuttujaan toka arvo 4. Sitten muuttujaan kolmas sijoitetaan summa eka+toka eli arvo 7. Lopuksi ohjelma kirjoittaa tulosjonoon arvon 7. Näin "jonot" ovat siis ohjelman käyttäjän näkökulmasta toimintoja näytön ja näppäimistön äärellä.
Peräkkäin kirjoitetut operaatiot siis suoritetaan peräkkäin - "vasemmalta oikealle, ylhäältä alaspäin". Alkeisoperaatioiden (sijoitus, lukeminen, kirjoittaminen) suorittamista ohjataan ns. rakenteisin operaatioin. Monet rakenteiset operaatiot perustuvat siihen, että väitetään jotakin algoritmin tilasta, ja jos väite on tosi, "tehdään jotakin", jos epätosi, "tehdään jotakin muuta". Esimerkkejä tällaisista ovat valinta ja toisto:
Esimerkki valinnasta:
println("Anna luku. Tutkin, onko se positiivinen")
var luku = readInt
if (luku>0) // valinta
println("Luku on positiivinen.")
else
println("Luku ei ole positiivinen.")
Esimerkki toistosta:
println("Moneenko kertaan haluat saada onnittelut?")
var määrä = readInt
var monesko = 0
while (monesko < määrä) { // toisto
println("Onnea!")
monesko = monesko + 1
}
Mitä seuraava ohjelmointivirheen sisältävä ohjelma tekee
kun sille syötetään positiivinen luku? Entä kun sille
syötetään negatiivinen luku? Simuloi ja opi! Mitä opit?
Esimerkki toistosta:
println("Moneenko kertaan haluat saada onnittelut?")
var määrä = readInt
var monesko = 0
while (monesko määrä) { // toisto
println("Onnea!")
monesko = monesko + 1
}
Huom: Rakenteiset operaatiot ovat "rakenteisia" siksi,
että niiden rakenneosina on muita lauseita. Noita rakenneosia voidaan
kutsua alialgoritmeiksi: "Toistolause toistaa alialgoritmia..."
Lady Ada Lovelace, lordi Byronin tytär, keksi valinnan ja toiston 1800-luvun lopulla. Hän laati ohjelmia Charles Babbagen suunnittelemaan mekaaniseen tietokoneeseen, jossa ohjelmat suoritettiin suoraan reikäkorttipakalta. Lady Ada keksi täydentää konekieltä operaatioilla, jotka jostakin ehdosta riippuen hyppäävät joidenkin korttien ohi tai jotka siirtävät jo ohitetun korttipakan osan uudelleen luettavaksi!
Ehdollisuus - algoritmin tilasta riippuva toiminnan ohjaus - on keskeistä algoritmeja laadittaessa. Ilman ehdollisuutta algoritmi toimisi samoin kaikilla syöttötiedoilla! Alialgoritmien nimeäminen ja nimettyjen alialgoritmien kutsuminen on myös tärkeä väline: Kerran ohjelmoitua alialgoritmia ei tarvitse kirjoittaa uudelleen ja uudelleen.
Käytännössä ohjelmat kuitenkin ovat niin suuria ja monimutkaisia, että algoritmin laatijan ajatusten hallintaan tarvitaan välineita. Jollakin tavoin on voitava rajoittaa kerralla ajateltavien asioiden määrä niin pieneksi, että ajatusten voima vielä riittää oikean toimintalogiikan laatimiseen ja ohjelmointivirheiden välttämiseen.
Jo edellä mainitut valinta ja toisto sekä alialgoritmien nimeäminen ovat välineitä ohjelmointiongelman jäsentelyyn. Muita ovat mm. ns. lohkorakenne, oliot, luokat, kirjastot, ...
"Oikean" ohjelman tila on tyypillisesti hyvin suuri, ts. muuttujia on hyvin paljon. Kuitenkin useimmilla muuttujilla on merkitystä vain jossakin ohjelman osassa. Jäsentelyn välineet yleensä antavat keinoja myös muuttujien ja ohjelman muun kaluston käytettävyyden säätelyyn, ns. nimien näkyvyyden säätelyyn.
Rakenteiset lauseet ja lohkorakenne liittyvät ns. osittavaan ongelmanratkaisuun (stepwise refinement, "top down" -suunnittelu) ja ns. rakenteiseen ohjelmointiin (Algol, Pascal, ...).
Oliot, niiden luokat ja luokkien hierarkia puolestaan ovat ns. olio-ohjelmoinnin perusvälineitä. Ne täydentävät rakenteisen ohjelmoinnin ajatteluvälineitä toisaalta kokoavalla ongelmanratkaisulla (abstraktit tietotyypit, abstraktit koneet, "bottom up" -suunnittelu), toisaalta ongelmamaailman mallinnusvälineillä ("Missu on kissa, kissa on imettäväinen, imettäväinen on selkärankainen, selkärankainen kuuluu 'eläinkuntaan'") (Smalltalk, Modula, C++, Java, ...)
Tietokoneessa kaikki tiedot, muuttujien arvot, esitetään bittijonoina ja koneen kannalta bittijono on vain bittijono. Mutta ohjelmoijan kannalta jotkin bittijonot esittävät kokonaislukuja, jotkin desimaalilukuja, jotkin merkkejä, jotkin totuusarvoja, jotkin merkkijonoja, jotkin kokonaislukumatriiseja, ... Ohjelmoijan kannalta siis muuttujien arvoilla ja samalla itse muuttujilla on jokin tyyppi (type). Vanhanaikaisilla ohjelmointikielillä ohjelmoidessa tyypillinen virhe oli vahingossa käsitellä vaikkapa desimaaliluvun bittiesitystä kokonaisluvun tavoin, kokonaislukua totuusarvon tavoin jne.
Tuollaisia virheitä oli vaikea välttää, mutta sitten keksittiin hyvä ajatus: Miksei panna tietokonetta tarkistamaan, että arvoja käytetään ohjelmoijan tarkoittamalla tavalla. Juuri tuollainen mekaaninen tarkistustyöhän sopii vallan erinomaisesti juuri koneelle.
Edellytys oli, että ohjelmoija ilmoittaa ennalta, mihin aikoo mitäkin muuttujaa käyttää. Kieliin otettiin mukaan muuttujien määrittelyt: esitellään ennalta, millaiseen tarkoitukseen kutakin muuttujaa käytetään. Sitten ohjelmointijärjestelmä (kääntäjä ja käännetty ohjelma) pyrkii tarkastamaan ja varmistamaan, ettei ohjelmoija yritä sijoittaa muuttujalle vääräntyyppistä arvoa. Tätä nimitetään vahvaksi tyypitykseksi.
Takaisin luvun 1 sisällysluetteloon.