Oppimateriaali perustuu kevään 2010 kurssiversion materiaaliin ja Arto Wiklan ohjelmointisivustoon. Materiaalin copyright © (aakkosjärjestyksessä): Matti Luukkainen, Matti Paksula, Arto Vihavainen, Arto Wikla. Materiaalia saa vapaasti käyttää itseopiskeluun. Muu käyttö vaatii luvan.

I Algoritmeja (Scala)

(Muutettu viimeksi 16.9.2010, sivu perustettu 20.8.2010. Arto Wikla)

Tässä luvussa tutustutaan algoritmin ideaan ja perustavanlaatuisten algoritmien laadintaan. Algoritmi on toimintaohje jonkin ongelman ratkaisemiseen tai vaikkapa jonkin palvelun toteuttamiseen. Nykyiset tietokoneet aina suorittavat jotakin algoritmia, kun ne yleensäkin tekevät jotakin. Myös useimmat ohjelmointikielet perustuvat algoritmin kirjoittamiseen – toisin sanoen ohjelmoija ajattelee laativansa ajassa etenevää toimintaohjetta. Tosin on toisenlaisiakin ohjelmointikieliä...

---> Lisätietoa algoritmeista

Tässä luvussa algoritmeja kirjoitetaan Scala-ohjelmointikielellä, joka nimensä mukaisesti skaalautuu hyvin yksinkertaisesta hyvin monimutkaiseen. Turha varmaan mainitakaan, kummassa päässä skaalaa tässä luvussa liikutaan. On syytä harjoituksissa noudattaa tämän luvun esimerkkien tyyliä, vaikka Scalalla kaiken voi tehdä ties millä tavalla.

Ohjelmoinnin aloittaminen

Ohjelman kirjoittaminen

Aloitetaan ohjelmointi kansainväliseen tapaan. Kirjoita seuraava ohjelma tekstitiedostoon Hello.scala.

println("Hello world!");

Toisinaan tuollaista tekstitiedostoon kirjoitettua ohjelmaa kutsutaan koodiksi. Nimitys on kotoisin 1950-luvulta, jolloin konekielikäskyjä "koodattiin" symbolisilla nimillä.

Ohjelman suorittaminen

Tämän jälkeen anna komentotulkissa komento scala hello.scala. Olet nyt itse asiassa aloittanut ohjelmoijan urasi! Editoi tiedostoasi: muuta tulostettavaa tekstiä ja lisää uusia tulostuslauseita. Talleta tiedosto ja suorita taas. Osaat siis jo tehdä ohjelmia, jotka tulostavat, mitä ikinä maailmassa voidaankaan pyytää tulostamaan.

Algoritmi tietokoneen suoritettavaksi

Tietokone ei kerta kaikkiaan "osaa" suorittaa mitään muuta algoritmia kuin sellaista, joka on kirjoitettu koneen sisäisellä omalla konekielellä. Tuo kieli kuitenkin on useimpiin ohjelmointitehtäviin auttamattoman kömpelöä ja koneenläheistä.

Lähdekielinen ohjelma

Ohjelma kirjoitetaan useimmiten jollakin konekieltä korkeamman tason kielellä, lausekielellä. Toisinaan näin kirjoitettua ohjelmaa kutsutaan lähdekoodiksi. Lähdekoodi talletetaan tekstimuodossa ja suoritetaan jollakin tavalla.

Kääntäjä ja tulkki

Tietokone ei siis osaa suorittaa mitään muita kuin konekielellä ilmaistuja algoritmeja. Siksi tarvitaan kääntäjä tai tulkki, joka muokkaa lähdekielisen ohjelman tavalla tai toisella konekielioperaatoiden jonoksi.

---> Lisätietoa kääntäjistä ja tulkeista

Muuttuja ja sijoitus, tietotyyppi

Algoritmisessa ohjelmoinnissa muuttuja on keskeinen käsite. Muuttuja on nimetty "laatikko", jossa säilytetään jotakin arvoa. Ja kuten nimi "muuttuja" antaa ymmärtää, tuota arvoa voi muuttaa. Algoritmin tila tarkoittaa algoritmin muuttujien arvoja "tällä hetkellä".

Vaikka tietokoneen muistissa kaikki arvot esitetään lopulta vain bittien jonona, "bittimoskana", nykyaikaisissa ohjelmointikielissä ohjelmoija käsittelee arvoja ja muuttujia siten, että niillä on tyyppi.

Tyypillisiä tyyppejä ovat esimerkiksi merkkijono, kokonaisluku, "liukuluku" eli desimaaliluku ja totuusarvo. Muuttujaan sijoitetaan arvo yhtäsuuruusmerkillä (=). Sijoituslause on syytä lukea "muuttuja saa arvon", ei "muuttuja on". Miksi?

---> Lisätietoa tyypeistä

Scala-kielellä muuttuja määritellään ilmauksella var muuttujan nimi. Muuttuja saa tyypikseen määrittelyn yhteydessä annetun arvon tyypin. (Muitakin tapoja Scalassa on...)

Esimerkkejä muuttujien määrittelystä ja arvojen muuttamisesta

var teksti = "juttuhomma";
var kokonaisluku = 1234;
var liukuluku = 3.14;
var onkoTotta = true;

println(teksti);
println(kokonaisluku);
println(liukuluku);
println(onkoTotta);

teksti = "uusjuttu";
kokonaisluku = -666;
liukuluku = 1.2;
onkoTotta = false;

println(teksti);
println(kokonaisluku);
println(liukuluku);
println(onkoTotta);

Kuten esimerkissä nähdään, muuttujan arvoa siis voi muuttaa. Mikä on algoritmin tila ennen ensimmäisiä tulostuksia? Entä ennen toisia?

Tunnukset eli muuttujien yms. nimet

Ohjelmia laadittaessa monenlaisille itse määritellyille asioille annetaan itse päätettäviä nimiä eli tunnuksia (identifier). Esimerkiksi juuri muuttujien nimet ovat näitä tunnuksia. Javassa ja Scalassa tunnukset alkavat aina kirjaimella ja voivat sisältää kirjaimia ja numeromerkkejä. (Myös alaviiva "_ lasketaan "kirjaimeksi".)

Vaikka skandinaaviset "ääkkösemmekin" kelpaavat tunnuksissa käytettäviksi, niitä on paras välttää, koska maailmalla on käytössä pari hyvin yleistä toisistaan poikkeavaa tapaa koodata nuo rakkaat kirjaimemme, ks. Wikipedia-artikkeli merkkikoodauksesta.

Nimessä ei siis saa olla erikoismerkkejä, kuten vaikkapa huutomerkkejä (!). Välilyönti ei ole sallittu, sillä se erottaa kielen rakenteita toisistaan. Välilyönti kannattaa korvata alaviivalla tai camelCase tyylillä, jolloin nimi muistuttaneeKamelia.

Muuttujan nimi ei myöskään saa olla jo entuudestaan käytössä. Tälläisiä nimiä ovat mm. aikaisemmin määritellyt muuttujat ja esimerkiksi tulostusoperaation nimi println – ja monet muut varatut nimet.

Muuttuja kannattaa nimetä siten, että sen käyttötarkoitus on selvää ilman selityksiä tai miettimistä. Itse asiassa tällä kurssilla muuttujat pitää nimetä kuvaavasti!

Kunnollisia muuttujien nimiä:

Huonompia muuttujien nimiä:

Virheellisiä muuttujien nimiä:

Kurssilla suositaan (Javan tapaan) Camel Case -nimiä: kuukaudenEnsimmainenPaiva, jne... Ja säästetään isolla kirjaimella alkavat tunnukset Javan luokkanimille. Kurssin Scala-osiossa siis kaikki tunnukset alkakoot pienellä kirjaimella.

Selitteet eli kommentit

Ohjelmatekstiin on mahdollista, hyödyllistä ja välttämätöntä liittää selittäviä tekstejä eli kommentteja. Miksi?

Scalalla ja Javalla on käytössä kaksi kommentointitapaa, monirivinen ja rivin loppu -kommentti:

/* Tämä ohjelma esittelee muuttujan määrittelyä,
   arvon tulostamista ja arvon muuttamista.      */

var kokonaisluku = 1234;  // määritellään ja annetaan alkuarvo

println(kokonaisluku);    // tulostellaan

/* ja sitten
   muutetaan muuttujan arvoa: 
*/
kokonaisluku = -666;

println(kokonaisluku);   // muuttuiko?

Tulostaminen

Tulostukseen on käytettävissä jo tutun println-operaation lisäksi operaatio print. Ensin mainittu vaihtaa riviä tulostettuaan, jälkimmäinen ei vaihda.

print("kissa")
println("kävelee")  // tulostaa: kissakävelee

println("kissa")
println("kävelee")  // tulostaa: kissa
                    //           kävelee

var i = 4321;
print("Muuttujan i arvo on ");
print(i);
println(".");    // tulostaa Muuttujan i arvo on 4321.

Merkkijonoista ja niiden katenoinnista

Tuttu operaatio + sovellettuna kahden merkkijonon välille synnyttää uuden merkkijonon, jossa kaksi merkkijonoa liitetään toisiinsa. Hienolla nimellä tätä kutsutaan merkkijonojen katenoinniksi.

println("kissa" + "kävelee") // tulostaa: kissakävelee

Eipä tästä vielä paljon iloa ole, mutta kun kuulee ilosanoman, että merkkijonoja ja lukuarvoja voi katenoida, alkaa tulostusoperaatoiden kirjoittaminen helpottua:

var i = 4321;
println("Muuttujan i arvo on " + i + ".");        // tulostaa Muuttujan i arvo on 4321.

println("Yhtä suurempi arvo on " + (i+1) + ".");  // tulostaa Yhtä suurempi arvo on 4322.
                  //  HUOM: SULUT I:N KASVATUKSEN YMPÄRILLÄ VÄLTTÄMÄTTÖMÄT, SILLÄ
println("Yhtä suurempi arvo on " + i+1 + ".");    // tulostaa Yhtä suurempi arvo on 43211.

println("ahaa " + i + 7);   // ahaa 43217 
println("ahaa " + (i + 7)); // ahaa 4328
println(i + 7 + " ahaa");   // 4328 ahaa

Merkkijonokatenointi on toki käytettävissä myös merkkijonomuuttujien käsittelyssä:

var kissa = "topi";
var hanta = "katti";
var koko  = kissa + hanta;
println(koko);  // tulostaa topikatti

Merkkijonon sisään voi kirjoittaa eräitä ohjausilmauksia, mm.:

println("Merkkijonon sisäänkin saa lainausmerkin: \". Mukavaa.");
println("Sarkain eli tabulointikin onnistuu: \t Kas näin.");
println("Rivinvaihtojakin voi kirjoittaa: \n Tässä yksi. \n\nJa pari lisää.");

Kokonaisluvut

Ohjelmoinnissa – ja tietojenkäsittelytieteessä yleensäkin – laskennaksi kutsutaan melkeinpä mitä tahansa toimintaa, jossa arvoista tavalla tai toisella muokataan uusia arvoja. Tässä luvussa tarkastellaan numeerista laskentaa, aritmetiikkaa.

Operaatiot ovat tuttuja: +, -, * ja /. Erikoisempi operaatio on %, joka on jäännösjako, jakolaskun jakojäännös eli modulo. Myös laskentajärjestys on tuttu: kertolaskut ennen yhteenlaskuja, suluissa olevat ennen muita, ...

var eka = 2;
var toka = 4;
var kolmas = 6;
var arvo = eka + toka * kolmas;
println(arvo);  // tulostaa 26
var sulut = (1+1) + 3 * (2+5)   // 23
var suluitta = 1+1 + 3 * 2+5    // 13

Laskutoimituksia voi toki kirjoittaa myös tulostuslauseeseen

println((1+1) + 3 * (2+5)); // tulostaa 23

println(5 / 2)  // tulostaa 2
println(19 / 5) // tulostaa 3

println(5 % 2)  // tulostaa 1
println(19 % 5) // tulostaa 4

Liukuluvut

Desimaalilukujen (tai reaalilukujen) esittämiseen tietokoneissa käytetään ns. liukulukuja (floating point number). Yksinkertaistaen sanottuna ne esittävät reaaliluvun likiarvon kahtena kokonaislukuna: "merkitsevät numerot" ja "pisteen paikka". Tärkeää on muistaa, että tietokoneen liukuluku on aina likiarvo, ei "tarkka arvo". Asian voisi selittää vaikkapa sillä, että "merkiteseviä numeroita" voi tietokoneessa olla vain äärellinen määrä.

Scalassa muuttujan voi määritellä liukulukutyyppiseksi (mm.) sijoittamalla muuttujan alkuarvoksi desimaaliluvun:

var x = 0;    // x on kokonaislukutyyppinen
var y = 0.0;  // y on liukuluku
println(x + "  " + y) // tulostaa 0  0.0

x = 123;
y = 1.23;
println(x + "  " + y) // tulostaa 123  1.23

// x = y; // error: type mismatch;
// mutta
y = x;
println(x + "  " + y) // tulostaa 123  123.0

Jakolaskuoperaation "/" merkitys – onko kyse kokonaisjakolaskusta vai liukulukujakolaskusta – määräytyy siten, että jos jaettava, jakaja tai molemmat ovat liukulukuja, kyseessä on liukulukujakolasku. Kokonaisjako on kyseessä vain, jos sekä jakaja että jaettava ovat kokonaislukuja:

println(5 / 2)     // 2
println(5.0 / 2)   // 2.5
println(5 / 2.0)   // 2.5
println(5.0 / 2.0) // 2.5

Usein ohjelmoija saattaa haluta kahden kokonaisluvun jakolaskun tulokseksi liukuluvun:

var summa = 55;
var lukumaara = 20;

println(summa/lukumaara);      // 2
println(1.0*summa/lukumaara);  // 2.75

// mutta
println(summa/lukumaara*1.0);  // 2.0

Usein muuttujan uusi arvo lasketaan sen vanhasta vanhasta arvosta:

var pituus = 98;
pituus = pituus + 2;
pituus = pituus - 50;
pituus = pituus * 2;
pituus = pituus / 2;
println(pituus); // 50

Tällaisessa tuiki tavanomaisessa tilanteessa voi käyttää erityisiä muuttavia sijoitusoperaatioita:

var pituus = 98;
pituus += 2;
pituus -= 50;
pituus *= 2;
pituus /= 2;
println(pituus); // 50

Useimmiten on kuitenkin selkeämpää kirjoittaa sijoitusoperaatiot tutummassa muodossa!

Syöttötietojen lukeminen

Scala kielellä syötteiden lukeminen on vaivatonta:

var nimi = "";   // tyhjä merkkijono
var ika = 0;     // kokonaisluku
var pituus = 0.0 // liukuluku

println("Mikä on nimesi?");
nimi = readLine;            // merkkijono
print("Ja ikäsi: ");
ika = readInt;              // kokonaisluku
print("Entä pituutesi? ");
pituus = readDouble;        // liukuluku

println("Moi " + nimi +"!");
print("Tiedän että olet " + ika + "-vuotias ");
println("ja että olet " + pituus + " senttiä pitkä.");
println("Enko olekin viisas?");

Käyttöesimerkki:

Mikä on nimesi?
Violetta
Ja ikäsi: 20
Entä pituutesi? 167.8
Moi Violetta!
Tiedän että olet 20-vuotias ja että olet 167.8 senttiä pitkä.
Enko olekin viisas?

Jos ohjelmalle syöttää väärän tyyppistä tietoa, ohjelman suoritus päättyy virheeseen. Mutta ei välitetä siitä vielä...

Valinta – if-lause

Usein algoritmin halutaan toimivan vaihtoehtoisilla tavoilla riippuen esimerkiksi syötteiden arvosta. Valintalauseella ohjataan algoritmin toimintaa tällaisessa tilanteessa. Scalan, Javan ja monen muun kielen valintalause on ns. if-lause:

println("Anna kokonaisluku!");
var luku = readInt;

if (luku > 0)
  println("Luku on positiivinen.");

If-lauseen ehto (luku > 0) on väittämä algoritmin tilasta. Kun algoritmi etenee tuon ehdon laskentaan, tutkitaan juuri sillä hetkellä, sattuuko muuttujan arvo olemaan positiivinen. Jos sattuu, suoritetaan if-lauseen alilause println("Luku on positiivinen."). Tällöin ehdon totuusarvo on true. Jos ehto ei ole tosi eli jos väittämä ei pidä paikkaansa (false), alilausetta ei suoriteta.

If-lauseeseen voi liittää myös else-osan, jos haluaa jotakin tehtäväksi myös tilanteessa, jossa ehto on epätosi:

println("Anna kokonaisluku!");
var luku = readInt;

if (luku > 0)
  println("Luku on positiivinen.");
else
  println("Luku ei ole positiivinen.");

If- lauseita myös ketjutetaan melko usein:

println("Anna kokonaisluku!");
var luku = readInt;

if (luku > 0)
  println("Luku on positiivinen.");
else if (luku < 0)
  println("Luku on negatiivinen.");
else
  println("Ahaa, luku on nolla."); // ei ollut positiivinen eikä negatiivinen...

Loogisia lausekkeita: vertailuoperaatiot

Loogisilla lausekkeilla esitetään väitteitä ohjelman tilasta eli ohjelman muuttujien arvoista. Väitteiden totuusarvoa - totta tai ei totta - käytetään ohjaamaan algoritmin suoritusta. Loogisia lausekkeita (eli totuusarvoisia lausekkeita) muodostetaan tavallisimmin erilaisista vertailuista, joita mahdollisesti liitetään toisiinsa loogisilla operaatioilla.

Tavallisimpia vertailuoperaatioita:

Kaikki vertailut tuottavat totuusarvon true tai false.

On syytä pitää mielessa sijoitusoperaation "=" ja vertailuoperaation "==" ero:

i = 1;  // "i saa arvokseen yksi"
i == 1  // "onko i:n arvo yksi"

Suuremmuutta tai pienemmyyttä tutkivilla operaatioilla voi vertailla vain numeerisia lausekkeita, yhtäsuuruus ja erisuuruus ovat käytettävissä monien muidenkin arvojen vertailussa.

Esimerkki: Olkoon ohjelmassa vaikkapa seuraavat määrittelyt:

var i = 7;
var d = 3.14;
var s = "hupsista";
var b = false;

Tässä tilanteessa looginen lauseke

(i < 10)     on arvoltaan true
(d > 10.0)   on arvoltaan false
(8 == (i+1)) on arvoltaan true
(b != true)  on arvoltaan true
(s != "hupsista")  on arvoltaan false
(s == "hupsista")  on arvoltaan true

Huom: Liukulukujen (so. desimaalilukujen) vertailussa ei ole syytä käyttää yhtäsuuruusvertailuja (== ja !=), koska ns. pyöristysvirheiden takia yhtäsuuruusvertailut voivat antaa odottamattomia tuloksia. Tällaisessa tilanteessa on parempi tutkia lukujen erotuksen itseisarvon pienuutta.

Huom: String-arvojen vertailu ei sitten aikanaan Javalla onnistu yhtä vaivattomasti...

Loogisia lausekkeita: loogiset operaatiot

Usein on tarpeen liittää algoritmin tilaa koskevia väittämiä yhteen monimutkaisemmiksi väittämiksi. Käytössä on mm. seuraavat loogiset operaatiot:

Näiden operaatioiden operandit eli laskettavat voivat olla vain totuusarvoisia lausekkeita, tavallisimmin ne ovat vertailuja. Operaatioiden ehdollisuus tarkoittaa sitä, että operaation jälkimmäinen laskettava lasketaan vain, jos totuusarvo ei selviä jo ensimmäisestä!

println("Anna luku väliltä 5-10: ");
var luku = readInt;

if ( 5 <= luku && luku <= 10 )  // HUOM: ILMAUS (5 <= luku <= 10) EI OLE LUVALLINEN!
  println("Oikein!");
else
  println("Luku ei kelpaa.")
println("Valitse joko 1, 50 tai 100: ");
var vastaus = readInt;

if ( vastaus == 1 || vastaus == 50 || vastaus == 100 )
  println("Valitsit ohjeen mukaisesti.");
else
  println("Valitsit väärin.");

Loogisien operaatioiden totuustaulukko (a ja b voivat itse olla mitä tahansa loogisia lausekkeita, esimerkiksi vertailuja) (tuo ^ on ns. poissulkeva tai, xor):

 a    |   b   ||  a && b  |  a || b  |  a ^ b   |   !a 
--------------------------------------------------------
true  | true  ||  true    |  true    |  false   |  false
true  | false ||  false   |  true    |  true    |  false
false | true  ||  false   |  true    |  true    |  true
false | false ||  false   |  false   |  false   |  true

Lohko

Hyvin usein if-lauseen alilauseeksi if-osaan tai else-osaan halutaan useampi kuin yksi lause. Tällöin nuo lauseet kootaan yhdeksi lauseeksi, lohkoksi, aaltosulkeiden "{" ja "}" väliin:

println("Anna kokonaisluku!");
var luku = readInt;

if (luku > 0) {
  println("Luku on positiivinen.");
  println("Kaksinkertaisena se on " + 2*luku);
}
else {
  println("Luku ei ole positiivinen.");
  println("Mutta itsellään kerrottuna se ei ainakaan ole negatiivinen: " + luku*luku);
}

Huomaa että nuo aaltosulkeet ovat välttämättömät! Pelkkä sisentäminen ei riitä. Seuraava on siis väärin:

println("Anna kokonaisluku!");
var luku = readInt;

if (luku > 0)                          // TÄMÄ ON VIRHEELLINEN!
  println("Luku on positiivinen.");    // AALTOSULUT PUUTTUVAT!
  println("Kaksinkertaisena se on " + 2*luku);
else 
  println("Luku ei ole positiivinen.");
  println("Mutta itsellään kerrottuna se ei ainakaan ole negatiivinen: " + luku*luku);

Jotkut suosivat seuraava ulkoasua:

println("Anna kokonaisluku!");
var luku = readInt;

if (luku > 0) 
{
  println("Luku on positiivinen.");
  println("Kaksinkertaisena se on " + 2*luku);
}
else 
{
  println("Luku ei ole positiivinen.");
  println("Mutta itsellään kerrottuna se ei ainakaan ole negatiivinen: " + luku*luku);
}

Myös seuraavaa else-layout on mahdollinen:

if (luku > 0) {
  ...
} else {
  ...
}

Useimmissa ohjelmointikielissä (mm. Scala ja Java) ohjelman ulkoasulla ei ole kääntäjälle viestiä. Niinpä kääntäjälle kelpaa jopa seuraava sotku — sinänsä täysin toimiva ohjelma:

println("Anna kokonaisluku!"); var luku = readInt;if (luku > 
0) {println("Luku on positiivinen.");println("Kaksinkertaisena se on " 
+ 2*luku);}else{println("Luku ei ole positiivinen.");println(
"Mutta itsellään kerrottuna se ei ainakaan ole negatiivinen: "+ luku*luku);}

Mutta ohjelmoijalle ohjelman selkeä ja johdonmukainen ulkoasu on välttämätön! Miksi?

Ehdollinen toisto – while-lause

Samaan tapaan kuin väittämät algorimin tilasta ohjaavat valintaa if-lauseissa, väittämiä käytetään myös ohjaamaan algoritmin jonkin osan toistamista.

Ehdollinen toistolause, while-lause, toistaa alilausettaan yhä uudelleen ja uudelleen kunnes toistoehto ei enää ole totta. Toki on mahdollista, että ehto alkujaankin on epätosi, jolloin toistoja ei ole ainuttakaan:

println("Montako onnentoivotusta haluat?");
var lukumaara = readInt;

while (lukumaara > 0) {
  println("Onnea!");
  lukumaara = lukumaara - 1;
}

Toistolauseen alussa tutkitaan, onko väittämä (lukumaara > 0) totta. Jos se on, koko toistettava alialgoritmi suoritetaan. Sen jälkeen taas tutkitaan, onko ehto voimassa, jne... Huomaa että toistettavan alialgoritmin on syytä vaikuttaa jotenkin toistoehdon arvoon! Miksi?

Toinen esimerkki: Vaaditaan että käyttäjä syöttää parittoman luvun. Luku on pariton, jos se ei ole parillinen. Luku on parillinen, jos jakojäännös kahdella jaettaessa on nolla:

println("Anna pariton luku.");
var luku = readInt;

while (luku % 2 == 0) {
  println("Eihän luku " + luku + " ole pariton!");
  println("Yritä uudelleen");
  luku = readInt;
}

println("Kyllä! " + luku + " todella on pariton.");

Käyttöesimerkki:

Anna pariton luku.
44
Eihän luku 44 ole pariton!
Yritä uudelleen
-128
Eihän luku -128 ole pariton!
Yritä uudelleen
235
Kyllä! 235 todella on pariton.

Taulukko

Tähän mennessä tavatut muuttujat ovat olleet yksittäisiä "laatikoita, joihin mahtuu vain yksi arvo kerrallaan". Tällaisilla saa toki ohjelmoitua kaikki mahdolliset algoritmit. Mutta työlääksi ja tylsäksi se voi käydä...

Esimerkki: Laadi ohjelma, joka lukee viisi lukua ja tulostaa ne käänteisessä järjestyksessä:

var eka=0;
var toka=0;
var kolmas=0;
var neljas=0;
var viides=0;

print("Anna 1. luku: ");
eka = readInt;
print("Anna 2. luku: ");
toka = readInt;
print("Anna 3. luku: ");
kolmas = readInt;
print("Anna 4. luku: ");
neljas = readInt;
print("Anna 5. luku: ");
viides = readInt;

println("Luvut käänteisessä järjestyksessä:");

println(viides);
println(neljas);
println(kolmas);
println(toka);
println(eka);

Kyllähän homma siis onnistuu, mutta vaikkapa jo sadan luvun tapauksessa ohjelmoija joutuisi näkemään melkoisesti vaivaa. Puhumattakaan kymmennetuhannen tai miljoonan luvun tapauksesta. Ja aivan turhaan!

Taulukko (array) on muuttuja, joka sisältää monta "lokeroa", joihin jokaiseen voidaan tallettaa yksi arvo. Taulukko on siis ikään kuin monen muuttujan "lokerikko".

Ja nyt tulee koko homman idis: Taulukkomuuttujan yksittäiseen lokeroon voidaan viitata lokeron järjestysnumerolla! Ja tuo järjestysnumero voi olla muuttujan arvona. Muista, että muuttujan arvoa voi muuttaa!

Scalassa, Javassa ja monessa muussa ohjelmointikielessä taulukkomuuttujan lokerot on numeroitu nollasta alkaen. Lokeroiden numeroita on tapana kutsua indekseiksi ja lokeroita taulukon alkioiksi.

Äskeisen esimerkin voi taulukkoa käyttäen ohjelmoida vaikkapa seuraavasti:

var luvut = new Array[Int](5);  // tämä selitetään kohta...

var lokeronNumero = 0;

while (lokeronNumero < 5) {
  print("Anna " + (lokeronNumero+1) + ". luku: ");  // pyydetään 1., 2., ..., siksi tuo +1
  luvut(lokeronNumero) = readInt;
  lokeronNumero = lokeronNumero +1;
}

println("Luvut käänteisessä järjestyksessä:");

lokeronNumero = 4;
while (lokeronNumero >= 0) {
  println(luvut(lokeronNumero));
  lokeronNumero = lokeronNumero - 1;
}

Yllä ilmaus new Array[Int](5) on Scala-kielen tapa määritellä taulukkomuuttuja, jonka alkiot ovat tyypiltään kokonaislukuja ja jonka koko eli alkioiden lukumäärä on viisi. Java-kielessä taulukko määritellään hieman eri tavalla, mutta merkitys on sama.

Ilmaus luvut(lokeronNumero) on taulukon indeksointi; se tarkoittaa taulukkomuuttujan alkiota, jonka järjestysnumero on lokeronNumero. Jos muuttujan arvoa vastaavaa alkiota ei taulukossa ole, ohjelman suoritus keskeytyy virheeseen. Kelvollinen indeksi on siis ohjelmoijan vastuulla.

Taulukon pituuden voi tarkistaa ilmauksella luvut.length:

var t1 = new Array[Int](5);
var t2 = new Array[Double](500);
var t3 = new Array[int](1000000);

println(t1.length); // 5
println(t2.length); // 500
println(t3.length); // 1000000

Melkein kaikissa ohjelmointikielissä taulukkoa indeksoidaan hakasulkeilla "[" ja "]" tyyliin luvut[lokeronNumero], mutta Scalassa käytetään tavallisia kaarisulkeita.

Askeltava toisto – for-lause

Useimmissa ohjelmointikielissä on erityinen lause, joka soveltuu erityisen hyvin taulukon alkioiden läpikäymiseen. Monissa kielissä se on nimeltään for-lause. Niin myös Scalassa:

for (i <- 5 to 10)
  print(i + " ");  // 5 6 7 8 9 10

println(); // rivinvaihto

for (i <- (5 to 10).reverse)
  print(i + " ");  // 10 9 8 7 6 5

println(); // rivinvaihto

Viimeistellään ja yleistetään luvun alun esimerkki, jossa syöttöluvut tulostetaan käänteisessä järjestyksessä:

println("Montako lukua haluat tulostaa käännetyssä järjestyksessä?");
var koko = readInt;

var luvut = new Array[Int](koko);  // HUOM: taulukon koko saa olla siis myös muuttuja!

for (lokeronNumero <- 0 to luvut.length-1) {
  print("Anna " + (lokeronNumero+1) + ". luku: ");
  luvut(lokeronNumero) = readInt;
}

println("Luvut käänteisessä järjestyksessä:");

for (lokeronNumero <- (0 to luvut.length-1).reverse)  // arvot luvut.length-1, luvut.length-2, ..., 0
  println(luvut(lokeronNumero));

Kolme tyypillistä tapaa lukea vaihteleva määrä syötteitä

Vaihtelevaan määrään syöttötietoja voidaan varautua eri tavoin. Tarkastellaan esimerkkinä ohjelmaa, joka laskee syötteinä saamiensa kokonaislukujen summan. Tehdään ohjelmasta kolme erilaista versiota:

Merkkijonotaulukot

Jo aiemmin nähtiin esimerkki merkkijonomuuttujasta:

var teksti = "juttuhomma";
println(teksti);
teksti = "uusjuttu";
println(teksti);

Myös merkkijonot kelpaavat taulukon alkioiksi. Tällöin alkiotyypin nimi on String.:

var nimet = new Array[String](3);

nimet(0) = "Aku";
nimet(1) = "Iines";
nimet(2) = "Hessu";

for (indeksi <- 0 to nimet.length-1)
  println(nimet(indeksi));

Taulukon alkuarvojen luetteleminen

Taulukon voi määritellä myös luettelemalla alkioiden alkuarvot seuraavaan tapaan:

var henkilot = Array("Matti", "Maija", "Pekka", "Liisa");
var ika      = Array(     29,      22,     19,       27);

println("Kenen henkilön iän haluat tietää?");
var kuka = readLine;

// etsitään indeksi:

var indeksi = 0;

while (indeksi < henkilot.length && henkilot(indeksi) != kuka)
  indeksi = indeksi + 1;

// Nyt joko löytyi tai indeksi kasvoi yli:

if ( indeksi == henkilot.length )
  println("Henkilöä " + kuka + " ei löytynyt.")
else {
  var haetunIka = ika(indeksi);
  println("Henkilön " + kuka + " ikä on " + haetunIka + ".");
}

Tässä esimerkissä on paljon muutakin opittavaa kuin tuo taulukon määrittely alkuarvot luettelemalla. Mitä? Mieti, tutki ja opi!

Nimetty alialgoritmi

Ohjelmia laadittaessa esiintyy usein tilanne, että jokin toiminto - alialgoritmi - täytyy suorittaa ohjelman useassa eri kohdassa. Alialgoritmi voidaan tietenkin kirjoittaa uudelleen ja uudelleen, mutta tällä on joitakin huonoja seurauksia:

Jo varhaisissa ohjelmointikielissä käytettiin aliohjelmia, nimettyjä algoritmeja, jotka ensin määritellään ja nimetään. Tällaista nimettyä apuvälinettä voidaan sitten nimellä kutsua siellä missä tuota algoritmia tarvitaan, esimerkiksi pääohjelmassa tai jossakin aliohjelmassa. Kutsuminen tarkoittaa siis, että aliohjelma käydään suorittamassa ja suorituksen jälkeen automaattisesti palataan kutsua seuraavaan ohjelmakohtaan.

Mahdollisuus nimetä ja kutsua alialgoritmeja on jo sinänsä arvokasta. Näin voidaan välttyä "koodin kertaamiselta" ja samalla selkeyttää ohjelmaa. Tärkeä lisäominaisuus on mahdollisuus antaa aliohjelmille lähtötietoja, ns. parametreja, jotka ohjaavat alialgoritmin suoritusta.

Nimetyt alialgoritmit voivat olla "komentoja tehdä jotakin" samaan tapaan kuin vaikkapa println osaa kirjoittaa parametrinsa ohjelman käyttäjän kuvaruudulle.

Alialgoritmeja voi kirjoittaa myös sellaisiksi, että ne laskevat jonkin arvon ja palauttavat tuon arvon. Esimerkkejä tällaisista ovat vaikkapa trigonometriset funktiot, jotka useimmista ohjelmointikielistäkin löytyvät.

Vaikka vain jälkimmäiset muistuttavat matematiikan funktioita, eräissä ohjelmointikielissä myös komennon kaltaisia aliohjelmia kutsutaan "funktioiksi". Java-maailmassa nimettyjä aliohjelmia tavataan kutsua "metodeiksi", koska niillä on tärkeä merkitys olio-ohjelmoinnissa. Tästä lisää myöhemmin.

Nimetyt aliohjelmat olkoot toistaiseksi "funktioita".

Funktion määrittely ja kutsu

Funktio määritellään def-määreellä. Ensin esimerkki "komennon kaltaisesta" aliohjelmasta. Funktion nimen tarkoittama algoritmi kirjoitetaan alltosulkeiden väliin:

def tervehdi() {   // määrittely
  println("Moi!");
}

println("Kutsutaanpa tervehdystä:")
tervehdi();  // kutsu

println("Kutsutaanpa kolmasti:");
tervehdi(); tervehdi(); tervehdi();

Funktion määrittely ei vielä johda sen algoritmin suorittamiseen. Vasta kutsu – nimi mainiten – johtaa algoritmin suorittamiseen.

Arvon palauttava aliohjelma määritellään samaan tapaan, mutta määrittelyssä käytetään (sinänsä luontevasti) yhtäsuuruusmerkkiä funktion nimen ja merkityksen välissä. Palautettava arvo ilmaistaan return-lauseella. Lisäksi myös funktion paluuarvon tyyppi pitää mainita esimerkin mukaisesti. Tässä vaiheessa käytämme paluuarvojen tyyppeinä vain kolmea Int, Double ja String.

def onnenluku():Int =  {   // määrittely
  return 14;
}

println("Onnenluku = " + onnenluku());  // kutsu

println("Ja kolmeen kertaan: " + (onnenluku() + onnenluku() + onnenluku()));

Huom: Näissä kahdessa esimerkissä funktioilla ei ole parametreja. Silti sekä määrittelyssä että kutsussa niiden nimen yhteyteen kirjoitettiin "tyhjät" kaarisulut. Scalassa tämä ei olisi välttämätöntä, mutta koska Java ne vaatii, niitä on hyvä käyttää jo nyt.

Parametrit

Funktiolle siis voi antaa lähtötietoja parametrien avulla. Idea on seuraava:

Yleistetty tervehtimisalgoritmi ja sitä esittelevä ohjelma kokonaisuudessaan:

def tervehdi(kuka: String) {   // määrittely
  println("Moi " + kuka + "!");
}

println("Kutsutaanpa tervehdystä:")
tervehdi("Aku");  // kutsu

println("Kutsutaanpa kolmasti:");
tervehdi("Iines"); tervehdi("Minni"); tervehdi("Mikki");

var i = 7;
var nimi = "Jaska Jokunen";
tervehdi(nimi + ", lukusi on " + (i+2));  // tulostus: Moi Jaska Jokunen, lukusi on 9!

Tietenkin myös arvon palauttavalle aliohjelmalle voi antaa parametreja:

def tuplaa(luku: Int):Int =  {   // määrittely
  return 2 * luku;
}

println("Kolme tuplana = " + tuplaa(3));  // kutsu

var i = 8;
var j = tuplaa(i);

println( tuplaa(j) + tuplaa(2) );
println(tuplaa(tuplaa(tuplaa(1))));

Parametreja voi toki olla useampiakin:

def onnittele(kuka: String, kertaa: Int) {
  for (i <- 1 to kertaa)
    println( "Onnea " + kuka + "!");
}

onnittele("Liisa", 3);
onnittele("Pekka", 2);

def neliosumma(a: Int, b: Int): Int = {
  var summa = a + b;
  var nelio = summa * summa;
  return nelio;
}

println(neliosumma(1, 1));
println(neliosumma(3, 2));

Muuttujat funktion sisällä

Funktion eli aliohjelman sisällä määritellyt muuttujat eivät koskaan näy funktion oman lohkon ulkopuolelle. Tällaisia muuttujia kutsutaan funktion paikallisiksi muuttujiksi. Ne ovat käytettävissä vain metodin omassa algoritmissa.

Funktion paikalliset muuttujat ja muodolliset parametrit itse asiassa syntyvät, kun funktiota kutsutaan. Ja näille varattu tila vapautetaan, kun funktion suoritus päättyy. Koska paikalliset muuttujat eivät säily suorituskerrasta toiseen, niiden avulla myöskään "ei voi muistaa mitään" seuraavalle suorituskerralle.

Java-kielessä pätee sääntö: Mistään aliohjelmasta ei koskaan voi nähdä minkään toisen aliohjelman paikallista muuttujaa. Scalassa asia ei ole ihan näin yksinkertainen, mutta siitä ei tällä kurssilla tarvitse tietää mitään.

Taulukot ja funktiot

Myös taulukon voi välittää parametrina, mikä usein on näppärää:

def asetaJokaAlkiolle(taulu: Array[Int], arvo: Int) {
  for (i <- 0 to taulu.length-1)
    taulu(i) = arvo
}

var t = new Array[Int](10);
println(t(7));  // 0

asetaJokaAlkiolle(t, 100);
println(t(7));  // 100

Esimerkki peräkkäishausta: funktio etsii taulukosta yksi kerrallaan kysyttyä arvoa ja palauttaa vastaavan indeksin ensimmäisestä löytyneestä. Jos päästään talukon loppuun löytämättä haettua arvoa, palautetaan arvo -1, joka ei kelpaa taulukon indeksiksi. Se viestii, ettei haettua arvoa löytynyt:

def mikaOnArvonIndeksi(etsittava: Int, taulu: Array[Int]): Int = {
  for (i <- 0 to taulu.length-1)
    if (taulu(i) == etsittava)
      return i;
  // jos tänne päästään, ei löytynyt
  return -1;   // -1 ei voi olla indeksi
}

var t = Array(2, 4, 6, 8, 10, 12, 14);

println("Luvun 8 indeksi on " + mikaOnArvonIndeksi(8, t));
println("Luvun 7 indeksi on " + mikaOnArvonIndeksi(7, t));

if (mikaOnArvonIndeksi(8, t) != -1)
  println("8 löytyy");
else
  println("8 ei löydy");

if (mikaOnArvonIndeksi(7, t) != -1)
  println("7 löytyy");
else
  println("7 ei löydy");