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

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

Luku 3.5: Limsaa, jalkapalloa ja virheitä

Tästä sivusta:

Pääkysymyksiä: Millaisessa muodossa kurssilla jatkossa annetaan ohjelmointitehtävien tehtävänannot? Miten korjaan yleisiä virheitä Scala-koodista? Miten tulkitsen IntelliJ’n antamia virheilmoituksia?

Mitä käsitellään? Lisäharjoitusta if-valintakäskystä, luokkien laatimisesta ja muusta. Virheilmoituksia ja virheiden etsintää. Scaladoc-dokumentit spesifikaatioina.

Mitä tehdään? Tutustutaan ohjelmiin ja ohjelmoidaan. Ohjelmat vaativat lukuisten aiheiden ymmärtämistä ja liittämistä yhteen.

Suuntaa antava vaativuusarvio:

Suuntaa antava työläysarvio:? Kolme tuntia.

Pistearvo: A105.

Oheismoduulit: Miscellaneous (uusi), Football1 (uusi).

../_images/person05.png

Esimerkki: VendingMachine

Seuraava esimerkin teemana ovat virtuaaliset limuautomaatit. Toteutamme luokan, jollainen voisi hallinnoida yksinkertaisten kolikoilla toimivien limsanmyyntikoneiden toimintaa (leikisti; tässäkin alkeiskurssiesimerkissä on vedetty mutkat suoriksi).

Toteutamme VendingMachine-luokan pala palalta Scaladoc-dokumenttina (luku 3.2) annetun spesifikaation perusteella. Tässä hieman mutkikkaammassa luokassa yhdistyvät paitsi tuore if-asia myös moni muu aiempi teema.

Varsinaisesti uutta asiaa esimerkissä ei tule. Jos koet ymmärtäneesi tähänastiset kurssin asiat mainiosti, etkä ole väärässä, niin voit ohittaa tämän esimerkin ja siirtyä alla oleviin tehtäviin. (Voit myös vapaaehtoisena tehtävänä koettaa toteuttaa VendingMachine-luokan itse ennen kuin luet ratkaisun!)

Lue NYT dokumentaatiota

Tutustu Miscellaneous-moduulin luokan o1.soda.VendingMachine Scaladoc-dokumentaatioon.

Varmista, että ymmärrät millaisia metodeita VendingMachine-luokassa on ja miten niitä käytetään. Jatka vasta sitten eteenpäin tässä luvussa.

Luokka on toteutettu valmiiksi Miscellaneous-moduuliin, joten voit myös kokeilla sen käyttöä REPLissä, jos dokumentaatiosta jää epäselväksi, mitä metodit saavat aikaan. Toteutus esitellään alla.

Huomaa pakkaukset

Tähän asti olemme sijoittaneet lähes kaiken koodimme huolettomasti yleispakkaukseen o1. Yleisesti ottaen on parempi järjestää eri hankkeiden koodi omiin pakkauksiinsa, ja tästä eteenpäin teemmekin niin.

Huomaat pakkausjaon esimerkiksi Miscellaneous-moduulin sisältöä selatessasi.

Limuautomaatin tila ja sen alustaminen

Katsotaan ensiksi konstruktoriparametreja ja ilmentymämuuttujia:

class VendingMachine(var bottlePrice: Int, private var bottleCount: Int) {

  private var earnedCash = 0
  private var insertedCash = 0
Limuautomaattioliota luotaessa annetaan parametriksi pullon hinta. Automaattiolion on pidettävä kirjaa tästä tiedosta, joten määritellään myös ilmentymämuuttuja. bottlePrice-muuttuja on rooliltaan tuoreimman säilyttäjä; koska hintaa on tarkoitus muuttaa sijoituskäskyillä, niin otetaan se talteen var-muuttujaan (kuten dokumentaatiokin sanoo).
Toinen konstruktoriparametri on pullojen määrä. Tämäkin tieto vaihtuu pulloja lisättäessä ja ostettaessa; kokoojana toimiva var-muuttuja sopii tarkoitukseen. Tämä ilmentymämuuttuja on tarkoitettu vain luokan sisäiseen käyttöön, joten se olkoon private (luku 3.2). Huomaa, että voit kirjoittaa näkyvyysmääreen myös konstruktoriparametrin yhteyteen määritellylle ilmentymämuuttujalle.
earnedCash-muuttuja pitää kirjaa siitä, paljonko rahaa limuautomaatissa on eli paljonko automaatti on "ansainnut" viimeisen tyhjennyksen jälkeen. Tästä tiedosta on pidettävä kirjaa, jotta emptyCashbox-metodi saadaan toimimaan spesifikaation mukaisesti. earnedCash-muuttuja on vain luokan sisäiseen käyttöön eli private. Juuri luotu automaattiolio ei ole vielä ansainnut senttiäkään.
insertedCash-muuttuja pitää kirjaa siitä, montako senttiä limuautomaattiin on syötetty viimeisimmän oston jälkeen. Esimerkiksi jos on syötetty ensin 2 € ja sitten 1 €, eikä ole vielä ostettu pulloa, niin muuttujan arvon tulee olla 300. Tätä muuttujaa tarvitaan, jotta sellBottle-metodi saadaan toimimaan spesifikaation mukaisesti. Sekin on yksityinen ja alkuarvoltaan nolla.

Helppoja limumetoditoteutuksia

Viisi metodeista ovat toteutukseltaan varsin suoraviivaisia aiempien lukujen perusteella.

def addBottles(newBottles: Int) = {
  this.bottleCount = this.bottleCount + newBottles
}

def insertMoney(amount: Int) = {
  this.insertedCash = this.insertedCash + amount
}

def isSoldOut = this.bottleCount == 0

def enoughMoneyInserted = this.insertedCash >= this.bottlePrice

def emptyCashbox() = {
  val got = this.earnedCash
  this.earnedCash = 0
  got
}
addBottles- ja insertMoney-metodit yksinkertaisesti nostavat kokoojamuuttujien arvoja saamiensa parametriarvojen verran.
isSoldOut- ja enoughMoneyInserted-metodit kumpikin selvittävät tietyn seikan olion tiedoista vertailuoperaatiolla ja palauttavat näin saadun Boolean-tyyppisen arvon.
emptyCashbox-metodi nollaa automaatin ansiot ja palauttaa nollausta edeltäneen kokonaisansiomäärän. Tässä käytetään apuna paikallista muuttujaa tilioliosta (luku 2.2) tuttuun tapaan.

Ehkä on hyvä katsoa samaa koodia uudestaan palauttaen mieleen Scalan välimerkkikäytännöt:

def addBottles(newBottles: Int) = {
  this.bottleCount = this.bottleCount + newBottles
}

def insertMoney(amount: Int) = {
  this.insertedCash = this.insertedCash + amount
}

def isSoldOut = this.bottleCount == 0

def enoughMoneyInserted = this.insertedCash >= this.bottlePrice

def emptyCashbox() = {
  val got = this.earnedCash
  this.earnedCash = 0
  got
}
Kaksi ensimmäistä metodia vaikuttavat olion tilaan. Niiden määrittelyissä käytetään aaltosulkeita, vaikka määrittelyt ovatkin lyhyitä.
Kaksi seuraavaa lyhyttä metodia palauttavat arvon eivätkä vaikuta olioon mitenkään. Niiden määrittelyissä ei ole mitään sulkeita (mutta saisi laittaa).
Viides metodi vaikuttaa olion tilaan. Siksi siihen kirjataan paitsi aaltosulkeet myös tyhjät kaarisulkeet parametriluettelon paikalle, vaikka metodi ei yhtään parametria otakaan.

Välimerkkikäytäntöjä voit kerrata esim. kurssin tyylioppaasta.

toString-metodin toteutus

toString-metodissa pääsemme hyödyntämään ifelse-lauseketta. Tässä yksi toteutustapa:

override def toString = {
  "earned " + this.earnedCash / 100.0 + " euros, " +
    "inserted " + this.insertedCash + " cents, " +
    (if (this.isSoldOut) "SOLD OUT" else s"$bottleCount bottles left")
}
Tämän metodin runko on vain yksi pitkä merkkijonoja yhteen liittävä lauseke. Sen voi jakaa tähän tapaan usealle riville, kunhan katkaisee rivit plussien jälkeen eikä esimerkiksi kesken merkkijonon.
Viimeinen pala palautettavasta merkkijonosta muodostetaan sen perusteella, onko limut loppuunmyyty vai ei. Tämä if-lauseke toimii suuremman lausekkeen osana.
Sulkeet if-lausekkeen ympärillä ovat tarpeen, jotta tulee oikein määritellyksi, missä else-haara päättyy. (Vrt. pikkutehtävät luvun 3.4 alussa.)

Toinen toteutustapa

Alla on vertailun vuoksi toinen tapa toteuttaa tuo toString. Tässä käytetään paikallisia apumuuttujia ja upotetaan ne osaksi kuvausta.

override def toString = {
  val earnings = this.earnedCash / 100.0
  val bottleStatus = if (this.isSoldOut) "SOLD OUT" else s"$bottleCount bottles left"
  s"earned $earnings euros, inserted $insertedCash cents, $bottleStatus"
}

sellBottle-metodin toteutus

Tässä eräs toimiva toteutustapa sellBottle-metodille, jonka tehtävänä on "myydä pullo" eli muuttaa automaatin raha- ja pullotietoja sekä palauttaa joko vaihtorahan määrä tai negatiivinen luku myynnin epäonnistumisen merkiksi.

def sellBottle() = {
  if (this.isSoldOut) {
    -1
  } else if (!this.enoughMoneyInserted) {
    -1
  } else {
    this.earnedCash = this.earnedCash + this.bottlePrice
    this.bottleCount = this.bottleCount - 1
    val changeGiven = this.insertedCash - this.bottlePrice
    this.insertedCash = 0
    changeGiven
  }
}
Ensin tarkistetaan pullojen määrä isSoldOut-metodilla. Loppuunmyynnin tapauksessa päädytään palauttamaan -1.
Pullomäärän ollessa kunnossa jatketaan tarkastamalla, onko automaatissa riittävästi rahaa. Huomaa else if-ketjutus.
Muista: huutomerkki on not-operaattori, joka kääntää totuusarvon: tässä katsotaan, onko niin, että rahat eivät riitä. Jos on, palautetaan -1.
Olion tilaa muuttavat rivit suoritetaan vain, jos molemmat tarkistukset "menivät läpi". Metodi lisää automaatin ansioita, vähentää pullon, laskee vaihtorahat, nollaa syötetyt rahat ja lopuksi palauttaa vaihtorahasumman.
Huomaa vielä: metodin palautusarvon määrää viimeiseksi suoritettava käsky. Kun koodissa on "haaroja", se ei välttämättä ole ohjelmatekstissä viimeisenä oleva koodirivi. Tässä esimerkissä on kolme eri riviä, joista mikä tahansa voidaan tilanteesta riippuen suorittaa viimeisenä.

Limuautomaattitoteutuksen laadusta

Yllä oleva sellBottle-toteutus kyllä toimii spesifikaation mukaisesti. Mutta se saattaa sinustakin näyttää peräkkäisine if-käskyineen aavistuksen verran ikävältä. Koodi ei myöskään ole aivan "DRY" eli toisteeton (luku 1.4), koska siinä toistuu sama kohta — -1:n palauttaminen — turhaan kahdesti.

Lisäksi voimme kyseenalaistaa, onko hyvä ajatus palauttaa negatiivinen luku merkkinä siitä, ettei pulloa saatu myytyä — vaikka negatiivisen luvun käyttö epäonnistumisen merkkinä perinteinen ohjelmointikikka onkin. Tämä nimittäin avaa mahdollisuuden huolimattomuusvirheelle, jossa metodin kutsuja ei muista tarkistaa arvon etumerkkiä ja käsittelee palautettua negatiivista lukua kuin se olisi saadun vaihtorahan määrä ja osto olisi onnistunut.

Tulet oppimaan kurssilla tekniikoita, joilla sellBottle-metodia voi parantaa. Voit pitää tämän metodin ja nämä laatukysymykset mielessä lukiessasi myöhempiä lukuja.

Dokumentit spesifikaatioina tällä kurssilla

Scaladoc-dokumenteilla on kurssillamme keskeinen rooli, sillä niitä käytetään monien harjoitustehtävien tehtävänannoissa kahdella tavalla:

  1. Monissa harjoituksissa sinun on tarpeen käyttää yhtä tai useampaa luokkaa tai yksittäisoliota, jotka on laadittu kurssia varten ja joiden kuvaukset on annettu Scaladoc-muodossa. Saat siis sekä toimivan ohjelmakoodin että sen rajapinnan kuvauksen eli "käyttöohjeen" scaladocina.
  2. Sinulle annetaan monissa harjoituksissa yhden tai useamman luokan tai yksittäisolion rajapinnan kuvaus scaladoceina, mutta ei vastaavaa ohjelmakoodia (ainakaan kokonaan). Tehtäväksesi jää kirjoittaa sellainen Scala-koodi, joka vastaa annettua dokumentaatiota. Scaladoc-dokumentit siis toimivat spesifikaationa laadittavalle ohjelmalle; ajatus on sama kuin VendingMachine-esimerkissä äsken.

Tehtävissä käytetyt Scaladoc-dokumentit on lähes poikkeuksetta kirjoitettu englanniksi.

Muuttujat Scaladoc-pohjaisissa tehtävissä

Kuten luku 3.2 kertoi, yksityiset muuttujat eivät kuulu luokan julkiseen rajapintaan eivätkä siksi tule mukaan Scaladoc-dokumentteihin, joiden tehtävä on kuvata luokan käyttötapaa luokan ulkopuolelta katsottuna.

Niissä kurssin tehtävissä, joissa annetaan Scaladoc-muotoinen spesifikaatio, tämä tarkoittaa käytännössä sitä, että dokumenteissa näkyvät ilmentymämuuttujat sinunkin tulee määritellä julkisiksi laatimaasi ohjelmakoodiin (kuten bottlePrice äsken). Toisaalta muita julkisia ilmentymämuuttujia kuin dokumentaation määräämät ei luokalla tulisi olla.

Ei ole epätavallista, että tarvitset luokan sisäiseen tilakirjanpitoon muita muuttujia kuin scaladocin kuvaamat julkiset muuttujat, mutta tee noista lisämuuttujista yksityisiä (kuten esim. bottleCount- ja earnedCash-muuttujista äsken).

Huomaa lisäksi, että julkisen ilmentymämuuttujan vaihtaminen valista variksi tarkoittaa, että tuohon muuttujaan voi sijoittaa arvon luokan ulkopuolelta. Sekin siis muuttaa julkista rajapintaa. Jos dokumentaatio pyytää valin, tee val. Yksityisten muuttujien osalta voit valita val´\in tai `varin mutta val on yleensä parempi vaihtoehto (luku 1.4).

Pikkutehtävä

Mitkä seuraavista väittämistä pitävät paikkansa? Tee tarvittaessa sivistynyt arvaus ja lue saamasi palaute.

Pääseekö private-tietoihin siis jotenkin käsiksi?

(Seuraavaa ei tarvitse osata kurssilla. Tai usein muutenkaan.)

private-määre estää muuttujan (tai metodin) tavallisen käytön luokan ulkopuolelta, ja sillä merkitään, ettei muuttujaa pitäisi käyttää ulkopuolelta missään normaaliolosuhteissa. Kuitenkin kuten äskeinen tehtäväkin antoi ymmärtää, private ei ole aivan ehdoton este.

Tarkastellaan vaikkapa VendingMachine-luokkaa. Yksi sen yksityisistä muuttujista oli earnedCash, jonka arvoa säätelivät julkiset metodit. Tämä ei onnistu, kuten ei pidäkään:

val machine = new VendingMachine(250, 10)machine: o1.soda.VendingMachine = earned 0.0 euros, inserted 0 cents, 10 bottles left
machine.earnedCash = 123456<console>:9: error: variable earnedCash in class VendingMachine cannot be accessed in o1.soda.VendingMachine
     machine.earnedCash = 123456
             ^

Mutta seuraava onnistuu.

val accessToCurrentValue = machine.getClass.getDeclaredField("earnedCash")accessToCurrentValue: java.lang.reflect.Field = private int o1.soda.VendingMachine.earnedCash
accessToCurrentValue.setAccessible(true)accessToCurrentValue.set(machine, 123456)machine.emptyCashbox()res0: Int = 123456

Tässä siis vaihdoimme earnedCash-muuttujalle mielivaltaisen arvon ohittamalla rajapinnan pienellä kikkailulla. Tätä ei kuitenkaan tule tehtyä vahingossa.

Tehtävä: tunnista vääriä

Nouda IntelliJ’hin moduuli Football1, joka kuvaa jalkapallo-ottelujen tuloskirjanpitoa. Tämä moduuli sisältää kaksi luokkaa — Match ja Club — sekä käynnistysolion MatchTest.

Luokka Match käyttää luokkaa Club: kuhunkin otteluun liittyy kaksi ottelevaa seuraa. MatchTest sisältää testikoodia, jolla voi koekäyttää luokkia.

Tehtävänanto

Tutustu moduulin Scaladoc-dokumentaatioon sekä ohjelmakoodiin. (Muistutus: löydät dokumentaation IntelliJ’stä moduulin doc-kansiosta sekä selaimella luvun alun linkistä.)

Koodia tutkiessa osoittautuu, että:

  • Siinä on syntaksivirheitä ("kielioppivirheitä"; ks. luku 1.8), jotka estävät luokkien käytön tyystin.
  • Osa dokumentaation kuvaamista metodeista puuttuu. Eikä siinäkään vielä kaikki:
  • Koodissa on myös toiminnallisia bugeja. Osa Match-luokan metodeista on kieliopillisesti oikein muttei toimi niin kuin dokumentaatio kuvaa.

Tehtäväsi on:

  • korjata virheet ja täydentää puuttuvat metodit luokista Match ja Club niin, että luokat vastaavat annettua spesifikaatiota,
  • korjata MatchTest-testiohjelma toimivaksi eli sellaiseksi, että se käyttää Match-luokkaa korrektisti ja ohjelmassa olevien kommenttien mukaisesti,
  • täydentää tätä testiohjelmaa niin, että se testaa luokkia kattavammin (parhaaksi katsomallasi tavalla), ja
  • varmistaa testiohjelman avulla, että kaikki pelaa.

Ohjeita ja vinkkejä

Virheiksi on valittu sellaisia, joita ohjelmoinnin opiskelijat joskus tekevät. Niiden korjaamiseen kannattaa suhtautua ajatuksella, jotta osaat välttää tai ainakin korjata vastaavat tulevissa tehtävissä, joissa kirjoitat enemmän omaa koodia.

Alla on vaiheittainen opastus tehtävään.

Luithan varmasti Scaladoc-dokumentaation ensin? Silmäilitkö myös annettua koodia?

Vaihe 1/12: Club

Paina Football1-moduulin tai jonkin sen tiedostoista ollessa valittuna F10 (tai valikosta Build → Build Module 'Football1'). IntelliJ’n Messages-välilehti ponnahtaa esiin.

Aluksi siellä näkyy ainoastaan pari luokkaan Club liittyvää virheilmoitusta sekä yksi varoitus luokasta Match. Älä anna tämän hämätä; virheitä on kyllä muuallakin. Nyt vain on käynyt niin, että Club-luokassa oleva virhe on sen sorttinen, että IntelliJ ei edes luettele muita ongelmia ennen kuin Club korjataan. Ei ole harvinaista, että ohjelmassa oleva virhe peittää taakseen toisia virheitä.

Aloitetaan siis Club-luokasta. IntelliJ’n mukaan toinen virheistä on class-alkuisella rivillä ja toinen ihan tiedoston lopussa. Tätä ei kannata tulkita näin kirjaimellisesti. Kun ohjelmointityökalut yrittävät jäsentää ohjelmaa osiinsa, ja jokin menee pieleen (koska ohjelma ei ole sääntöjen mukainen), niin virhe usein häiritsee myös ohjelman loppuosan jäsentämistä. Yksikin jäsennysvirhe voi saada koko ohjelman vaikuttamaan tietokoneen näkökulmasta monin tavoin virheelliseltä. Etenkin sulkeiden ja muiden välimerkkien virheellinen käyttö sotkee jäsennyksen monesti täysin.

Club-luokassakin on oikeastaan vain yksi pieni virhe, joka tosin toistuu kahdesti samalla rivillä. IntelliJ’n virheilmoitus ':' expected but ',' found antaa osviittaa siitä, mistä on kyse. Virheilmoituksen voi vapaasti suomentaa: "Tähän piti tulla kaksoispiste, mutta vastaan tulikin (jo) pilkku." Punainen korostus osoittaa konstruktoriparametrien val name, val stadium välistä pilkkua.

Miksi rivillä pitäisi olla kaksoispiste? Mitä muuta siitä vielä puuttuu? Keksinet sen itsekin. Jos et, niin vertaa tätä määrittelyä aiempin esimerkkien luokkamäärittelyihin. Korjaa nyt virhe, niin jatketaan.

Monesti virheilmoitukset ovat vähemmän selviä, ja niitä voi joutua pohtimaan vähän pidempään tai selvittelemään vaikkapa googlitse. Harjoitus tekee mestarin.

Vaihe 2/12: isHigherScoringThan

Käännä koodi uudestaan (F10). Virheilmoitusluettelo päivittyy. Nyt punaista näkyy tulevan luokasta Match ja erityisesti testiohjelmasta MatchTest.

Aloitetaan Match-luokasta. Tuplaklikkaa jompaakumpaa Messages-välilehdellä näkyvää Error-ilmoitusta, niin löydät rivin, jolta ne molemmat ovat peräisin.

IntelliJ’n mukaan koodi this.totalGoals(anotherMatch) on virheellinen, ja niinhän se onkin. Huomaatko miksi? Korjaa virhe ja toinen vastaava samasta metodista.

Entä se virheilmoitus? IntelliJ’hän valitti tuosta metodikutsusta, että Int does not take parameters. Valitus voi äkkiseltään vaikuttaa vielä oudommalta kuin itse virheellinen koodinpätkä, mutta löytyy siitä tolkku, jos sen osaa tulkita.

totalGoals-metodihan palauttaa kokonaisluvun. Metodi on parametriton, joten this.totalGoals on lauseke, jonka arvona on jokin Int-tyyppinen arvo. Scala-työkalusto siis tulkitsee, että this.totalGoals(anotherMatch) on yritys "kutsua Int-arvoa parametrilla anotherMatch", missä ei ole järkeä; siksi yllä mainittu virheilmoitus.

Vaihe 3/12: goalDifference

Varoitusilmoituksista

Monet ohjelmointityökalut, IntelliJ mukaanlukien, huomauttavat joskus epäilyttävästä koodista varoituksilla (warning). IntelliJ’n Messages-välilehdellä varoitukset näyttävät pitkälti samalta kuin virheilmoituksetkin; ne erottaa keltaisesta huutomerkkikolmiosta warning.

Varoitus tarkoittaa, että koodissa on luultavasti (muttei 100-prosenttisen varmasti) jotain, joka on syytä korjata. Varoituksilla työkalu sanoo ohjelmoijalle: "Ei taida kannattaa tehdä noin?"

Kun IntelliJ varoittaa jostakin, se on usein oikeassa ainakin siinä, että jotain ohjelmassa kannattaa muuttaa. Automaattinen varoitusilmoitus ei vain välttämättä suoraan tai oikein ilmaise sitä, mitä kannattaa muuttaa.

Käytännössä hyvä nyrkkisääntö on suhtautua varoitusilmoituksiin samalla vakavuudella kuin virheilmoituksiinkin.

IntelliJ antaa Match-luokasta varoituksen procedure syntax is deprecated ja ehdottaa Unit-sanan lisäämistä goalDifference-metodin määrittelyyn, jotta olisi selvempää, että metodi palauttaa Unitin. Varoitus on aiheellinen mutta ehdotus huono.

Laita hiiren kursori goalDifference-nimen päälle. Esiin tulevasta kuvauksesta näet metodin nyt palauttavan Unit. Toisin kuin varoitusilmoituksen ehdotuksessa oletettiin, emme kuitenkaan halua metodin palauttavan Unit vaan Int. Ongelman syy on aiemmista tehtävistä tuttu välimerkkivirhe metodin määrittelyssä.

Korjaa ja käännä koodi uudestaan. Nyt näyttäisi siltä, että Match on kunnossa. Mutta älä vielä huokaise helpotuksesta siltäkään osin.

Vaihe 4/12: MatchTest ja otteluiden luominen

Yksi valituksista kohdistuu käskyyn new Match(club2, club1) yksittäisoliossa MatchTest.

Mutta sehän näyttää olevan aivan kunnossa. Se on aivan kunnossa.

Tämä on hyvä esimerkki siitä, ettei virheilmoitus aina osoita siihen paikkaan, jossa varsinainen virhe on. Virheilmoitus osoittaa nyt MatchTest-luokan ohjelmakoodiin ja ilmoittaa, ettei se toimi. Ja eihän se toimikaan, mutta syy on siinä, että Match-luokka on määritelty virheellisesti ja MatchTest yrittää käyttää sitä tavalla, jolla käytön pitäisi onnistua.

Type mismatch on virheilmoitus, josta on usein helppo päätellä, missä virhe on. Tässäkin tapauksessa virheilmoitus kertoo meille varsin selvästi, mistä on kyse. Lue se ja vertaa keskenään virheilmoituksen osoittamaa riviä, Match-luokan Scaladoc-dokumentaatiota ja annettua Match-toteutusta.

Korjaa virhe. Paina tämänkin tehtyäsi F10 ja muista sama myös edetessäsi seuraaviin vaiheisiin.

Vaihe 5/12: addAwayGoal

Tarkastellaan seuraavaksi virheilmoituksia, jotka kertovat, että value addAwayGoal is not a member of o1.football1.Match. Siis: "Mitään addAwayGoalia ei löydy Match-luokasta."

Häh? Onhan siellä koodissa tuo metodi; katso vaikka itse.

Katsoitko tarkkaan? Ja huomasitko virheilmoituksen kysymyksen: did you mean AddAwayGoal?

Tämänlaisen virheilmoituksen saadessaan kannattaa aina tarkistaa oikeinkirjoitus. Kirjoitusvirhe voi olla kutsuvassa koodissa tai kutsutussa koodissa.

Vaihe 6/12: homeCount ja awayCount

Sitten: variable awayCount in class Match cannot be accessed ja sama homeCount-muuttujalle.

Tämäkin virheilmoitus on varsin onnistunut. Se kertoo, ettei mainittuihin Match-olion muuttujiin pääse käsiksi MatchTest-oliosta.

Jos et jo aiemmin huomannut, miten Match-luokassa on muuttujat homeCount ja awayCount sekä metodit homeGoals ja awayGoals, niin kiinnitä siihen huomiota nyt. Millaisesta virheestä on kyse? Onko virhe Matchin vai MatchTestin koodissa?

Korjaa virhe.

Vaihe 7/12: totalGoals

Pari virheilmoitusta liittyy match1.totalGoals()-metodikutsuihin. Ilmoitus on tutunlainen (hieman ylempää): Int does not take parameters. Samoin sen syy on tuttu, vaikka tässä ei metodille tarjota varsinaisia parametreja vaan vain tyhjä parametriluettelo (luku 2.6).

Korjaa.

Vaihe 8/12: Match-olion luominen taas

Eräs MatchTest-ohjelman riveistä saa IntelliJ’n parkaisemaan: not found: value Match. Tämä tarkoittaa suunnilleen: "En osaa määrittää arvoa lausekkeelle Match."

Virheilmoitus ei ole sieltä selkeimmästä päästä tällä rivillä olevan virheen selventäjänä. Se kuitenkin antaa vihjeen siitä, että Scala-työkalusto on yrittänyt tulkita yksittäistä sanaa Match lausekkeena (esim. muuttujan tai metodin nimenä). Niinhän tässä ei haluta tapahtuvan, vaan olisi tarkoitus luoda uusi Match-olio. Osannet korjata asian.

Vaihe 9/12: isGoalless

Virhe: type mismatch; found: AnyVal, required: Boolean. Ilmoitus osoittaa isGoalless-kutsuihin MatchTestissä.

Selvästikin if-käskyyn tarvitaan Boolean-tyyppinen lauseke. Jostain syystä isGoalless ei näytä sellaista tuottavan, vaan saamme jotakin AnyVal-tyyppistä, mitä se sitten tarkoittaakin.

Pystytkö selvittämään tai arvaamaan, mikä isGoalless-metodissa on vikana? Saatko ongelman korjattua? Saatko toteutuksesta hyvin yksinkertaisen?

Jos ongelman ymmärtäminen ei meinaa onnistua, katso alempaa Lisää virhetilanteita: arvojen palauttaminen ja valintakäsky; tuo kappale käsittelee juuri tällaisia virheitä.

Vaihe 10/12: location

Viimeiset käännösaikaiset virheet oliosta MatchTest johtuvat jälleen Match-luokan vajavaisuudesta. Ilmoitus value location is not a member of o1.football1.Match kertoo tässä, että location-metodi on jäänyt Match-luokasta määrittelemättä. Ilmoitus on samanlainen kuin yllä addAwayGoal-metodista, mutta syy on nyt erilainen: tämä metodi tosiaan puuttuu.

Kirjoita toteutus location-metodille.

Vaihe 11/12: Testaa

Ohjelman pitäisi nyt olla ajokunnossa. Aja MatchTest. Mieti, mitä sen pitäisi tulostaa ja mitä se todellisuudessa tulostaa.

Huomaat, että osa tulosteista on kunnossa, mutta kaikki ei ole hyvin. Match-luokka toimii edelleen osin dokumentaation vastaisesti, vaikka IntelliJ ei luokasta enää virheitä löydäkään.

Alla on vinkkejä puutteiden korjaamiseksi.

Vaihe 12/12: Korjaa ja täydennä

Korjaa Match-luokkaan jääneet puutteet: yksi virheellinen metodi ja puuttuva toString.

Ohjeita ja vinkkejä:

  • Muokkaa MatchTest-ohjelmaa parhaaksi katsomallasi tavalla virhettä etsiessäsi.
  • Muistutus (viimeisen kerran tästä): Scaladoceista on syytä avata täydet kuvaukset metodien nimiä klikkaamalla.
  • Jos toString-metodin toteuttaminen tuottaa vaikeuksia, kannattaa:
    1. Varmistaa, että luit metodin koko Scaladocin.
    2. Varmistaa, että käytit override-sanaa alussa.
    3. Katsoa VendingMachine-luokan toString-metodi, josta on parikin eri toteutusta yllä.
    4. Lukea jo tässä välissä loput tästä luvusta. Kyse voi olla jostakin alempana esitellystä if-käskyihin liittyvästä virhetilanteesta.
    5. Pyytää apua, jos nuo vinkit eivät auttaneet.

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

Lisää virhetilanteita: muuttujat ja valintakäsky

Tutkitaan luvun lopuksi lisää koodia, joka ei toimi. Seuraavat esimerkit voivat parantaa ymmärrystäsi if-käskyistä ja vähän muustakin. Ne voivat myös auttaa välttämään yleisiä virheitä.

Eräs taannoinen kurssinkävijä laati harjoituksen vuoksi tällaisen min-funktiota muistuttavan koodinpätkän ja ihmetteli saamaansa virheilmoitusta. Moni muukin on tehnyt vastaavan virheen.

def palautaPienempi(eka: Int, toka: Int) = {
  if (eka < toka) {
    var palautetaan = eka
  } else {
    var palautetaan = toka
  }
  palautetaan
}
Koodi tuottaa käännösaikaisen virheilmoituksen: not found: value palautetaan. Eli muuttuja palautetaan ei ilmeisesti olisi määritelty tuossa viimeisellä rivillä.
Mutta johan se palautetaan-muuttuja on sinne määritelty, peräti kahteen kertaan?

Ongelma on tämä: palautetaan on määritelty if-käskyn haarojen sisällä. Kukin tällainen määrittely kattaa vain kyseisen haaran, eikä (kumpikaan) muuttuja ole käytettävissä if-käskyn ulkopuolella.

Jos muuttujan haluaa olevan käytettävissä myös if-käskyn jälkeen, se on määriteltävä if-käskyn ulkopuolella. Esimerkiksi tämä toimii:

def palautaPienempi(eka: Int, toka: Int) = {
  val palautetaan = if (eka < toka) eka else toka
  palautetaan
}

Tähän teemaan eli muuttujien käyttöalueisiin (scope) palataan luvussa 5.6.

Äskeisessä esimerkissähän ei tosin edes ole välttämätöntä tallentaa välitulosta muuttujaan. Tässä vielä yksinkertaisempi toteutus:

def pienempi(eka: Int, toka: Int) = if (eka < toka) eka else toka

Lisää virhetilanteita: arvojen palauttaminen ja valintakäsky

Miksei tämä toimi?

Sanotaan, että haluamme laatia funktion, joka palauttaa parametriksi saamansa kokonaisluvun neliön, jos annettu luku on positiivinen. Negatiivisen parametriarvon tapauksessa neliön sijaan palautetaankin nolla. Funktion parametrin tyyppi olisi Int ja paluuarvon samaten.

Seuraava toteutus on aloittelijalle varsin tyypillinen. Siinä on selkeästi ilmaistu molemmat tapaukset erillisillä if-käskyillä:

def kokeilu(luku: Int) = {
  if (luku > 0) luku * luku
  if (luku <= 0) 0
}

Onko tässä sitten jotain vikaa? Kokeillaan REPL:issä:

def kokeilu(luku: Int) = {
  if (luku > 0) luku * luku
  if (luku <= 0) 0
}kokeilu: (luku: Int)AnyVal

Funktion määrittely on laillista Scalaa. Mutta hyvin ei käy, kun koetamme vaikkapa käyttää funktion palautusarvoa laskutoimituksessa:

7 + kokeilu(10)7 + kokeilu(10)
  ^
<console>:12: error: overloaded method value + ...
cannot be applied to (AnyVal)

Virheilmoitus voidaan suomentaa suunnilleen niin, että "plus-operaattoria ei ole määritelty tietotyypille AnyVal". Mikä ihmeen AnyVal?

Ongelman selitys

Virheilmoitus on vähän outo mutta aiheellinen.

Katsotaan koodiamme tarkasti ja muistaen, että palautusarvo on viimeiseksi evaluoitavan lausekkeen arvo.

def kokeilu(luku: Int) = {
  if (luku > 0) luku * luku
  if (luku <= 0) 0
}
Kokeilufunktiossamme ensin evaluoidaan lauseke, jonka arvo on positiivisen luvun tapauksessa tuon luvun neliö. Muussa tapauksessa tällä ensimmäisellä if-käskyllä "ei ole arvoa", eli sen arvo on sisällötön Unit-arvo!
Sitten evaluoidaan jälkimmäinen käsky, jonka arvo on vastaavasti joko Int-arvo nolla tai Unit.
Jälkimmäisenä saatu arvo on koko funktion palautusarvo. Edeltävä if-rivi oli itse asiassa ihan merkityksetön.

Koska näin (huonosti) määritellyn funktion palautusarvo voi tilanteesta riippuen olla joko tyyppiä Int tai Unit, niin Scala-työkalusto päättelee koko funktion tyypiksi AnyVal, joka tarkoittaa suunnilleen "joku arvo". Koska yhteenlasku on määritelty vain kahden luvun välille eikä luvun ja "jonkun arvon", niin saimme virheilmoituksen käskystä 7 + kokeilu(10).

Toimiva versio

No, miten se sitten saadaan toimimaan?

def kokeilu(luku: Int) =
  if (luku > 0) luku * luku else 0
Nyt viimeiseksi evaluoitava lauseke on tämä ainoa if-käsky, jonka arvo Int-tyyppinen valittiin sitten kumpi haara vain.

Lisää AnyValista aikanaan luvussa 7.3.

Entä tämä ratkaisu?

Tässä vielä yksi versio funktiostamme. Tarkoitus on siis, että funktion palautusarvon tyyppi on Int, ja funktio palauttaa tapauksesta riippuen joko neliön tai nollan. Koeta itse päätellä (tai kokeilla REPLissä), toimiiko se, ja lue vastauksesta saamasi palaute.

def kokeilu(luku: Int) =
  if (luku > 0) luku * luku else if (luku <= 0) 0

Pari käytännön vinkkiä virheenetsintään

Jos saat hämmentäviä virheilmoituksia, kannattaa katsoa tarkasti, mitkä ovat kunkin lausekkeen ja palautusarvon tietotyypit. Tämä kannattaa usein muutenkin mutta erityisesti silloin, jos ilmoituksessa näkyy sana Any ja olet käyttänyt if-valintakäskyä.

REPLissä tyypit näkyvät helposti tulosteissa. IntelliJ’n koodieditorissa voit pitää hiiren kursoria ohjelman osan päällä, niin näkyviin tulee tuon osan tietotyyppi. Kokeile! (Liikuta hiirtä koodin päällä ja katso tyyppitietoja.)

Lisäkikka: ohjelmointityökalut osaavat joissakin tapauksissa antaa selkeämpiä virheilmoituksia, jos kirjaat funktiollesi palautusarvojen tyypin erikseen (luvun 1.8 mukaisesti):

def kokeilu(luku: Int): Int = // jne.

Tyypin saa kirjata näin vaikka kaikille funktioille. Palautusarvojen vapaaehtoinen kirjaaminen voi muutenkin selkiyttää ohjelmia ja vähentää virheitä. Monet Scala-ohjelmoijat merkitsevät palautusarvon tyypin kaikkiin julkisiin metodeihin (mitä sinun kuitenkaan ei ole pakko O1:ssä tehdä).

Yhteenvetoa

  • Kurssilla Scaladoc-dokumentteja käytetään usein tehtävänannoissa.
  • Ohjelmointityökalujen antamat virheilmoitukset ovat usein epäselviä, mutta niitä voi tulkita ja harjoittelu auttaa.
  • Kun if-käskyn haarat kattavat kaikki mahdolliset tapaukset, on syytä käyttää else-sanaa ilman ehtolauseketta, jotta asia tulee ohjelmointityökaluillekin selväksi.
  • Lukuun liittyviä termejä sanastosivulla: dokumentaatio, Scaladoc; if; käännösaikainen virhe, syntaksivirhe.

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...