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

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

Luku 5.6: Silmukoita, merkkijonoja ja vaalit

Tästä sivusta:

Pääkysymyksiä: Miten käytän itse silmukoita kirjoittaessani luokkaa? Saisiko lisäesimerkkejä for-silmukoista? Mitä jos laittaa silmukoita sisäkkäin? Jospa FlappyBugissa olisi useita esteitä?

Mitä käsitellään? Käytännön harjoitusta edellisen luvun aiheesta. Lisäksi: Merkkijonojen ja Range-olioiden läpikäyminen. Sisäkkäiset for-silmukat.

Mitä tehdään? Lähinnä ohjelmoidaan.

Suuntaa antava työläysarvio:? Neljä tai viisi tuntia.

Pistearvo: A220.

Oheismoduulit: ForLoops, Election (uusi), FlappyBug.

../_images/person03.png

Johdanto

Tähänastisissa esimerkeissä for-silmukoilla on käyty läpi vain vektoreita ja puskureita. Kuitenkin kuten luvussa 5.5 mainittiin, voi for-silmukalla käydä läpi hyvinkin erilaisia alkiokokoelmia. Vaikkapa merkkijonoja:

Merkkijonon läpikäyminen

Sanotaan esimerkin vuoksi, että halutaan tulostaa raportti kustakin tietyn merkkijonon sisältämästä merkistä. Merkkijonolle "kissa" tulostettaisiin näin:

Merkki: k
Merkki: i
Merkki: s
Merkki: s
Merkki: a

Tämä onnistuu seuraavalla Scala-koodilla.

for (merkki <- "kissa") {
  println("Merkki: " + merkki)
}
Merkkijonokin on kokoelma merkkejä — siis Char-tyyppisiä arvoja (luku 5.2) — ja sitäkin voi käyttää tässä nuolen oikealla puolella.
Muuttuja merkki on siis tyyppiä Char.
Silmukan runko toistuu yhtä monta kertaa kuin merkkijonossa on merkkejä. Merkit käydään läpi järjestyksessä alusta loppuun.

Entä jos haluttaisiinkin numeroitu tuloste ja lisäksi merkkien Unicode-arvot?

Indeksillä 0 on k, Unicodessa merkki #107.
Indeksillä 1 on i, Unicodessa merkki #105.
Indeksillä 2 on s, Unicodessa merkki #115.
Indeksillä 3 on s, Unicodessa merkki #115.
Indeksillä 4 on a, Unicodessa merkki #97.

Yksi toteutustapa on tämä:

var indeksi = 0
for (merkki <- "kissa") {
  println("Indeksillä " + indeksi + " on " + merkki + ", Unicodessa merkki #" + merkki.toInt + ".")
  indeksi += 1
}
Käytetään askeltajamuuttujaa pitämään kirjaa indeksistä, jossa ollaan menossa. Sen arvoa kasvatetaan yhdellä joka kierroksella aina tulostamisen jälkeen.
Char-luokassa on määritelty toInt-metodi, joka palauttaa kyseisen merkin Unicode-arvon kokonaislukuna.

Äskeinen ohjelma löytyy myös ForLoops-oheismoduulista pakkauksesta o1.looptest, samoin kuin usea muu tämän luvun esimerkki ja tehtävä.

Tutki debuggerilla

Debuggeri on ohjelman suoritusvaiheiden tutkimiseen sopiva aputyökalu. Esimerkiksi IntelliJ’hin sisältyy debuggeri.

Tässä kurssimateriaalissa on erillinen sivu, joka esittelee debuggerin käyttöä.

Suosittelemme, että tutustut debuggerimateriaaliin ennen kuin jatkat tässä luvussa. Voit sitten käydä tämän luvun ohjelmia läpi debuggerilla rivi riviltä. Se voi auttaa hahmottamaan ohjelmien toimintaa ja toimii myös harjoituksena debuggerin käytöstä.

Debuggeri voi auttaa virheiden etsinnässä monissa tulevissa harjoitustehtävissä ja kurssin ulkopuolella ohjelmoidessasi.

Pieni ohjelmointitehtävä: vain isot kirjaimet

Moduulin ForLoops pakkauksesta o1.looptest löytyy yksittäisolio Task2. Olion toivottu toiminta on kuvattu sen ohjelmakoodin kommentissa. Täydennä olio toiveiden mukaiseksi.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Pieni vaikka monisanaisempi tehtävä: DNA:n ominaisuuksia

Solujemme DNA:n keskeisiä rakennusaineita ovat neljänlaiset emäkset (base): guaniini (G), adeniini (A), sytosiini (C) ja tymiini (T). DNA-säiettä voidaan kuvata luettelemalla siinä olevat emäkset järjestyksessä, esimerkiksi GATCACAGGT jne. Eliön DNA on yksilöllinen, mutta saman lajin yksilöiden väliset erot ovat pieniä, ja lajien välilläkin on merkittäviä yhtäläisyyksiä.

DNA:n eräitä ominaisuuksia kuvaa sen GC-pitoisuus (GC content) eli prosenttiluku, joka kertoo, kuinka suuri osuus emäksistä on guaniinia tai sytosiiniä. Esimerkiksi DNA-pätkän ATGGACAT GC-pitoisuus on 37,5 %, koska kahdeksasta emäksestä kolme on guaniini tai sytosiini. GC-pitoisuudella on merkitystä esimerkiksi geeniteknologiassa (ks. esim. Wikipedia).

Nouda Task3. Voit kokeilla ajaa tämän pikkuohjelman, joka toimii tekstikonsolissa näin:

Enter a species file name (without .mtdna extension): human
The GC content is 80.13914555929992%.

Ohjelma näyttää siis laskevan käyttäjän mainitseman lajin DNA:n GC-pitoisuuden. Syötteeksi sille voi antaa jonkin merkkijonoista human, chicken, chimpanzee, mouse, opossum, pufferfish, yeast tai test; ohjelma käyttää apunaan vastaavannimisiä moduuliin liitettyjä tiedostoja, joihin on kirjattu lajien DNA-sekvenssejä (tarkemmin sanoen mitokondrio-DNA-tietoja; mtDNA).

Ohjelman raportoima tulos on kuitenkin pielessä, koska tiedostossa Task3.scala määritelty funktio gcContent on virheellisesti toteutettu. Tuloksen kuuluisi syötteellä human olla reilut 44 % eikä 80 %.

Tehtäväsi on tutustua funktion toteutukseen (löytyy mainitusta tiedostosta sekä alta) ja tehdä siihen tarvittava pikkuinen muutos, joka saa sen toimimaan.

Funktion tulisi laskea parametriksi annettua merkkijonoa vastaava GC-pitoisuus, eli esimerkiksi parametriarvolla "ATGGACAT" palautettaisiin Double-arvo 37.5. Tässä leluesimerkissämme kaikki merkityksettömät merkit, kuten välilyönnit ja emästä kuvaamattomat kirjaimet, yksinkertaisesti ohitetaan, joten esimerkiksi merkkijono "ATG G ACATXXYZYZ" tuottaisi myös tuloksen 37.5.

def gcContent(dna: String) = {
  var gcCount = 0
  var totalCount = 0
  for (base <- dna) {
    if (base == 'G' || base == 'C') {
      gcCount += 1
    } else if (base == 'A' || base == 'T') {
      totalCount += 1
    }
  }
  100.0 * gcCount / totalCount
}
Char-olioillekin on literaalimerkintä vastaavasti kuin luvuille, totuusarvoille ja merkkijonoille. Huomaa, että toisin kuin merkkijonoliteraali, Char-arvo eli yksittäinen merkki kirjoitetaan yksinkertaisiin hipsukoihin.

Jos haluat kokeilla ohjelmaa itse valitsemillasi kirjainsekvensseillä, voit muokata kansiosta mkDNA_examples löytyvää tiedostoa test.mtdna ja antaa ohjelmalle syötteen test.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Lukujen läpikäyminen: to, until ja indices

Luvussa 5.2 mainittiin, että Int-olioilla on metodi nimeltä to, jota on yleensä mukavinta käyttää operaattorinotaatiolla eikä pistenotaatiolla. Se palauttaa viittauksen Range-olioon:

5 to 15res0: Range.Inclusive = Range 5 to 15

Range-olio edustaa kokonaislukuluetteloa. Sekin on alkiokokoelma, jonka voi käydä läpi for-silmukalla:

val tuhanteen = 1 to 1000
for (luku <- tuhanteen) {
  println(luku)
}

Silmukkamuuttuja luku toimii askeltajana, joka saa vuorollaan kunkin positiivisen kokonaisluvun 1, 2, 3, ..., 1000. Ohjelma tulostaa nämä luvut järjestyksessä omille riveilleen.

Äskeisen ohjelman voi kirjoittaa yksinkertaisemminkin:

for (luku <- 1 to 1000) {
  println(luku)
}

Vastaavaa käskyä voi käyttää myös indeksejä käyttävän kokoelman — esimerkiksi merkkijonon — indeksien läpikäymiseen. Tämä ohjelma tuottaa tutun tulosteen:

val merkkijono = "kissa"
for (indeksi <- 0 to merkkijono.length - 1) {
  val merkki = merkkijono(indeksi)
  println("Indeksillä " + indeksi + " on " + merkki + ", Unicodessa merkki #" + merkki.toInt + ".")
}
Jos käytämme to-metodia, on parametriarvoksi muistettava antaa vähennyslaskun merkkijono.length - 1 tulos, jotta nollasta alkava indeksi pysyy rajoissa.

Pieniä ohjelmointitehtäviä: to, until yms.

Moduulin ForLoops pakkauksesta o1.looptest löytyy lisää yksittäisolioita, jotka on tarkoitus muokata: katso nyt tiedostot Task4.scala, Task5.scala ja Task6.scala. Olioiden toivottu toiminta on myös kuvattu noissa tiedostoissa. Täydennä ne toiveiden mukaisiksi.

Nämä pienet tehtävät ovat toisistaan riippumattomia. Voit tehdä ja palauttaa ne yksitellen, järjestyksessä.

Muista testata ohjelmia ensin itse. Käytä debuggeria virheiden paikallistamisessa apuna, kun on tarvetta.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

indices-metodi

Kun halutaan käydä läpi kaikki merkkijonon tai muun kokoelman indeksit, on kokoelman indices-metodi usein kätevä vaihtoehto until- ja to-metodeille.

val merkkijono = "kissa"merkkijono: String = kissa
for (indeksi <- merkkijono.indices) {
  println("Indeksillä " + indeksi + " on " + merkkijono(indeksi))
}Indeksillä 0 on k
Indeksillä 1 on i
Indeksillä 2 on s
Indeksillä 3 on s
Indeksillä 4 on a

Tämä perustuu siihen, että indices palauttaa juuri sellaisen Range-olion, jonka olisimme voineet until-metodillakin luoda:

merkkijono.indicesres1: Range = Range 0 until 5

Election-tehtävä

Tehtävänanto

../_images/module_election.png

Election-ohjelman rakenne on varsin yksinkertainen.

Tehtäväsi on:

  • noutaa Election-moduuli,
  • tutustua moduulin Scaladoc-dokumentaatioon ja valmiina annettuun Candidate-luokan ohjelmakoodiin, joka kuvaa ehdokkaita vaaleissa,
  • kirjoittaa Scala-ohjelmakoodi, joka toteuttaa puuttuvan luokan District, joka kuvaa vaalipiirejä, ja
  • (tietysti) testata, että luokka toimii ja korjata tarvittaessa.

Yleisiä ohjeita ja vinkkejä

  • District-luokka muistuttaa monella tavalla luvun 5.5 AuctionHouse-luokkaa. Kertaa tuota lukua tarvittaessa.
    • Useassa District-luokan metodissa voi käyttää for-silmukkaa toteutuksen osana.
    • Luvussa 5.5 nähty ohjelmakoodi voi toimia inspiraationa District-luokan toteutukselle.
  • Käytä annettua käynnistysoliota ElectionTest, joka kutsuu District-luokan metodeita. Voit muokata sitä testataksesi erilaisia tapauksia.
    • Ellei sinulla ole jonkinlaista toteutusta kullekin District-luokan metodille, ei ElectionTest-ohjelmaa voi annetussa muodossa ajaa.
    • Voit kuitenkin testata testiohjelmalla osittaistakin District-toteutusta, jos "kommentoit ulos" puuttuvien metodien kutsut testiohjelmasta tai laadit toistaiseksi toteuttamattomille District-luokan metodeille aluksi toteutuksenraakileet (vaikkapa ???-merkintää käyttäen; luku 4.1).
  • Kannattanee edetä seuraavaksi kuvatuissa vaiheissa.

Suositellut työvaiheet

  1. Tutustu ensin dokumentaatioon. Huomaa, että usea District-luokan metodi on merkitty toteutettavaksi vasta huomattavasti myöhemmässä tehtävässä (luku 9.2). Nyt tässä luvussa kuuluu toteuttaa kaikki muut puuttuvat metodit paitsi nämä erikseen merkityt.
  2. Tutustu moduulin sisältämään ohjelmakoodiin.
  3. Ryhdy toteuttamaan District-luokkaa seuraavissa vaiheissa. Käytä ElectionTest-käynnistysoliota sitä mukaa kuin luot District-luokkaan uusia metodeita.
  4. Määrittele luokkaan District konstruktoriparametrit ja ilmentymämuuttujat.
  5. Luo toString-metodi.
  6. Luo printCandidates-metodi.
    • Voit ottaa mallia vaikkapa AuctionHouse-luokan nextDay-metodista.
  7. Luo candidatesFrom-metodi.
    • Voit ottaa mallia AuctionHouse-luokan purchasesOf-metodista.
    • Huomaa, että palautusarvon tyypiksi on määrätty Vector[Candidate]. Voit käyttää puskuria palautettavan kokoelman koostamiseen (kuten AuctionHouse1-moduulissa), mutta siirrä puskurin sisältö lopuksi vektoriin toVector-metodilla.
  8. Luo topCandidate-metodi.
    • Voit ottaa mallia AuctionHouse-luokan priciest-metodista.
    • Osaatko käyttää vektorin head- ja tail-metodeita niin, ettei metodissa turhaan vertailla ensimmäistä alkiota itseensä? (Ei välttämätöntä.)
  9. Luo totalVotes-nimiset metodit.
    • Voit ottaa mallia AuctionHouse-luokan totalPrice- ja numberOfOpenItems-metodeista.

Vapaaehtoinen mutta hyvin suositeltava lisätehtävä

Toimivatko kaksi totalVotes-metodiasi jo? Hyvä! Mutta tuliko niistä keskenään kovin samankaltaiset: useita samoja koodirivejä molemmissa?

Toisteisuus heikentää koodin muokattavuutta, kasvattaa virheiden mahdollisuutta eikä ole kaunista.

Yksi hyvä tapa tehdä koodista vähemmän toisteista on käyttää apumetodia, johon kirjataan se osa algoritmista, joka on näille metodeille yhteistä. Tässä yhteinen osa on äänten laskeminen ehdokkaita sisältävästä vektorista: totalVotes-metodit eroavat toisistaan vain siinä, mitkä ehdokkaat otetaan mukaan laskentaan.

Apumetodin voi määritellä vaikkapa tähän tapaan:

private def countVotes(candidates: Vector[Candidate]) = {
  // Laske yhteisäänet saadusta vektorista ja palauta summa.
}

Apumetodista on perusteltua tehdä yksityinen, koska metodi on tarkoitettu vain District-tyypin sisäiseen käyttöön.

Osaatko toteuttaa apumetodin countVotes ja toteuttaa totalVotes-metodit niin, että ne kutsuvat countVotes-metodia antaen sille asianmukaisen parametriarvon?

Ja vielä...

AuctionHouse-luokan metodeissa ja luultavasti myös useassa District-luokkaan laatimassasi metodissa toistuu täsmälleen sama kaava:

  1. Määritellään tulosmuuttuja ja asetetaan sille alkuarvo.
  2. Sitten käytetään silmukkaa, joka käy läpi kokoelman alkiot ja suorittaa niille jonkin operaation: for (muuttuja <- kokoelma) { ... }
  3. Lopuksi palautetaan tulosmuuttujan arvo, joka määritettiin silmukan aikana.

Toistuvan kaavan vuoksi metodien koodissa esiintyy toisiaan muistuttavia rivejä.

Eikös tämäkin ole eräänlaista toisteisuutta? Voisiko jopa tämän välttää?

Kyllä voi ja tyylikkäällä tavalla voikin. Mutta se vaatii lisää esitietoja, joten palataan aiheeseen luvuissa 6.3 ja 6.4.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Pikkutehtävä: kuvan muodostaminen silmukalla

Tässä koodia pohdittavaksi ja ehkä REPLissäkin tutkittavaksi:

val vareja = Vector(White, Blue, Green, Black, Orange)
var yhdistelma = rectangle(150, 150, Red)
for (vari <- vareja) {
  val raita = rectangle(50, 150, vari)
  yhdistelma.leftOf(raita)
}
yhdistelma.show()

Mitkä kaikki seuraavista kuvaavat tämän ohjelman toimintaa oikein?

FlappyBug-tehtävä (osa 16/17: enemmän esteitä)

Tähän asti FlappyBug-pelissämme on ollut vain yksi este. Muokkaa peliä niin, että esteitä on useita. Kutakin estettä kuvaa oma Obstacle-olionsa, jotka tallennetaan vektoriin. Esteet käyttäytyvät keskenään samalla tavalla, mutta ne ovat erikokoisia ja kullakin on oma sijaintinsa.

Tarvittavat muutokset ovat:

  1. Esteet osaksi peliä: korvaa luokan Game obstacle-ilmentymämuuttujan osoittama 70-pikselinen este vektorilla, jossa on kolme estettä. Ensimmäisen koko on 70, toisen 30 ja viimeisen 20. Vaihda samalla obstacle-muuttujan nimeksi obstacles.
  2. Kukin este liikkeelle: muuta Game-luokan timePasses-metodia niin, että se liikuttaa kutakin vektoriin tallennetuista esteistä.
    • Vinkki: käytä for-silmukkaa. Varsin yksinkertainen silmukka riittää.
  3. Kuolinehdon päivitys: muuta Game-luokan isLost-metodia niin, että se palauttaa true, jos ötökkä ei ole pelialueella tai jos mikä tahansa esteistä koskettaa ötökkää.
    • Vinkki: tässä kohdassa on vähän enemmän tekemistä (toistaiseksi opituilla konsteilla) kuin edellisessä. Yksi mahdollisuus on käyttää for-silmukan apuna Boolean-tyyppistä var-muuttujaa: pidä kirjaa siitä, onko esteitä läpikäyvän silmukan suorituksen aikana löytynyt sellainen este, joka koskettaa ötökkää. Määrittele muuttuja metodin paikalliseksi muuttujaksi ennen for silmukan alkua.
  4. Kukin esteistä näkyviin: muuta käyttöliittymän makePic-metodia niin, että se muodostaa kuvan kustakin pelin estevektoriin tallennetusta esteestä ja asettaa nuo kuvat taustaa vasten. Viimeisenä päälle asetetaan edelleen ötökän kuva.
    • Vinkki: voit käyttää for-silmukan tukena Pic-tyyppistä kokoojamuuttujaa: samalla kun käyt läpi vektoria, päivität kokoojaa asemoimalla uuden estekuvan kokoojan vanhan arvon päälle. (Vrt. edellinen pikkutehtävä.)

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Uusi muuttujarooli: (yksisuuntainen) lippu

Äskeisessä isLost-vinkissä oli esimerkki eräästä muuttujan roolista (luku 2.6) eli yleisestä muuttujan käyttötavasta, josta ei ole tässä materiaalissa aiemmin mainittu.

Ohjelmoijat puhuvat melko usein lipuista (flag). Lipuksi kutsutaan tallennettua tietoa, jota käytetään ilmoittamaan jostakin asiaintilasta: "lippu nostetaan salkoon".

Yllä ehdotettiin, että käytät isLost-metodissa Boolean-tyyppistä muuttujaa tietyllä tavalla. Tämä muuttuja on lippu, joka "nostetaan" (eli saa arvon true) merkkinä siitä, että ötökkää koskettava este on löydetty. Tarkemmin sanoen kyseessä on yksisuuntainen lippu (one-way flag): kun sen arvo on kerran vaihdettu, se ei enää muutu.

Lipulla on vain kaksi mahdollista arvoa, minkä vuoksi lippumuuttujat ovat usein Boolean-tyyppisiä.

Tehokkuudesta huolestuneille

Äskeisessä isLost-metodissa homma on periaatteessa selvä heti, kun yksikin ötökkää koskettava este löytyy. Muita esteitä ei silloin edes tarvitsisi käydä läpi. Ehdotetulla tavalla toteutettu metodi kuitenkin käy aina läpi koko estevektorin.

Nythän noita esteitä oli vektorissa vain kolme, joten koneelta ei mene kauan käydä vektori läpi loppuun asti. Mutta mitä jos kävisimme samalla idealla läpi jotakin valtavaa kokoelmaa selvittääksemme, löytyykö sieltä vähintään yksi tietyn ehdon toteuttava alkio? Voisi toivoa, että koneen saisi lopettamaan heti, kun tulos on selvillä.

Asia järjestyy kyllä; konstit on monet. Palaamme aiheeseen mm. luvuissa 6.4 ja 7.1 työkalupakkimme painavoituessa.

Sisäkkäiset silmukat

Silmukoita voi laittaa sisäkkäin. Tällöin sisempi silmukka suoritetaan jokaisella ulomman silmukan "kierroksella".

Sisempi kaksikierroksinen silmukka tulee siis suoritetuksi useita kertoja, ulompi kolmikierroksinen silmukka vain kerran. Sisin println-käsky suorittuu yhteensä kuusi kertaa.

Silmukanlukemistehtävä

Mieti, mitä tapahtuu, kun seuraava ohjelmakoodi (joka ei tee mitään varsinaisesti hyödyllistä) suoritetaan. Mitä arvoja kukin muuttuja saa eri vaiheissa? Mikä merkkijono lopulta muodostuu tulosmuuttujaan?

val sanoja = Vector("funktio", "metodi", "proseduuri", "aliohjelma", "rutiini")
var tulos = ""
for (sana <- sanoja) {
  if (sana.length < 10) {
    var laskuri = 0
    for (kirjain <- sana) {
      if (laskuri % 2 == 0) {
        tulos += kirjain
      }
      laskuri += 1
    }
  }
}
println(tulos)
Montako kertaa alustetaan laskurimuuttuja nollaksi?
Montako kertaa tarkastetaan, onko laskurimuuttujan arvo parillinen?
Mikä on suurin arvo, jonka laskurimuuttuja saavuttaa?
Minkä kirjainrimpsun viimeinen rivi tulostaa?

Pieni ohjelmointitehtävä

Tee viimeinenkin o1.looptest-pakkauksen pikkutehtävä: Task7.scala.

Muista taas testata ja käyttää debuggeria tarvittaessa.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Käyttöalue eli skooppi

Tässä vaiheessa kurssia on tullut vastaan jo esimerkkiä sisäkkäisistä koodirakenteista. Tarkastellaan siis nyt, miten sisäkkäisyys vaikuttaa esimerkiksi muuttujien määrittelyihin.

Kullakin ohjelman pääosalla — muuttujalla, metodilla, luokalla, yksittäisoliolla — on käyttöalue (eli näkyvyysalue eli skooppi; scope). Käyttöalue on se osa ohjelmakoodia, josta kyseiseen osaan pääsee käsiksi. Käyttöalue riippuu yhteydestä, jossa kyseinen piirre on määritelty, minkä lisäksi sitä voi säädellä näkyvyysmääreillä kuten private.

Yritys käyttää muuttujaa tai metodia sen käyttöalueen ulkopuolelta aiheuttaa käännösaikaisen virheilmoituksen.

Tarkastellaan ensin kertauksenomaisesti luokan välittömien osien käyttöaluetta ja sitten paikallisten muuttujien käyttöaluetta.

Luokan osien käyttöalue

Sinun ei tarvitse ymmärtää, mitä seuraava luokka tekee (joskin se on opetetun perusteella ymmärrettävissä). Luokka on tässä vain esimerkkinä sisäkkäisyydestä ja käyttöalueista, ja tutkimme sitä pintapuolisesti:

class Joulukuusi(puunKorkeus: Int) {
  val korkeus = puunKorkeus.min(1000)
  private val leveinKohta = this.leveys(this.korkeus - 1)

  override def toString = {
    var kuva = this.rivi('*', 1)
    for (riviNro <- 1 until this.korkeus) {
      kuva += this.rivi('^', this.leveys(riviNro))
    }
    kuva += this.rivi('|', (this.leveinKohta - 4).min(3).max(1))
    kuva
  }

  private def leveys(rivi: Int) = rivi / 2 * 2 + 1

  private def rivi(merkki: Char, montako: Int) = {
    val tyhjaaVasemmalle = (this.leveinKohta - montako) / 2
    val kokoRivi = " " * tyhjaaVasemmalle + merkki.toString * montako + "\n"
    kokoRivi
  }
}
Julkisen ilmentymämuuttujan kuten korkeus käyttöalueeseen sisältyy koko luokka. Lisäksi siihen voi viitata luokan ulkopuoleltakin, kunhan käytössä on kyseisen luokan ilmentymä: olio.korkeus. Samoin julkista metodia kuten toString voi käyttää mistä tahansa päin luokkaa tai sen ulkopuolelta. Ilmentymämuuttuja tai metodi on julkinen ellei toisin määritetä.
Yksityisen ilmentymämuuttujan kuten leveinKohta ja yksityisen metodin kuten leveys tai rivi käyttöalue on koko kyseinen luokka (mahdollisine kumppaniolioineen; luku 5.3).
Luokka itse on julkinen, joten sitä voi käyttää muualta ohjelmasta vapaasti. (Yksityisiäkin luokkia on mahdollista määritellä, mutta sitä emme tee tällä kurssilla.)

Paikallisten muuttujien käyttöalue

class Joulukuusi(puunKorkeus: Int) {
  val korkeus = puunKorkeus.min(1000)
  private val leveinKohta = this.leveys(this.korkeus - 1)

  override def toString = {
    var kuva = this.rivi('*', 1)
    for (riviNro <- 1 until this.korkeus) {
      kuva += this.rivi('^', this.leveys(riviNro))
    }
    kuva += this.rivi('|', (this.leveinKohta - 4).min(3).max(1))
    kuva
  }

  private def leveys(rivi: Int) = rivi / 2 * 2 + 1

  private def rivi(merkki: Char, montako: Int) = {
    val tyhjaaVasemmalle = (this.leveinKohta - montako) / 2
    val kokoRivi = " " * tyhjaaVasemmalle + merkki.toString * montako + "\n"
    kokoRivi
  }
}
Metodien sisäisiin algoritmitoteutuksiin ei pääse käsiksi mistään kyseisen metodin ulkopuolelta.

Kun siirrät hiiren kursorin seuraavien laatikoiden päälle, korostuvat mainittujen muuttujien käyttöalueet.

Parametrimuuttuja kuten merkki tai montako on määritelty koko kyseisen metodin ohjelmakoodissa. Sitä voi käyttää sieltä mistä vain.
Metodin koodissa uloimmalla tasolla määritelty paikallinen muuttuja, kuten tyhjaaVasemmalle, on käytettävissä määrittelykohdasta alkaen metodin koodin loppuun.
Vastaavasti on muuttujan kokoRivi kohdalla.
Kun ulompi käsky sisältää muuttujamäärittelyn, niin määritelty muuttuja on käytettävissä vain kyseisen ulomman käskyn sisällä. Esimerkiksi tässä muuttuja riviNro on määritelty vain for-silmukan sisällä.

Toinen esimerkki

Tässä vielä yksi (sinänsä täysin hyödytön) esimerkkikoodi, joka korostaa paikallisten muuttujien käyttöalueita:

def metodi(parametri: Int) = {
  println("Terve")
  var ulko = parametri * 2
  val joku = 1
  for (keski <- 1 to 3) {
    println("Moi")
    val joku = 10
    ulko += keski * 2 + joku
    if (keski < ulko + parametri) {
       val sisa = readLine("Anna luku: ").toInt
       ulko += sisa + joku
    }
  }
  ulko + joku
}
parametrin käyttöalue on koko metodi.
ulkon käyttöalue on koko metodi sen määrittelystä alkaen.
keski toimii vain silmukan sisällä.
sisa-muuttuja on käytettävissä vain if-käskyn sisällä.

Entä joku? Niitä on kaksi, mikä on sallittua sisäkkäisissä rakenteissa:

Sisempi joku-muuttuja on käytettävissä silmukan sisällä sen määrittelykäskyn jälkeen.
Ulompi joku on periaatteessa käytettävissä koko metodissa sen määrittelystä alkaen. Mutta: sisempi joku peittää (shadows) ulomman samannimisen muuttujan ja käytännössä poistaa for-silmukan sen käyttöalueesta. Niinpä...
... osa joku-nimistä viittaa sisempään muuttujaan...
... ja yksi ulompaan.

Esimerkistä on pääteltävissä yleinen periaate: käyttöalue rajoittuu sisimpään lohkoon (block), jossa muuttuja on määritelty. Epävirallisesti voimme sanoa, että lohko tarkoittaa ohjelman osaa, jossa on sisällä käskyjä, esimerkiksi for-silmukkaa tai if-käskyn haaraa. Lohkot voivat olla sisäkkäisiä; tyylikkäästi kirjoitetussa koodissa sisennykset auttavat hahmottamaan lohkorakenteen.

Käyttöalueen valitseminen muuttujalle

Nyrkkisääntö: Valitse mahdollisimman pieni toimiva käyttöalue.

Jos et keksi hyvää perustelua tehdä muuttujasta ilmentymämuuttujaa, tee siitä paikallinen muuttuja. Paikalliset muuttujat kannattaa sijoittaa sisimpään mahdolliseen lohkoon.

Käyttöalueen rajaaminen usein parantaa ohjelman luettavuutta ja muokattavuutta. Kun käyttöalue on kapea, on selvää, missä osissa koodia muuttuja on merkityksellinen eikä synny turhia riippuvuuksia koodin eri osien välillä. Pieni käyttöalue joskus myös vähentää turhaa muistinkäyttöä ja voi kasvattaa suoritusnopeuttakin.

Liiallinen ilmentymämuuttujien käyttö on tyypillinen aloittelevan olio-ohjelmoijan virhe. Tutkitaan nyt siksi käyttöalueiden näkökulmasta vielä yhtä esimerkkiä, luvusta 3.5 tuttua VendingMachine-limuautomaattiluokkaa. Siinä valinnat ilmentymä- ja paikallisten muuttujien välillä on tehty järkevästi:

class VendingMachine(var bottlePrice: Int, private var bottleCount: Int) {
  private var earnedCash = 0
  private var insertedCash = 0

  def sellBottle() = {
    if (this.isSoldOut || !this.enoughMoneyInserted) {
      None
    } else {
      this.earnedCash = this.earnedCash + this.bottlePrice
      this.bottleCount = this.bottleCount - 1
      val changeGiven = this.insertedCash - this.bottlePrice
      this.insertedCash = 0
      Some(changeGiven)
    }
  }

  def addBottles(newBottles: Int) = {
    this.bottleCount = this.bottleCount + newBottles
  }
  // ...
}
Limuautomaattiin päätyneen rahan määrä on automaatin tilaa kuvaava arvo, automaattiolion ominaisuus. Tästä arvosta tulee pitää kirjaa silloinkin, kun ei olla suorittamassa mitään automaattiluokan metodia. Näinpä se on syytä määritellä ilmentymämuuttujiksi. Sama pätee muihinkin tämän luokan ilmentymämuuttujiin.
Myyntimetodissa tarvitaan väliaikaisesti muuttujaa vaihtorahasumman tallentamiseen ennen sen palauttamista. Kyseessä on yksittäistä metodia suoritettaessa käytettävä tilapäissäilö, välitulos, joka ei kuvaa mitään pysyvämpää limuautomaatin tilasta eikä ole muiden metodien kannalta kiinnostava. Niinpä on järkevää käyttää paikallista muuttujaa.
Metodien parametriarvot ovat usein (kuten tässäkin) kiinnostavia vain paikallisesti.

Määrittele siis ilmentymämuuttujiksi vain muuttujat, joille pätee ainakin jokin seuraavista.

  • Muuttuja selvästi kuvaa oliolle kuuluvaa tietoa (henkilön nimi, opiskelijan kurssit).
  • Muuttuja tallentaa arvon, jonka täytyy pysyä tallessa silloinkin, kun ei olla suorittamassa mitään luokassa määriteltyä metodia.
  • Muuttuja liittyy johonkin resurssioptimointiin (jollaisia emme tavoittele tällä kurssilla).

Yhteenvetoa

  • for-silmukalla voi käydä vektorien ja puskurien alkioiden lisäksi läpi myös esimerkiksi lukuja ja merkkijonojen merkkejä.
  • Silmukoita voi laittaa sisäkkäin, jolloin sisempi silmukka tulee suoritetuksi jokaisella ulomman silmukan "kierroksella".
  • Mm. muuttujilla ja metodeilla on käyttöalue eli rajattu osa ohjelmasta, josta käsin niitä voi käyttää.
    • Käyttöalue kannattaa yleensä valita mahdollisimman pieneksi.
    • Paikallisten muuttujien käyttöalueeseen vaikuttaa metodin lohkorakenne.
  • Lukuun liittyviä termejä sanastosivulla: silmukka, for-silmukka, iteraatio; kokoelma, merkkijono; käyttöalue, lohko; yksisuuntainen lippu; debuggeri.

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.

Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.

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