Tämä kurssi on jo päättynyt.

Kurssin viimeisimmän version löydät täältä: O1: 2024

Luku 1.4: Arvojen tallentaminen muuttujiin

Tästä sivusta:

Pääkysymyksiä: Miten pääsen kätevästi käsiksi ohjelmassa tarvitsemiini arvoihin? Miten voin tallentaa tietoa tietokoneen muistiin?

Mitä käsitellään? Muuttujat (val ja var). Ensisilmäys laatuseikkoihin ohjelmoinnissa. Samalla opitaan myös vähän lisää edellisen luvun aiheista, erityisesti merkkijonoista.

Mitä tehdään? Ohjelmoidaan REPLissä ja luetaan.

Suuntaa antava työläysarvio:? Reilu tunti.

Pistearvo: A40.

Oheismoduulit: Ei ole.

../_images/sound_icon.png

Muuta: Eräissä tämän luvun kohdissa on kaiuttimista tai kuulokkeista hyötyä. Aivan pakolliset ne eivät ole.

../_images/person01.png

Johdanto: välitulosongelma

Miten käyttäisit edellisestä luvusta tuttuja aritmeettisia lausekkeita laskeaksesi Scalalla seuraavat?

  • kuutosen kuution, siis 63
  • kuutosen kertoman, siis 6!
  • kuutosen kertoman kuution, siis 6!3

Eräs ratkaisu REPLissä on annettu alla.

6 * 6 * 6res0: Int = 216
1 * 2 * 3 * 4 * 5 * 6res1: Int = 720

Kaksi ensimmäistä — kuutosen kuutio ja kuutosen kertoma — menivät ihan siedettävästi näinkin. Entä kolmas?

Aargh:

1 * 2 * 3 * 4 * 5 * 6 * 1 * 2 * 3 * 4 * 5 * 6 * 1 * 2 * 3 * 4 * 5 * 6res2: Int = 373248000

Tämä koodinpätkä on jo aika epämiellyttävä kirjoittaa ja lukea. Lisäksi kone joutuu nyt tekemään turhankin monta kertolaskua, kun se suorittaa käskyn (millä ei tosin tässä ole käytännön merkitystä).

Olisi parempi, jos voisi kirjoittaa: "Laske kertoma, ota välitulos talteen ja laske sen kuutio." Ja niin voikin.

Muuttujat

Lähes kaikissa ohjelmissa on tarvetta arvojen tallentamiselle tietokoneen muistiin. Näin voidaan pitää kirjaa ohjelman kannalta oleellisista asioista, kuten välituloksista tai vaikkapa GoodStuff-ohjelman tapauksessa käyttäjän kokemusten saamista arvosanoista ja hinnoista.

Tietysti tallennettuihin arvoihin pitäisi päästä myös jotenkin käsiksi. Parhaiten tämä onnistuu käyttämällä nimiä, joilla arvoihin voi viitata.

Arvojen tallentamiseen käytetään ohjelmoinnissa muuttujia (variable). Muuttuja on nimetty varastointipaikka yhdelle arvolle. Käskyä, jolla arvo tallennetaan muuttujaan, sanotaan sijoitukseksi (assignment).

Välituloksen sijoittaminen muuttujaan

Tässä parempi tapa ratkaista "kuutosen kertoman kuutio" -ongelma.

Määritellään ensin muuttuja, johon otetaan välitulos talteen. Se onnistuu tällaisella käskyllä. Muista taas, että voit siirtää hiiren kursorin vihreiden laatikkojen ylle nähdäksesi, mihin selitykset liittyvät.

val kertoma = 1 * 2 * 3 * 4 * 5 * 6
Käytetään avainsanaa val (lyhennetty englannin ilmaisusta value variable), jonka perään kirjoitetaan...
... ohjelmoijan valitsema muuttujan nimi, tässä kertoma, jonka perään tulee yhtäsuuruusmerkki...
... ja viimeisenä lauseke, jonka evaluointi tuottaa muuttujaan sijoitettavan arvon.

REPL vastaa muuttujan määrittelyyn näin:

val kertoma = 1 * 2 * 3 * 4 * 5 * 6kertoma: Int = 720
REPL kuittaa onnistuneen muuttujamäärittelyn laittamalla näkyviin määritellyn muuttujan nimen (automaattisesti numeroidun resX:n sijaan) sekä...
... muuttujan tietotyypin ja muuttujaan tallennetun arvon. Scalassa sekä muuttujilla että arvoilla on tietotyypit, ja muuttujan tietotyypin on oltava yhteensopiva muuttujaan tallennetun arvon tietotyypin kanssa.

Nyt kertoman kuution voi laskea muuttujan avulla:

kertoma * kertoma * kertomares3: Int = 373248000

Huomaa, että muuttujan nimi yksinäänkin toimii lausekkeena. Tuon lausekkeen arvo on kyseiseen muuttujaan tallennettu arvo. Muuttujan nimeä, kuten muitakin lausekkeita, voi käyttää suurempien lausekkeiden muodostamiseen. Esimerkiksi tässä muuttujan nimeä on käytetty (kolmeenkin kertaan) osalausekkeena aritmeettisessa lausekkeessa.

Muuttujaan sijoittamisen vaiheet

Alla on vielä animaatio yllä kuvailtujen koodirivien suorituksesta. Katso se, vaikka olisitkin jo mielestäsi tajunnut yllä olevan esimerkin! Kiinnitä erityistä huomiota järjestykseen, jossa suoritus etenee vaiheittain. Näiden vaiheiden järjestys on keskeinen, kun käsittelemämme ohjelmat mutkistuvat.

Arvioi äskeisen animaation perusteella, mitkä kaikki seuraavista väittämistä pitävät paikkansa, ja valitse ne.

Varo tutunlaista matikkaa!

Ohjelmakoodi joiltain osin muistuttaa tuttuja matemaattisia merkintätapoja. Aivan samasta asiasta ei kuitenkaan ole kyse, ja samankaltaisuus on harhauttanut monta ohjelmoinnin aloittelijaa.

Esimerkki tästä on sijoituskäskyssä. Se ei ole yhtälö, jossa yhtäsuuruusmerkin molemmat puolet ovat samanarvoisessa asemassa. Sijoituskäsky ohjeistaa tietokonetta sijoittamaan oikealla olevan lausekkeen arvon vasemmalla puolella nimettyyn muistipaikkaan. Esimerkiksi käsky-yritelmä 1 * 2 * 3 * 4 * 5 * 6 = val kertoma ei siis toimi lainkaan vaan tuottaa virheilmoituksen.

Koodin laatu parani

Tässä kaksi edellä laatimaamme ohjelmaa:

1 * 2 * 3 * 4 * 5 * 6 * 1 * 2 * 3 * 4 * 5 * 6 * 1 * 2 * 3 * 4 * 5 * 6res4: Int = 373248000
val kertoma = 1 * 2 * 3 * 4 * 5 * 6
kertoma * kertoma * kertomares5: Int = 373248000

Uusi muuttujaa käyttävä versiomme ohjelmakoodista ratkaisee aivan saman ongelman kuin alkuperäinen yksirivinen ratkaisukin. Kuitenkin uusi koodimme on ihmiselle selkeämpi lukea. Toinen parannus on (periaatteessa) se, että kone joutuu laskemaan hieman vähemmän kuin edellisessä versiossa. Tämä oli siis ensisilmäys kahteen ohjelman laatukriteeriin: tyyliin ja suoritustehokkuuteen.

Parannetussa koodissamme voi ainakin periaatteessa nähdä jo kolmannenkin laatuparannuksen: vähensimme saman ilmaisun toistoa eriyttämällä kertoman laskemisen omaksi toimenpiteekseen. Toiston vähentäminen helpottaa ohjelman jatkokehittämistä ja muuntamista. Esimerkiksi jos haluaisit muokata ohjelman laskemaan vaikkapa luvun seitsemän kertoman kuution, niin muutos on helpompi tehdä muuttujaan perustuvaan toiseen ratkaisuumme kuin alkuperäiseen versioon: sinun ei tarvitse muuttaa kuin yhtä kohtaa koodista eikä kolmea. Tämä ei ainoastaan vähennä vaivaa vaan myös huolimattomuusvirheiden mahdollisuutta. Tässä erittäin pienessä ohjelmassamme tämäkin parannus on toki käytännön merkitykseltään vähäinen.

Koodin toisteisuuden välttämisen periaate kulkee nimellä DRY (sanoista don’t repeat yourself); jotkut käyttävät periaatteen rikkomisesta ilmaisua WETWET (write everything twice write everything twice). Isommissa ohjelmissa DRY-periaatetta on tärkeä noudattaa, ja siihen palaamme kurssilla myöhemmin. Tässä vaiheessa riittää saada muhimaan tausta-ajatus siitä, että ohjelmoijan kuuluu arvioida pelkän toimivuuden lisäksi myös laatuseikkoja.

Muuttujien nimet lausekkeina ja lausekkeissa

Muuttujaan sijoitettavan arvon voi määritellä erilaisilla lausekkeilla. Yksinkertaisimmat lausekkeet ovat literaaleja, ja myös literaalin arvon voi sijoittaa muuttujaan:

val kokeilu = 100kokeilu: Int = 100

Tutustu muuttujien käyttöön ohjelmoimalla REPLissä ja vastaa oheisiin helppoihin kysymyksiin.

Muuttujaa voi käyttää toisen muuttujan alustamiseen samoin kuin missä vain muussa lausekkeessa. Luo ensin kokeilu-muuttuja kuten yllä ja suorita sitten tämä käsky:

val toinen = 1 + kokeilu

Mikä on nyt toinen-muuttujan arvo?

Muuttujan nimi yksinäänkin on lauseke:

toinen

Mikä on tämän lausekkeen arvo? (Oletetaan, että muuttuja on alustettu kuten edellä.)

Muuttujan sisältämän arvon voi kopioida toiseen:

val kolmas = toinen

Huomaa tässäkin tapauksessa, että sijoitus tapahtuu aina "yhtäsuuruusmerkin oikealta puolelta vasemmalle": oikeanpuoleisena mainitun muuttujan on oltava ennestään olemassa (tai tulee virheilmoitus); vasemmanpuoleinen val-sanan perässä mainittu muuttuja luodaan tällä käskyllä.

Mikä arvo on yllä olevan käskyn suorittamisen jälkeen muuttujassa kolmas?

Muuttujia voi käyttää myös silloin, kun määritellään parametreja käskyille. Tässä esimerkki tulostuskäskystä, jonka parametriarvon määräävässä lausekkeessa on käytetty kahta muuttujaa:

println(kokeilu - kolmas)

Minkä arvon tämä käsky tulostaa?

Tarkastellaan seuraavan käskyn antamista REPLissä:

val joku = kolmas * (kokeilu + 10)

Mitkä kaikki seuraavista väittämistä pitävät paikkansa?

Erityyppisiä muuttujia

Kaikki yllä käytetyt muuttujat olivat Int-tyyppisiä, ja niillä oli arvoinaan kokonaislukuja. Voimme määritellä myös muuntyyppisiä muuttujia, mikä onnistuu yksinkertaisimmin sijoittamalla muuttujaan toisentyyppinen arvo, vaikkapa Double-tyyppinen luku:

val arvosana = 9.5arvosana: Double = 9.5

Tai merkkijono:

val nimi = "Satu"nimi: String = Satu
val nooaAlku = "cccedddfeeddc---"nooaAlku: String = cccedddfeeddc---

Tai väri tai kuva, kuten seuraavassa tehtävässä.

Määritellään REPLissä pari muuttujaa ja annetaan niille jotkin arvot:

val ympyranKoko = 300ympyranKoko: Int = 300
val ympyranVari = BlueympyranVari: o1.gui.Color = Blue

Koetetaan näyttää ympyrä, jolla on noiden muuttujien mukainen halkaisija ja väri:

show(ympyranKuva)<console>:18: error: not found: value ympyranKuva
      show(ympyranKuva)
           ^

Jotain unohtui välistä! Virheilmoitus kertoo, että muuttujaa ympyranKuva, jota yritimme käyttää, ei ole määritelty. Eikä olekaan.

Kirjoita alle käsky, joka määrittelee muuttujan ympyranKuva niin, että äskeinen show-käsky voidaan antaa ja tuo esiin ympyrän. Käytä apuna luvussa 1.3 kohdattua circle-käskyä sekä jo määriteltyjä muuttujia ympyranKoko ja ympyranVari. (Älä siis syötä alle tuota show-käskyä vaan vain muuttujan määrittely.)

Muuttujien nimeämisestä

Ohjelmoija valitsee muuttujien nimet eli virallisemmin sanoen niiden tunnukset (identifier). Kuten edeltä on käynyt ilmi, Scala-kieltä käytettäessä muuttujien nimet aloitetaan yleensä pienellä kirjaimella. Mitään teknistä pakotetta pienen alkukirjaimen käyttöön ei ole, mutta tämän tavan noudattaminen kuuluu hyvään Scala-ohjelmointityyliin.

../_images/camelCase.png

Jos muuttujan nimessä on useita sanoja, jäljempien sanojen aluissa on tapana käyttää isoja kirjaimia. Tässä esimerkkejä:

  • munHienoMuuttuja
  • numberOfPlayers
  • xCoordinate

Tämä Scalassa käytetty nimeämistapa, joka on ohjelmoinnissa melko yleinen muttei mitenkään ainoa, kulkee nimellä camelCase.

Älä käytä muuttujien nimissä välilyöntejä. Numeroita saa käyttää, mutta nimi ei voi alkaa numerolla. Erikoismerkkejä (+, & jne.) on aloittelijan syytä välttää nimissä kokonaan; joillakin merkeillä on myös erityismerkityksiä Scalassa. Skandinaavisia "ääkkösiä" ja eksoottisempia kirjoitusmerkkejä voi periaatteessa käyttää, mutta koska ne voivat joidenkin ohjelmoijan aputyökalujen kanssa tuottaa ongelmia, niin saattaa olla parempi välttää niitä. Scala-kielen "taikasanoja" kuten val, virallisemmin varattuja sanoja (reserved words), ei voi käyttää muuttujien niminä.

Isot ja pienet kirjaimet lasketaan nimissä eri kirjaimiksi. Kun siis teet muuttujan omaKokeilu, niin ole tarkkana, että kirjoitat nimen aina samassa muodossa. Nimi omakokeilu ei toimi tuon muuttujan käyttämiseen.

Kurssimateriaalin pienimmissä esimerkeissä on muuttujat ja muut ohjelman osat useimmiten nimetty suomeksi, jotta korostuisi, mitkä sanat ovat ohjelmoijan itse valitsemia ja mitkä ovat osa (englannista sanoja lainaavaa) Scala-ohjelmointikieltä. Kurssin isommissa oheismoduuleissa taas on yleensä käytetty englantia. Itse voit tehdä tältä osin niin kuin parhaaksi näet. Yritysten ohjelmistohankkeissa yleensä käytetään englantia, ja monet suomenkielisetkin ohjelmoijat käyttävät englanninkielisiä nimiä myös henkilökohtaisissa projekteissaan.

Hyvään ohjelmointityyliin kuuluu muuttujien nimeäminen niiden käyttötarkoitusta kuvaavasti. Tästä näet paljon esimerkkejä jatkossa. Pikkuisissa kokeilukoodinpätkissä toki voi käyttää yleisluontoisempia ja lyhyempiä nimiä kuten luku, a tai vaikka kokeilu.

Nimeämistä käsitellään myös kurssin tyylioppaassa, johon kannattaa tutustua jossain vaiheessa kurssin alkupuolella, ei kuitenkaan välttämättä vielä tässä ihan alussa.

Muuttujia ja merkkijonoja

Käytätkö IntelliJ’tä Mac-tietokoneella? Lue tämä ennen seuraavaa tehtävää. (Ohita muuten.)

Kurssin oletusarvoiset näppäimistöasetukset, jotka IntelliJ’n A+ Courses -lisäosa asentaa, eivät ole kaikilta osin yhteensopivat Mac-näppäimistöjen kanssa. Asian voi korjata seuraavasti.

Kokeile pystytkö kirjoittamaan REPLissä esim. dollarimerkin $ ja hakasulkeet [] normaalisti. Jos pystyt, kaikki on hyvin. Mikäli ne eivät toimi, valitse käyttöösi Macille sopivammat asetukset. Valikosta: IntelliJ IDEA → Preferences → Keymap ja sitten otsikon alla näkyvästä pudotusvalikosta O1 MacOS Keymap. Paina OK. Nyt pitäisi toimia.

Jos ei onnistunut, lue seuraava:

Mikäli olit hyvissä ajoin liikkeellä ja asensit IntelliJ’n ennen kurssin virallista alkua 9.9.2020, niin (hyvin toimittu, mutta) tuolta ei välttämättä löydy valintaa O1 MacOS Keymap. Siinä tapauksessa toimi näin:

  1. Valitse ylävalikosta A+ → Reset A+ Courses Plugin Settings ja hyväksy.
  2. Valitse samasta valikosta A+ → Turn Project into A+ project ja hyväksy taas ehdotetut toimenpiteet.
  3. Nyt O1 MacOS Keymap pitäisi löytyä asetusvalikosta IntelliJ IDEA → Preferences → Keymap.

(Tulemme parantamaan A+ Courses -lisäosaa niin, että tuo käy automaattisesti, mutta toistaiseksi se pitää tehdä itse; onneksi vain kerran. Pahoittelut lisävaivasta.)

Arvojen upottaminen merkkijonoon

Käytetään tätä muuttujaa:

val ikavuodet = 20ikavuodet: Int = 20

Mitä, jos haluaisimme tuottaa tekstin, joka kuvailee muuttujaan tallennetun tiedon? Teksti voisi olla vaikkapa muotoa "Asiakas on X vuotta vanha.", missä X korvataan muuttujaan tallennetulla arvolla. (Tämä on yleinen tarve. Lukemattomat sovellusohjelmat raportoivat samaan tyyliin muistiin tallennettuja tietoja.)

Tässä yksi tapa:

s"Asiakas on $ikavuodet vuotta vanha."res6: String = Asiakas on 20 vuotta vanha.
Huomaa ensimmäistä lainausmerkkiä edeltävä s-kirjain. Se toimii merkkinä siitä, että upotamme merkkijonoliteraaliin lausekkeen arvon. (Englanniksi tämä tekniikka tunnetaan nimellä string interpolation.)
Merkkijonoon voi kirjoittaa dollarimerkin $ ja sen perään nimen. Nimi korvautuu vastaavalla arvolla. Tämä toimii, kunhan alussa oli se s-kirjain. (Jos s unohtuu, mitä tapahtuu? Arvaa ja kokeile REPLissä, menikö oikein.)
Tulos on siis nimenomaan merkkijono: tekstinpätkä, johon luvut sisältyvät numeroita vastaavina kirjoitusmerkkeinä, eivät lukuarvoina.

Alla on lisäesimerkkejä merkkijonoupotuksista.

val luku = 10luku: Int = 10
s"Luku kahdesti ja välilyöntejä välissä: $luku   $luku"res7: String = Luku kahdesti ja välilyöntejä välissä: 10   10

Merkkijonoon voi upottaa useitakin lausekkeita. Yllä upotettiin kaksi kertaa sama lauseke; alla kaksi eri lauseketta.

s"$luku on vähän pienempi kuin ${luku + 1}."res8: String = 10 on vähän pienempi kuin 11.

Upotettavan lausekkeen ei tarvitse olla vain muuttujan nimi, vaan monimutkaisempikin lauseke kelpaa. Tällöin lauseke on rajattava aaltosulkeilla, kuten juuri tuossa yllä on rajattu aritmeettinen lauseke luku + 1. (Jos aaltosulkeet unohtuvat, mitä tapahtuu? Arvaa ja kokeile REPLissä, menikö oikein.)

Äskeisissä esimerkeissä merkkijonoon upotettiin kokonaislukuja kuvaavia merkkejä, mutta sama toimii toki muunkintyyppisille arvoille, kuten Doubleille:

val arvosana = 9.5arvosana: Double = 9.5
val raportti = s"arvosana: $arvosana"raportti: String = arvosana: 9.5
s"$arvosana on saamasi arvosana"res9: String = 9.5 on saamasi arvosana

Merkkijonon sisään voi upottaa myös String-tyyppisiä arvoja:

val nimi = "Satu"nimi: String = Satu
println(s"$nimi, $raportti")Satu, arvosana: 9.5

Tuo viimeinen käsky siis tekee saman kuin tämä merkkijonoja plus-operaattorilla peräkkäin laittava käsky:

println(nimi + ", " + raportti)Satu, arvosana: 9.5

Oletetaan, että on olemassa Int-tyyppinen muuttuja nimeltä asukasluku. Kirjoita alle yksi String-tyyppinen lauseke, jonka arvo on muotoa "Kaupungissa on X asukasta.", missä X:n kohdalle on upotettu asukasluku-muuttujan arvo.

(Älä kirjoita tähän println-käskyä tai määrittele uusia muuttujia. Kirjoita vain se lauseke, joka muodostaa pyydetyn merkkijonon. Voit itse kokeilla REPLissä jollakin konkreettisella asukasluku-muuttujan arvolla.)

Oletetaan, että on olemassa Int-tyyppiset muuttujat nimeltä kaupunki1 ja kaupunki2. Kirjoita alle yksi String-tyyppinen lauseke, jonka arvo on muotoa "Kaupungeissa on X ja Y asukasta eli yhteensä Z.", missä X:n ja Y:n kohdalle on upotettu muuttujien arvot ja Z:n kohdalle noiden arvojen summa.

Huomaa yllä mainittu aaltosulkeiden tarve, kun upotat merkkijonoon summalausekkeen.

(Älä kirjoita tähänkään println-käskyä tai määrittele uusia muuttujia.)

Lisää merkkijonojen plus-operaatiosta

Olemme nähneet, että merkkijonoja voi laittaa peräkkäin plus-operaattorilla. Voit yhdistää merkkijonoon plussalla myös muunlaisen arvon kuten luvun. Lopputulos on samanlainen kuin merkkijonoupotuksessa. Alla on muutama yllä käytetyistä esimerkeistä kirjoitettuna plus-operaattorilla merkkijonoupotuksen (eli sn ja dollarimerkin) sijaan.

"Asiakas on " + ikavuodet + " vuotta vanha."res10: String = Asiakas on 20 vuotta vanha.
"Luku kahdesti ja välilyöntejä välissä: " + luku + "   " + lukures11: String = Luku kahdesti ja välilyöntejä välissä: 10   10
val raportti = "arvosana: " + arvosanaraportti: String = arvosana: 9.5

Käytännön Scala-ohjelmissa muodostetaan merkkijonoja sekä plus-merkillä yhdistäen että dollarimerkillä upottaen. Niinpä molemmat tavat on syytä tuntea, ja tässä oppimateriaalissakin käytetään molempia.

Plus-operaattorin käyttöön liittyy kuitenkin Scalassa eräs rajoitus, joka pitää huomata. Verrataan kahta koodinpätkää:

s"$arvosana on saamasi arvosana"res12: String = 9.5 on saamasi arvosana
arvosana + " on saamasi arvosana"arvosana + " on saamasi arvosana"
         ^
warning: method + in class Double is deprecated (since 2.13.0):
Adding a number and a String is deprecated. Use the string interpolation `s"$num$str"`

Ensimmäinen käsky toimii halutusti merkkijonoupotuksella. Äkkiseltään voisi luulla, että toinen käsky toimisi yhtä hyvin. Kuitenkin saamme varoitusilmoituksen: Scala-työkalustomme varoittaa, että noin ei ole suotavaa tehdä.

Merkkijonon ja luvun peräkkäin liittävä plus-operaatio on määritelty Scalassa näin: "yhdistä plussan vasemmalla puolella olevan merkkijonon perään oikealla oleva luku". Toisin päin — eli luku vasemmalla ja merkkijono oikealla — ei käykään.

Älä siis käytä tuollaisessa tapauksessa plus-operaattoria. Voit käyttää sen sijaan merkkijonoupotusta, kuten varoitusilmoituskin vihjaa.

Lisää vaihtoehtoja äskeisille

(Tämä ei ole tässä kohden tärkeää, mutta kiinnostanee joitakin kurssilaisia. Voit ohittaa tämän ilman tunnontuskia.)

On vielä muitakin tapoja muodostaa merkkijonoja. Esimerkiksi kaikki seuraavat käskyt tuottavat saman tuloksen:

s"$arvosana on saamasi arvosana"res13: String = 9.5 on saamasi arvosana
"" + arvosana + " on saamasi arvosana"res14: String = 9.5 on saamasi arvosana
arvosana.toString + " on saamasi arvosana"res15: String = 9.5 on saamasi arvosana
Merkintä "" on tyhjä merkkijono (empty string) eli nollan merkin mittainen merkkijono. Kun siihen yhdistetään luku, saadaan tuon luvun kuvaava merkkijono (johon voi sitten edelleen yhdistellä muita arvoja). Lisää käyttöä tyhjille merkkijonoille löytyy mm. luvussa 4.1.
toString-käskyllä voi nimenomaisesti määrätä koneen tuottamaan lukua vastaavan merkkijonon. Tästä käskystä kerrotaan lisää mm. luvuissa 2.5 ja 5.2.

Vauhtia musiikkiin

Käytetään taas luvun 1.3 esittelemää play-käskyä. Muodostetaan ensin hieman pidempi melodia muuttujia apuna käyttäen. Ukko Nooassa alku toistuu uudestaan kappaleen lopussa, mikä järjestyy esimerkiksi näin:

val nooaAlku = "cccedddfeeddc---"nooaAlku: String = cccedddfeeddc---
val valiosa = "eeeeg-f-ddddf-e-"valiosa: String = eeeeg-f-ddddf-e-
val kokoNooa = nooaAlku + valiosa + nooaAlkukokoNooa: String = cccedddfeeddc---eeeeg-f-ddddf-e-cccedddfeeddc---
play(kokoNooa)

Kokeillaan nyt kappaleemme soittamista kahdella eri tempolla. Tallennetaan ensin käyttämämme tempot muuttujiin, joilla on kuvaavat nimet:

val normaaliTempo = 120normaaliTempo: Int = 120
val hidasTempo = 60hidasTempo: Int = 60

play pystyy soittamaan melodioita eri nopeuksilla. Tämän merkiksi sille on annettava merkkijono, jossa on nuotteja kuvaavien kirjaimien perässä kauttaviiva ja edelleen sen perässä haluttua tempoa kuvaavat numeromerkit. Esimerkiksi tällainen:

kokoNooa + "/" + normaaliTempores16: String = cccedddfeeddc---eeeeg-f-ddddf-e-cccedddfeeddc---/120

Juuri tuollaista kauttaviivallista merkkijonoa play osaa käsitellä. Tässä pari esimerkkiä kokeiltavaksi:

play(kokoNooa + "/" + normaaliTempo)play(kokoNooa + "/" + hidasTempo)
120 sattuu olemaan juuri play-käskyn oletustempo, joten ensimmäinen soittokerta kuulostaa ihan samalta kuin aiemmatkin.
Kun käytetään tempoa 60, Nooan saunaretki sujuu ikämiehelle sopivampaan tahtiin.

Jos muuttujille ei ole muuta tarvetta, niin koko merkkijonon voi toki kirjata tempoineen kerrallakin kuten alla.

play("cdefg/140")

Soitetaanpa Ukko Nooa oikein ripeästi, vaikkapa normaaliin nähden kaksinkertaisella tempolla. Pistetään kone laskemaan, paljonko kaksinkertainen nopeus on:

play(kokoNooa + "/" + normaaliTempo + normaaliTempo)

Mutta eihän se toimi! Selvitä, mistä kiikastaa, ja valitse seuraavista kaikki paikkansa pitävät väitteet. Ratkaisu kaikkiin kohtiin löytyy, kun kokeilet itse REPLissä ja kiinnität huomiota sekä arvoihin että tietotyyppeihin. Voit esimerkiksi tulostaa merkkijonon sen soittamisen sijaan.

Muuttujan arvon vaihtaminen

Ohjelmissa on usein tilanteita, joissa jokin tieto muuttuu. Esimerkiksi pelissä pelihahmon sijaintikoordinaatti voi muuttua hahmon liikkuessa, ja GoodStuff-sovelluksessamme käyttäjän suosikkihotelli voi vaihtua toiseen, kun hän lisää tietoja sovellukseen.

Yksi tapa vastata tällaisiin tarpeisiin ohjelmissa on pitää kirjaa vaihtuvasta tiedosta muuttujassa ja vaihtaa muuttujan arvo toiseen tilanteen niin vaatiessa.

Kuulostaa aika hyvältä, mutta tämäpä ei val-sanaa käyttäen määritellyillä muuttujilla onnistukaan! val-muuttujan arvo on "lukittu" muuttujaan, eikä sitä voi vaihtaa toiseksi.

var-muuttujat

Scala-kielen sanalla var (englannin sanasta variable) voi luoda muuttujan, johon sijoitetaan arvo kuten val-muuttujaankin. Ainoana mutta tärkeänä erona val- ja var-muuttujan välillä on se, että jälkimmäisen arvoa voi vaihtaa toiseksi myöhemmin.

Tutustu seuraavaan animaatioon huolellisesti.

Tässä esimerkissämme siis lopulta vaihdettiin sekä luku- että tupla-muuttujan arvoa. Tämä edellyttää, että muuttujat on määritelty var-sanalla. Jos vaihtaisit var-sanat valeiksi, niin yllä käytetyt koodirivit eivät toimisikaan, vaan saisit virheilmoituksen error: reassignment to val.

Kenties hämäävä REPL-juttu

Itse asiassa REPL sallii sinun määritellä olemassa olevan nimisiä muuttujia uudestaan kirjoittamalla uuden var- tai val-alkuisen alustuksen samalle muuttujanimelle. Jos teet näin, saattaa vaikuttaa siltä, että val-muuttujankin arvoa voisi vaihtaa. Oikeastaan kyse on kuitenkin siitä, että uusi määrittely peittää aikaisemman.

Tämä on Scala REPLin erityispiirre, eikä REPLin ulkopuolisissa Scala-ohjelmissa voi tällä tavoin peräkkäisillä käskyillä luoda keskenään samannimisiä muuttujia. Joten kannattaa unohtaa tämä tekniikka.

Toinen esimerkki

Kun var-muuttujan arvoa muuttaa, voi sen vanhaa arvoa käyttää apuna uutta arvoa määritettäessä:

Varo matikkaa! (taas)

Huomaa ja muista: Matematiikassa muuttuja on symboli, joka vastaa jotakin arvoa. Ohjelmoinnissa muuttuja on nimetty muistipaikka, johon voi tallentaa arvon.

Eron käytännön merkitys korostuu var-muuttujien kohdalla. Ohjelma ei ole yhtälöryhmä! Samassa ohjelmassa voi hyvin esiintyä esimerkiksi sijoitukset luku = 10 sekä luku = 5. Myös tutusta matemaattisesta näkökulmasta omituiselta vaikuttava luku = luku + 10 on mahdollinen. Ja sijoituskäskyjen järjestyksellä on väliä!

Miksi val?

Protesti! Miksi ikinä tekisin val-muuttujan? Eikö varilla voi tehdä kaikki samat ja enemmän?

var-muuttujat tosiaan lisäävät mahdollisuuksia, mutta se ei ole pelkästään hyvä puoli.

Ohjelmoijan on järkeiltävä koodinsa toiminnasta koodia kirjoittaessaan ja siitä virheitä etsiessään. Järkeilyä helpottaa se, että ohjelmoija tietää tiettyjen asioiden koodissa olevan muuttumattomia. Hän voi esimerkiksi nähdä val-sanasta suoraan, että tietyn muuttujan arvo ei missään ohjelman suorituksen vaiheessa muutu miksikään. Tämä seikka alkaa tuntua merkityksellisemmältä, kunhan kohtaat suurempia ja monimutkaisempia ohjelmia; huomannet val-muuttujien edun itsekin vielä tämän kurssin aikana.

Pienissä REPL-kokeiluissa ei ole niin väliä, kummanlaisia muuttujia käytät, mutta tässä jo valmiiksi nyrkkisääntö tulevia ohjelmointitehtäviä varten:

Tee jokaisesta muuttujasta val, ellei sinulla ole juuri nyt selvää syytä tehdä siitä var.

Älä tee var-muuttujia "varmuuden vuoksi, jos sitä arvoa vaikka tarvitsisi muuttaa". Se on heikkoa ohjelmointityyliä. Jos myöhemmin osoittautuu, että val ei sovi tarkoitukseen, voit vaihtaa variin.

Onko valit edes mitään "muuttujia"?

Voidaan ajatella, että vain var-muuttujat ovat niitä varsinaisia muuttujia, koska niiden arvoa voi muuttaa luomisen jälkeenkin. Kuitenkin myös val-muuttujaa voi perustellusti kutsua "muuttujaksi", muun muassa siksi, että se voi saada eri arvoja ohjelman eri suorituskerroilla (vaikkapa käyttäjän syötteestä) ja siksi, että sama val-muuttujan luontikäsky saatetaan suorittaa ohjelman aikana useita kertoja niin, että joka kerta luodaan eriarvoinen val-muuttuja edellisen tilalle. Näistä nähdään esimerkkejä myöhemmin kurssilla.

val-muuttujat ovat myös lähempänä matemaattisen muuttujan käsitettä kuin varit, ja onpa esitetty sellaistakin, että juuri val-muuttujat niitä oikeita muuttujia ovatkin, kun taas varit ovat "sijoituspaikkoja" ("assignables"). Jätetään tämä sanasota nyt kuitenkin tähän.

Funktionaalinen ohjelmointi

On olemassa laaja ohjelmoinnin suuntaus, funktionaalinen ohjelmointi, jossa puhtaimmillaan käytetään ainoastaan val-tyylisiä muuttujia. Aiheeseen tutustutaan tämän oppimateriaalin luvussa 10.2 ja syvemmin jatkokursseilla.

Kysyttyä: onko valilla ja varilla eroa muistinkäytön tai muun suorituskyvyn kannalta?

Pohjimmiltaan eroa ei ole. Muuttujille varatun muistin määrä riippuu vain tietotyypistä; siitä luvussa 5.4.

Käytännössä asia on monimutkaisempi, ja valinta varin ja valin välillä voi vaikuttaa esimerkiksi niihin optimointeihin, joita kääntäjät tekevät muokatessaan ohjelmoijan kirjoittaman Scala-koodin suoritettavaan muotoon. Huomionarvoista on esimerkiksi se, että val-muuttujista voi olla etua, kun ohjelmia tehostetaan jakamalla suorituksen osia eri tahojen rinnakkain suoritettavaksi. Tuota teemaa ei tällä peruskurssilla syvemmin käsitellä.

var-tehtäviä ja soittimia

Mitä tämä koodinpätkä tulostaa?

var esimerkki = 2
esimerkki = esimerkki * esimerkki
esimerkki = esimerkki * esimerkki
println(esimerkki * esimerkki)

Kirjoita tuloste tähän:

Tässä vielä merkkijonoesimerkki:

var sana = "laama"
sana = sana + "nni"
sana = "piiri"
sana = "ta" + sana
sana = "noi" + sana

Mikä on sana-muuttujan arvo lopuksi?

Käsky play mahdollistaa myös virtuaalisoittimen vaihtamisen oletusarvoisesta, eli pianosta, toiseksi. Se järjestyy laittamalla merkkijonon sisään, esimerkiksi heti alkuun, instrumentin numero hakasulkeissa. Numeron on oltava väliltä 1–128.

Otetaanpa, lapset, Ukko Nooa nokkahuilulla eli soittimella numero 75:

play("[75]>cccedddfeeddc---")

Tässä yhteydessä hakasulkeet eivät ole mikään yleisemmin Scala-ohjelmointiin liittyvä tekniikka vaan merkkejä merkkijonossa (lainausmerkkien sisällä) siinä missä muutkin. play-käsky tulkitsee ne soitinmerkinnäksi. (Hakasulkeille löytyy kyllä Scala-ohjelmissa muuta käyttöä seuraavassa luvussa 1.5.)

Tutustu nyt huolellisesti seuraavaan koodinpätkään. Mieti, mitä arvoja kukin muuttuja saa missäkin vaiheessa, kun käskyt annetaan yksi kerrallaan peräjälkeen.

var soitin = 13
val nuotit = "<<<h.>c#.d.e.f#.d.f#-.e#.c#.e#-.e.c.e-.<h.>c#.d.e.f#.d.f#.h.a.f#.d.f#.a-- "
var peikkotanssi = "[" + soitin + "]" + nuotit + "/138"
play(peikkotanssi)
soitin = 72
play(peikkotanssi)

Kumpi seuraavista kuvaa sitä, mitä tapahtuu, kun esimerkin viimeinen rivi suoritetaan? Tutki ilmiötä itse REPLissä tarpeen mukaan. Soittokäskyn lisäksi tai sijaan voit tulostaa tuolle käskylle esimerkissä annetut parametriarvot.

Lisätään äskeiseen ohjelmaan yksi rivi. Tavoitteena on, että kun rivit suoritetaan yksi kerrallaan järjestyksessä, niin peikkotanssi tulee soitetuksi ensin soittimella 13 (marimba) ja toiseksi soittimella 72 (klarinetti).

Tässä on tarvittava koodirivi:

peikkotanssi = "["+ soitin + "]" + nuotit + "/138"

Monesko tämä lisätty rivi olisi toimivassa seitsenrivisessä koodissa? Vastaa yhdellä numerolla.

play ja MIDI-äänisynteesi

Soittokäskyn play tuntemat soittimet on määritelty General MIDI -standardissa, jonka nimessä MIDI on lyhenne sanoista Musical Instrument Digital Interface eli digitaalinen soitinrajapinta. MIDI-pohjaisesti voi tuottaa synteettistä ääntä erilaisilla virtuaalisoittimilla; tulosten laatu vaihtelee. Soittokäskymme tarjoaa merkkijonoihin perustuvan, yksinkertaiseen käyttöön sopivan tavan hyödyntää keskeisimpiä MIDI-äänisynteesitoimintoja.

Numeroitu soitinluettelo löytyy midi.org -sivustolta.

Tällä kurssilla käytämme MIDI-ääntä "huviksemme", lähinnä eräissä kurssin alkupuolen merkkijonoesimerkeissä. Kuvaamme merkkijonoilla nuotteja, emme suoranaisesti ääntä. Äänen digitaalisesta kuvaamisesta ja nauhoitetusta äänestä tulee puhetta esimerkiksi kurssilla Ohjelmointistudio 1.

play ja pisteet

Äsken käytetyssä peikkotanssimerkkijonossa oli pisteitä. play-käsky tulkitsee pisteitä edeltävät nuotit teräväksi staccato-soitoksi, jossa nuotti soitetaan lyhyempänä ja äänen perässä on tauko.

var ja tietotyypit

Muuttujan tietotyyppi määrää, millaisia arvoja voit sijoittaa muuttujaan. Se ei muutu, ei var-muuttujankaan tapauksessa. Esimerkiksi String-tyyppiseen muuttujaan voi sijoittaa vain String-arvoja, kuten seuraava esimerkki osoittaa.

var titteli = "opiskelija"titteli: String = opiskelija
titteli = "DI"titteli: String = DI
titteli = 12345Int(12345) <: String?
false
<console>:8: error: type mismatch;
found   : Int(12345)
required: String
titteli = 12345
          ^

Virheilmoitusten tulkintataito kehittyy ohjelmointikokemuksen mukana. Tässä virheilmoituksessa lukee osapuilleen, että:

"Onko kokonaisluku 12345 eräänlainen merkkijono? No ei. Virhe: Tietotyypit eivät sovi yhteen; riviltä titteli = 12345 löytyi kokonaisluku 12345 kohdasta, johon olisi kaivattu merkkijonoa."

Lukutyyppien yhteispeliä

On kuitenkin tilanteita, joissa näennäisesti rikotaan sääntöä tyyppien yhteensopivuudesta. Yksi tällainen tilanne on Int-arvon sijoittaminen Double-muuttujaan, kuten seuraavan esimerkin loppupäässä:

var lukuarvo = 123.45lukuarvo: Double = 123.45
val tasaluku = 100tasaluku: Int = 100
tasaluku * tasalukures17: Int = 10000
lukuarvo = tasalukulukuarvo: Double = 100.0
lukuarvo * tasalukures18: Double = 10000.0

Viimeinenkin sijoitus onnistui: muuttujasta tasaluku katsottu Int-arvokin "kelpaa Double-arvoksi". Kuitenkin kuten esimerkin viimeisistä tulosteista huomaat, Int-arvon sijaan tulee muuttujaan lukuarvo tallennetuksi vastaava Double-arvo, jolla laskeminen tuottaa Double-tyyppisiä tuloksia.

Tämä on hyödyllistä monessa tulevassa tilanteessa, jossa laaditaan sellainen ohjelman osa, joka toimii desimaaliluvuilla ja jonka halutaan toimivan samalla tavoin myös kokonaisluvuille.

Olipa kerran var-muuttujat nimeltä hannu ja kerttu, jotka olivat keskenään samantyyppisiä. Tarkastellaan seuraavaa koodinpätkää:

hannu = kerttu
kerttu = hannu

Mikä seuraavista väittämistä kuvaa parhaiten sitä, mitä muuttujien arvoille käy, kun muuttujat on ensin luotu ja alustettu joillakin arvoilla ja äskeiset koodirivit suoritetaan annetussa järjestyksessä? Tutki asiaa tarpeen mukaan ohjelmoimalla REPLissä.

Seuraavasta koodinpätkästä on muokattu pieni arvoitus korvaamalla pari kohtaa kysymysmerkeillä. Lue koodi läpi ja mieti, mitä se tekee kahden ensiksi määritellyn muuttujan arvoille.

var eka = ???
var toka = ???
val apumuuttuja = eka
eka = toka
toka = apumuuttuja
println(eka + ", " + toka)

Lisäksi tiedetään, että viimeinen tulostuskäsky tulostaa "vemmelsääri, ristihuuli". Mikä oli muuttujan toka alkuarvo?

Edellä näit, että Double-tyyppiseen muuttujaan voi sijoittaa Int-tyyppisen lausekkeen arvon, jolloin muuttujaan tallentuu kokonaislukua vastaava desimaaliluku. Toimiiko sama myös toisin päin, eli tallentuuko desimaalilukua vastaava kokonaisluku? Kokeile REPLissä.

res-muuttujat REPLissä

REPLin res-alkuiset vastaukset sille syötettyihin lausekkeesiin ovat jo tulleet tutuiksi. Itse asiassa REPL luo aina uusia val-muuttujia, joiden nimet alkavat res ja joita voit käyttää itse määrittelemiesi muuttujien tapaan:

1 + 1res19: Int = 2
res19 * 10res20: Int = 20
val yhteensa = res19 + res20yhteensa: Int = 22

Voit hyödyntää tuota omissa kokeiluissasi, jos haluat. Kurssimateriaalin esimerkeissä tätä mahdollisuutta ei kuitenkaan käytetä, vaan keskitymme opettelemaan ohjelmointitekniikoita, jotka toimivat myös REPLin ulkopuolella. Numeroidut res-alkuiset muuttujat ovat nimenomaan REPL-ympäristön erikoisuus.

Yhteenvetoa

  • Muuttuja on nimetty tallennuspaikka yhdelle arvolle. Muuttujia käytetään tiedon varastoimiseen koneen muistissa.
    • Esimerkiksi GoodStuff-ohjelmassa muuttujia voidaan käyttää kirjaamaan kokemusten tietoja (hinta, arvosana) sekä se, mikä on käyttäjän suosikkikokemus.
  • Ohjelmoija pääsee arvoihin käsiksi muuttujien nimien avulla. Nimiä voi käyttää lausekkeina ja siis myös suurempien lausekkeiden osina.
  • Scalassa on kahdenlaisia muuttujia: val ja var.
    • val-muuttujaan sijoitetaan arvo, joka ei sen jälkeen vaihdu. val-muuttujien käyttö selkeyttää ohjelmakoodia, ja niitä tulee käyttää ensisijaisesti.
    • var-muuttujan arvoa voi muuttaa sijoittamalla uuden arvon vanhan tilalle. var-muuttujat mahdollistavat ohjelman tilan muuttamisen sijoituskäskyillä; niitä käytetään harkitusti tarpeen mukaan.
  • Järkevästi nimetyt muuttujat selkiyttävät ohjelmakoodia. Muuttujien käyttö voi parantaa myös ohjelman tehokkuutta ja muokattavuutta.
  • Lukuun liittyviä termejä sanastosivulla: muuttuja, sijoittaa; lauseke, arvo, evaluoida; var-muuttuja, val-muuttuja; varattu sana; DRY; merkkijonoupotus.

Tässä vielä edellisen luvun käsitekaavio tämän luvun tärkeimmillä käsitteillä täydennettynä:

Palaute

Huomaathan, että tämä on henkilökohtainen osio! Vaikka olisit tehnyt lukuun liittyvät tehtävät parin kanssa, täytä palautelomake itse.

Tekijät

Tämän oppimateriaalin kehitystyössä on käytetty apuna tuhansilta opiskelijoilta kerättyä palautetta. Kiitos!

Materiaalin luvut tehtävineen ja viikkokoosteineen on laatinut Juha Sorva.

Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.

Tehtävien automaattisen arvioinnin ovat toteuttaneet: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä ja Aleksi Vartiainen.

Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.

Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista suunnittelivat Juha Sorva ja Teemu Sirkiä. Teemu Sirkiä ja Riku Autio toteuttivat ne apunaan Teemun aiemmin rakentamat työkalut Jsvee- ja Kelmu.

Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset laati Juha Sorva.

O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.

Tapa, jolla käytämme O1Libraryn työkaluja (kuten Pic) yksinkertaiseen graafiseen ohjelmointiin, on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.

Oppimisalusta A+ luotiin alun perin Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Nykyään tätä avoimen lähdekoodin projektia kehittää Tietotekniikan laitoksen opetusteknologiatiimi ja tarjoaa palveluna laitoksen IT-tuki. Pääkehittäjänä on tällä hetkellä Markku Riekkinen, jonka lisäksi A+:aa ovat kehittäneet kymmenet Aallon opiskelijat ja muut.

A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen ovat luoneet Nikolai Denissov, Olli Kiljunen ja Nikolas Drosdek yhteistyössä Juha Sorvan, Otto Seppälän, Arto Hellaksen ja muiden kanssa.

Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.

Lisäkiitokset tähän lukuun

Luvussa tehdään vääryyttä Edvard Griegin säveltämälle musiikille. Kiitos ja anteeksi.

a drop of ink
Palautusta lähetetään...