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

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

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

../_images/person05.png

Esimerkki: VendingMachine

Seuraava esimerkin teemana ovat virtuaaliset limuautomaatit. Toteutamme luokan, jollainen voisi hallinnoida yksinkertaisten 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 ohittaakin 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-projektin 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 projektiin Miscellaneous, 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 nyt ja tulevaisuudessa

Tähän asti kurssilla 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-projektin sisältöä selatessasi. Muista asia myös REPLissä kokeillessasi: esimerkiksi VendingMachine-luokka on pakkauksessa o1.soda ja pitää siis ottaa käyttöön asianmukaisella import-käskyllä. Sama koskee myös pakkauksen o1.football1 sisältöä myöhemmin tässä luvussa.

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ä (luku 2.6); 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 (luku 2.6) 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 aaltosulkuja, 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:

override def toString = {
  "earned " + this.earnedCash / 100.0 + " euros, " +
    "inserted " + this.insertedCash + " cents, " +
    (if (this.isSoldOut) "SOLD OUT" else this.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.
Sulut if-lausekkeen ympärillä ovat tarpeen, jotta tulee oikein määritellyksi, missä else-haara päättyy. (Vrt. pikkutehtävät luvun 3.4 alussa.)

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 kirjoitettu pääsääntöisesti englanniksi.

Yksityiset muuttujat Scaladoc-pohjaisissa tehtävissä

Kertaus luvusta 3.2: 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).

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 Eclipseen projekti Football1, joka kuvaa jalkapallo-ottelujen tuloskirjanpitoa. Projekti 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 projektin Scaladoc-dokumentaatioon sekä ohjelmakoodiin. (Muistutus: löydät dokumentaation Eclipsessä projektin sisältä 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 siten, 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 siten, 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/11: Club

Katso Eclipsen Problems-välilehteä. Aluksi siellä näkyy ainoastaan luokkaan Club liittyviä virheilmoituksia. Ä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ä Eclipse ei edes luettele tuolla välilehdellä muita ongelmia ennen kuin Club korjataan. Ei ole harvinaista, että ohjelmassa oleva virhe peittää taakseen toisia virheitä.

Aloitetaan siis Club-luokasta. Eclipsen mukaan luokassa on jopa kolme virhettä, joista osa on class-alkuisella rivillä ja osa 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ä. Erityisesti 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. Eclipsen 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/11: isHigherScoringThan

Tallenna. Virheilmoitusluettelo päivittyy. Nyt punaista näyttää tulevan sekä luokasta Match että testiohjelmasta MatchTest.

Aloitetaan Match-luokasta. Klikkaa vaikkapa kyseistä Problems-välilehdellä näkyvää virheilmoitusta, niin löydät rivin, josta se on peräisin.

Virheellinen koodi on Eclipsen mukaan this.totalGoals(anotherMatch). Tämä metodikutsu on tosiaan virheellinen. Huomaatko miksi? Korjaa virhe ja toinen vastaava samasta metodista.

Entä se virheilmoitus? Eclipsehän valitti tuosta metodikutsusta, että Int does not take parameters. Virheilmoitus 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.

Tämä korjattuasi — ja muista taas tallentaa — näyttäisi siltä, että Match on kunnossa. Mutta älä vielä huokaise helpotuksesta siltäkään osin.

Vaihe 3/11: MatchTest ja otteluiden luominen

Yksi valituksista kohdistuu käskyyn new Match(club2, club1) yksittäisoliossa MatchTest. Mitäs vikaa siinä on? Sehän on aivan oikeaoppisen näköinen.

Tämä on hyvä esimerkki siitä, että virheilmoitus ei aina osoita siihen paikkaan, jossa varsinainen virhe on. Virheilmoitus osoittaa nyt MatchTest-luokan ohjelmakoodiin ja ilmoittaa, että se ei 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.

Vaihe 4/11: 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?

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

Vaihe 5/11: 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 6/11: 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 7/11: Match-olion luominen taas

Eräs MatchTest-ohjelman riveistä saa Eclipsen 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 helposti korjata tilanteen.

Vaihe 8/11: 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 ja samalla yksinkertaistettua metodin toteutusta? Jos ei meinaa onnistua, katso alempaa Lisää virhetilanteita: arvojen palauttaminen ja valintakäsky; tuo kappale käsittelee juuri tällaisia virheitä.

Vaihe 9/11: 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 10/11: 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. Osa Match-luokan metodeista toimii edelleen dokumentaation vastaisesti, vaikka Eclipse ei luokasta enää virheitä löydäkään.

Alla on vinkkejä puutteiden korjaamiseksi.

Vaihe 11/11: Korjaa ja täydennä

Korjaa Match-luokkaan jääneet virheet. Kirjoita scaladocien mukaiseksi myös puuttuva toString-metodi.

Ohjeita ja vinkkejä:

  • Tulosteeseen ilmestyy (), kun tulostetaan Unit-palautusarvo (ks. luku 1.6).
  • Muokkaa MatchTest-ohjelmaa parhaaksi katsomallasi tavalla virheitä 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. Lukea jo tässä välissä loput tästä luvusta. Kyse voi olla jostakin alempana esitellystä if-käskyihin liittyvästä virhetilanteesta.
    4. Pyytää apua, jos nuo vinkit eivät auttaneet.

Palauttaminen

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

Vähän varoitusilmoituksista

Monet ohjelmointityökalut, Eclipse mukaanlukien, huomauttavat joskus epäilyttävästä koodista varoituksilla (warning). Eclipse merkitsee varoitukset kuten käännösaikaiset virheilmoituksetkin, paitsi että värinä on keltainen eikä punainen. Et ole ehkä vielä törmännyt varoitusilmoitukseen, mutta ennemmin tai myöhemmin epäilemättä törmäät.

Varoitus tarkoittaa, että koodissa on luultavasti (muttei ihan 100-prosenttisen varmasti) jotain, joka on syytä korjata. Varoituksilla Eclipse sanoo ohjelmoijalle: "Ei taida kannattaa tehdä noin?" Kun Eclipse varoittaa jostakin, se on erittäin usein oikeassa ainakin siinä, että jotain ohjelmassa kannattaa muuttaa. Automaattinen varoitusilmoitus ei vain välttämättä suoraan ilmaise sitä, mitä kannattaa muuttaa.

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

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

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, jos ilmoituksessa näkyy sana Any ja olet käyttänyt if-valintakäskyä.

REPLissä tyypit näkyvät helposti tulosteissa. Eclipsen koodieditorissa voit pitää hiiren kursoria ohjelman osan päällä, niin näkyviin tulee tuon osan tietotyyppi. Seuraavassa editorista napatussa kuvassa hiiri lepää kokeilu-sanan päällä, ja esiin tulee tietoja funktion parametreista ja palautusarvon tyypistä:

../_images/eclipse_type_tooltip-fi.png

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!

Kierrokset 1–13 ja niihin liittyvät tehtävät ja viikkokoosteet on laatinut Juha Sorva.

Kierrokset 14–20 on laatinut Otto Seppälä. Ne eivät ole julki syksyllä, mutta julkaistaan ennen kuin määräajat lähestyvät.

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 ovat suunnitelleet Juha Sorva ja Teemu Sirkiä. Niiden teknisen toteutuksen ovat tehneet Teemu Sirkiä ja Riku Autio käyttäen Teemun toteuttamia Jsvee- ja Kelmu-työkaluja.

Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset on laatinut Juha Sorva.

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

Opetustapa, jossa 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+ on luotu Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Pääkehittäjänä toimii tällä hetkellä Jaakko Kantojärvi, jonka lisäksi järjestelmää kehittävät useat tietotekniikan ja informaatioverkostojen opiskelijat.

Kurssin tämänhetkinen henkilökunta on kerrottu luvussa 1.1.

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

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