Luku 3.5: Limsaa, jalkapalloa ja virheitä

../_images/person05.png

Esimerkki: VendingMachine

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

Limuautomaatin tila ja sen alustaminen

Katsotaan ensiksi luontiparametreja 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 luontiparametri 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 luontiparametrin 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 tyylikäytäntöjä:

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. Rivitämme niiden määrittelyt, vaikka ne ovatkin yksirivisiä ja lyhyitä.

Kaksi seuraavaa metodia palauttavat arvon eivätkä vaikuta olioon mitenkään. Kun ne ovat lisäksi noin lyhyitäkin, voimme jättää ne rivittämättä niin halutessamme. Nämä kaksi metodia ovat parametrittomia, emmekä kirjaa niille parametriluetteloa lainkaan.

Viides metodi on myös parametriton mutta vaikuttaa olion tilaan. Sille kirjaamme myös tyhjät kaarisulkeet parametriluettelon paikalle, vaikka metodi ei yhtään parametria otakaan.

Mainittuja kä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 then "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 then "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 then
    -1
  else if !this.enoughMoneyInserted then
    -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 paluuarvon määrää viimeiseksi suoritettava käsky. Kun koodissa on "haaroja", se ei välttämättä ole ohjelmatekstin viimeinen koodirivi. Tässä esimerkissä on kolme eri riviä, joista mikä tahansa saatetaan tilanteesta riippuen suorittaa viimeisenä.

Limuautomaattitoteutuksen laadusta

Tuo 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. Yksityisiin muuttujiin 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. Silti kuten äskeinen tehtäväkin antoi ymmärtää, ei private 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 = VendingMachine(250, 10)machine: o1.soda.VendingMachine = earned 0.0 euros, inserted 0 cents, 10 bottles left
machine.earnedCash = 123456-- Error:
  |machine.earnedCash = 123456
  |^^^^^^^^^^^^^^^^^^
  |variable earnedCash cannot be accessed

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. (Kertaus: löydät dokumentaation IntelliJ’stä moduulin doc-kansiosta sekä selaimella luvun alun linkistä. Tarkemmat ohjeet luvussa 3.2.)

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 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 ohjelmassa olevien kommenttien mukaisesti

  • täydentää tätä testiohjelmaa niin, että se testaa luokkia kattavammin (parhaaksi katsomallasi tavalla)

  • varmistaa testiohjelman avulla, että kaikki pelaa.

Ohjeita ja vinkkejä

Virheiksi on valittu sellaisia, joita ohjelmoinnin opiskelijat joskus tekevät. Käy ne läpi 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/17: Club-luokan ensimmäinen virhe

Paina Football1-moduulin tai jonkin sen tiedostoista ollessa valittuna F10 (tai valikosta Build → Build Module 'Football1'). IntelliJ’n Build-välilehti ponnahtaa esiin. Siellä näkyy luettelot virheilmoituksista, joista osa liittyy Clubiin, osa Matchiin ja osa MatchTestiin.

Aloitetaan Club-luokasta. IntelliJ’n mukaan kaksi kolmesta virheestä on class-alkuisella rivillä ja kolmas tiedoston lopussa end-rivillä.

class-rivillä on tosiaan virhe, ja se toistuu kahdesti samalla rivillä. IntelliJ’n virheilmoitus ':' expected but ',' found antaa osviittaa. Sen voi vapaasti suomentaa "Tähän piti tulla kaksoispiste, mutta vastaan tulikin (jo) pilkku."

Voit napsauttaa ensimmäistä virheilmoitusta kahdesti, niin editoriin tulee esiin juuri se rivi, johon virheilmoitus viittaa. Kokeile!

Rivillä punainen korostus osoittaa luontiparametrien val name, val stadium välistä pilkkua.

Miksi tuossa 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 virhe nyt, niin jatketaan. Niin, ja korjaa se toinenkin vastaava virhe samalla rivillä.

Korjauksen kirjoitettuasi käännä ("rakenna"; build) koodi uudestaan: F10.

Vaihe 2/17: Club-luokan toinen virhe

Toinen Club-virheilmoitus osoittaa luokan loppua. Tuon viimeisen rivin virhe on edellistäkin pienempi.

Virheilmoitus kertoo: misaligned end marker. Loppumerkki on väärin asemoitu. Korjaa sisennys. (Koska loppumerkki on vapaaehtoinen, sen voisi myös poistaa.)

Toistaiseksi kohtaamamme virheilmoitukset ovat olleet kohtalaisen selkeitä ja osoittaneet juuri niille riveille, joissa virheet ovat. Näin ei ole aina. Monesti virheilmoitukset ovat epäselviä, ja niitä voi joutua pohtimaan pidempään tai selvittelemään vaikkapa googlitse. Harjoitus tekee mestarin.

Vaihe 3/17: Aloitetaan Matchin korjaaminen

Virheilmoitus osoittaa private var -alkuista riviä ja protestoi: this kind of statement is not allowed here. Se tarkoittaa suunnilleen: ei tällaista käskyä saa laittaa tähän kohtaan.

Jaa, mihin kohtaan? Katsokaamme virheen ympärille.

Hieman ylempänä, class Match -alkuisella rivillä, näkyy lopussa alleviivaus virheen merkiksi. Virhe on pieni ja melko ilmeinen, ja jos viet hiiren kursorin sinne kaksoispisteen päälle, IntelliJ vielä vahvistaa: suljehan se sieltä puuttuu.

Alkuperäinen virheilmoitus viittasi siis siihen, että private-muuttujaa ei voi kirjoittaa mihin tahansa. Koska class-määrittely oli pielessä, ei tuo muuttujan määrittelykään kelvannut. Ilmoitus osoitti oikeaan suuntaan, vaikkei ollutkaan selkein mahdollinen.

Lisää puuttuva kaarisulje. Käännä koodi uudestaan F10llä. Luettelo Match-luokan ongelmista päivittyy.

Vaihe 4/17: goalDifference

Match-luokasta tulee nyt virhe missing return type, jonka tuplaklikkaaminen vie goalDifference-metodin kohdalle.

Virheilmoitus puuttuvasta paluuarvon tyypistä on teknisesti oikein muttei erityisen selvästi kerro, mikä varsinainen ohjelmointivirhe on tehty.

Funktion rungossa olisi tarkoitus olla (Int-tyyppinen) lauseke, joka määrää funktion paluuarvon. Tästä funktiomäärittelystä puuttuu välimerkki, minkä vuoksi funktion runko ei tule määritellyksi lainkaan.

Vie hiiren kursori goalDifference-nimen päälle, niin IntelliJ näyttää lisätietoja metodista. Siellä näkyy muun muassa, että metodin paluuarvon tyyppi olisi Unit. Scala ei ole siihen Unitia parempaakaan tyyppiä saanut pääteltyä, koska metodin määrittelyssä on välimerkkivirhe.

Emme halua metodin palauttavan Unit vaan Int. Korjaukseksi riittää yhtäsuuruusmerkin lisäys, jolloin metodi palauttaa runkona olevan lausekkeen arvon. (Voit myös muuttaa koko metodin yksiriviseksi, jos haluat.)

Vaihe 5/17: isHigherScoringThan

Käännä koodi painamalla F10. Ongelmaluettelo päivittyy.

Match-luokan kohdalla näkyy nyt yksi virheilmoitus punaisella ja pari varoitusta keltaisella. Tutkitaan ensin virheilmoitusta ja palataan varoituksiin myöhemmin. (MatchTest hälyttää myös, ja kovasti hälyttääkin, mutta palataan myös siihen myöhemmin.)

IntelliJ’n mukaan koodi this.totalGoals(anotherMatch) on viallinen, ja IntelliJ on tässä ihan oikeassa. Huomaatko miksi? Korjaa virhe ja toinen vastaava samalta riviltä. Virheilmoitus method totalGoals in class Match does not take parameters on osuva.

Käännä koodi uudestaan. Nyt näyttää siltä, että Match olisi varoitusviestejä lukuunottamatta kunnossa. Älä kuitenkaan huokaise helpotuksesta.

Vaihe 6/17: Varoitus isGoalless-metodissa

Varoitusilmoituksista

Monet ohjelmointityökalut, IntelliJ mukaanlukien, huomauttavat joskus epäilyttävästä koodista varoituksilla (warning). IntelliJ’n Build-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. 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.

Tutkitaan seuraavaksi varoitusta, jonka IntelliJ antaa Match-luokasta — tarkemmin sanoen sen isGoalless-metodista. Varoitus sanailee, että a pure expression does nothing in statement position ja ehdottaa vielä päälle, että ehkäpä siitä kohdasta puuttuisi sulkeita.

Varoitukselle on aihetta: tuossa metodissa on jotain pielessä. Ilmoitus on kuitenkin harhaanjohtava ja ehdotus sulkeiden lisäämisestä huono.

Kenties jo näetkin, mikä on vikana. Tehdään silti niin, ettei vielä korjata tuota metodia, sillä on sivistävää tutkia, millaisia oireita tuo virhe aiheuttaa muualla ohjelmassa. Palaamme siihen myöhemmin. Anna nyt noiden varoitusten olla ja jatka seuraavaan vaiheeseen.

Vaihe 7/17: Kaikki pielessä MatchTestissä

MatchTest-olion koodista tulee virheitä solkenaan. Suurin osa niistä on muotoa illegal start of toplevel definition, jollainen näkyy tulevan yhdestä jos toisestakin tuon olion rivistä, jolla kutsutaan jotakin funktiota.

Nuo ilmoitukset eivät ole selkeimmästä päästä varsinaisen virheen selventäjinä eivätkä suoraan osoita korjausta vaativaa riviäkään. Ne kuitenkin antavat vihjeen siitä, miten tietokone on koodin jäsentänyt.

Virheilmoituksen voi lukea "uloimmalla tasolla koodirivi ei voi alkaa noin", missä "uloin taso" tarkoittaa koodia, joka ei ole minkään funktion, yksittäisolion tai luokan sisällä.

Ilmoitus tuntuu erikoiselta, sillä kyllähän noiden virherivien pitäisi olla yksittäisolion MatchTest-sisällä. Ihan oikeinhan ne on sinne kirjoitettu object MatchTest -rivin perään. Miksi IntelliJ sitten väittää, että tuo koodi olisi "uloimmalla tasolla" ja siksi epäkelpoa?

Syy on jälleen kerran välimerkissä. Katso object MatchTest -rivin loppua. Nykyisellään käy niin, että tuo olion koko määrittely on vain yhden rivin mittainen eikä seuraavia koodirivejä lasketa sen osiksi lainkaan (sisennyksistä huolimatta). Lisää puuttuva merkki ja käännä uudelleen.

Vaihe 8/17: Eikö se korjaantunutkaan?

Moni katosi, mutta osa illegal start -virheistä sinne vielä jäi. Ensimmäinen niistä osoittaa tiettyyn println-käskyyn.

Tarkka silmäys koodiin kertoo, että tuo rivi on sisennetty väärin, kuten on seuraavakin. Ne tosiaan ovat "uloimmalla tasolla", jonne ne eivät kuulu. Korjaa.

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

Vaihe 9/17: Otteluiden luominen

Yksi valituksista kohdistuu käskyyn 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.

Huomaa, että virheilmoitus on monirivinen ja näkyy kokonaisuudessaan Build-välilehdellä virheluettelon vieressä, kun valitset kyseisen virheen luettelosta.

Tämä on ns. type mismatch -virhe. Tällainen virhe sanoo, että tiettyyn kohtaan koodia kaivattiin erästä tyyppiä oleva arvo, mutta löytyikin jotain muuta. Tällaisista virheilmoituksista on melko usein helppo päätellä, missä virhe on; tässäkin tapauksessa virheilmoitus kertoo kohtalaisen 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 10/17: addHomeGoal

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

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

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

Tämänlaisen virheilmoituksen saadessasi tarkista aina oikeinkirjoitus. Kirjoitusvirhe voi olla kutsuvassa koodissa tai kutsutussa koodissa. Huomaa myös, että virheilmoitusten did you mean -ehdotukset eivät välttämättä ole oikeassa. Toteuta ohjelma spesifikaation (eli tässä Scaladocin) mukaiseksi.

Vaihe 11/17: homeCount ja awayCount

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

Olemme onnekkaita, sillä ilmoitus kertoo sekä virheen todellisen sijainnin että syyn. 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 12/17: isHomeWin ja isAwayWin

Jaaha: value isHomeWin is not a member of o1.football1.Match ja sama isAwayWinille.

Mutta ihan varmastihan Match-luokassa on nyt nuo metodit! Nimetkin on oikein kirjoitettu.

Match-luokan koodista nuo määrittelyt toisiaan löytyvät, mutta pienen virheen vuoksi noita metodeita ei kuitenkaan nyt ole Match-olioilla tarjolla. Seuraava pikkuesimerkki johdattaa virheen äärelle.

Sanotaan, että haluamme määritellä kolmimetodisen luokan:

class Luokka:

  def toiminto1(luku: Int) =
    println(luku)

  def toiminto2 = 1 + 1

  def toiminto3 = 2 + 2

end Luokka

Ehkä kuitenkin vahingossa kirjoitamme sen näin:

class Luokka:

  def toiminto1(luku: Int) =
    println(luku)

    def toiminto2 = 1 + 1

    def toiminto3 = 2 + 2

end Luokka

Tuo jälkimmäinen luokkamäärittelymme tarkoittaa ihan samaa kuin tämä seuraava, johon on lisätty loppumerkki:

class Luokka:

  def toiminto1(luku: Int) =
    println(luku)

    def toiminto2 = 1 + 1

    def toiminto3 = 2 + 2
  end toiminto1

end Luokka

Nyt siis toiminto2 ja toiminto3 ovatkin sisennysten vuoksi toiminto1-metodin sisällä. Niistä tulee ensimmäisen metodin paikallisia funktioita eikä luokan metodeita. (Tämä ei ollut lainkaan tarkoituksemme, mutta paikallisilla funktioilla on kyllä paljonkin muuta käyttöä, mistä kerrotaan kurssilla myöhemmin.)

Korjaa metodit isHomeWin ja isAwayWin Match-luokasta. Tarkkana sisennyksissä!

Vaihe 13/17: totalGoals

Yksi virheistä liittyy match1.totalGoals()-metodikutsuun. Ilmoitus on tutunlainen (hieman ylempää): method totalGoals does not take parameters. Samoin sen syy on tuttu, vaikka tässä ei metodille tarjota varsinaisia parametreja vaan vain tyhjä parametriluettelo.

Korjaa.

Vaihe 14/17: isGoalless kuntoon

Virhe: Found: Unit   Required: Boolean. Tämä type mismatch -virhe osoittaa isGoalless-kutsuihin MatchTestissä.

Selvästikin if-käskyyn tarvitaan Boolean-tyyppinen lauseke. Jostain syystä isGoalless ei näytä sellaista tuottavan, vaan saamme Unitin.

Pystytkö selvittämään tai arvaamaan, mikä isGoalless-metodissa on vikana? Saatko ongelman korjattua? Saatko toteutuksesta hyvin yksinkertaisen? Joskus korjaaminen tarkoittaa vain koodin poistamista.

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ä. Tee se pikkutehtäväkin. (Ja muista vielä vinkki: isGoallessille voi määritellä erittäin yksinkertaisen, toimivan toteutuksen.)

Kun korjaat tämän virheen, poistuu myös aiemmin huomaamamme varoitusviesti.

Vaihe 15/17: 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 joistakin muistakin metodeista yllä, mutta syy on nyt erilainen: location-metodi tosiaan puuttuu kokonaan.

Kirjoita toteutus location-metodille.

Vaihe 16/17: 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 17/17: 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ä): Avaa Scaladoceista täydet kuvaukset metodien nimiä klikkaamalla.

  • Jos toString-metodin toteuttaminen tuottaa vaikeuksia:

    1. Varmista, että luit metodin koko Scaladocin.

    2. Varmista, että käytit override-sanaa alussa.

    3. Katso VendingMachine-luokan toString-metodi, josta on parikin eri toteutusta yllä.

    4. Lue jo tässä välissä loput tästä luvusta. Kyse voi olla jostakin alempana esitellystä if-käskyihin liittyvästä virhetilanteesta.

    5. Pyydä 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 then
    var palautetaan = eka
  else
    var palautetaan = toka
  palautetaan

Koodi tuottaa käännösaikaisen virheilmoituksen not found: 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 haluat käyttää muuttujaa 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 then 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 then 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 then luku * luku
  if luku <= 0 then 0

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

def kokeilu(luku: Int) =
  if luku > 0 then luku * luku
  if luku <= 0 then 0def kokeilu(luku: Int): Unit
-- Potential Issue Warning:
    if luku <= 0 then 0
...

Funktion määrittely on periaatteessa laillista Scalaa. REPL kuittaa, että funktio tuli määritellyksi, mutta ...

... varoittaa, että jotain tässä taitaa olla menossa pieleen.

Määritellyksi tulleen funktion paluuarvon tyyppinä näkyy olevan Unit, vaikka halusimme funktion palauttavan luvun.

Tutkitaan vielä. Kun koetamme kutsua funktiota, ei käy hyvin:

kokeilu(10)7 + kokeilu(10)-- Type Error:
7 + kokeilu(10)
^^^
None of the overloaded alternatives of method + in class Int ... match arguments (Unit)

Emme saa paluuarvona lukua. Ja kun yritämme käyttää funktiota osana laskutoimitusta, saamme virheilmoituksen, jonka voi suomentaa suunnilleen "plus-operaattoria ei ole määritelty yhdistelmälle Int + Unit". Selvästi funktiomme palauttaa vain Unit. Mistä on kyse?

Ongelman selitys

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

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

Kokeilufunktiossamme on kaksi if-käskyä, jotka ovat toisistaan täysin erilliset. Jälkimmäinen on ensimmäisen perässä. Kumpikaan ei ole toisen else-haarassa tai muutenkaan yhteydessä toiseen.

Ensin funktio evaluoi lausekkeen, jonka arvoksi haluttaisiin positiivisen luvun tapauksessa tuon luvun neliö. Tällä lausekkeella ei ole else-haaraa, eikä se lainkaan määrittele, mitä sen arvoksi muussa tapauksessa tulisi!

Sen jälkeen (ja täysin riippumattomasti siitä, mitä edellinen rivi teki) evaluoidaan jälkimmäinen käsky. Tässäkin käskyssä on kerrottu, minkä arvon yksi haara tuottaa (eli nollan) mutta toinen haara on jäänyt kokonaan määrittelemättä — eikä tämä toinen rivi tiedä mitään siitä, mitä edellinen teki, eikä siis osaa sulkea pois positiivisen luvun mahdollisuutta.

Jälkimmäisen rivin tuottama arvo on koko funktion paluuarvo. Edeltävä if-rivi oli itse asiassa ihan merkityksetön.

Koska näin (huonosti) määritellyn funktion paluuarvo voi tilanteesta riippuen olla joko Int tai tarkemmin määrittelemätön, Scala-työkalusto päätyy tulokseen, että koko lausekkeen (eli funktion toisen rivin) tyyppi on vain Unit eli "ei oikein mitään".

Lisätieto

else-osaton käskymme if luku <= 0 then 0 on käytännössä lyhennetty versio tällaisesta käskystä:

if luku <= 0 then
  0
  ()
else
  ()

Tyhjät sulkeet tarkoittavat Unit-arvoa.

Toimiva versio

No, miten se sitten saadaan toimimaan?

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

Viimeiseksi evaluoitava lauseke on nyt tämä ainoa if-käsky, jonka arvo Int-tyyppinen valittiin sitten kumpi haara vain.

Entä tämä ratkaisu?

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

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

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

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

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

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

Tyypin saa kirjata näin vaikka kaikille funktioille. Paluuarvojen vapaaehtoinen kirjaaminen voi muutenkin selkiyttää ohjelmia ja vähentää virheitä. Monet Scala-ohjelmoijat merkitsevät paluuarvon 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 valitset eri arvojen välillä (vaikutuksettomalla) if-lausekkeella, muista kirjoittaa loppuun else-haara ilman ehtolauseketta, jotta jokin arvo tulee kaikissa tapauksissa valituksi.

  • 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, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Joel Toppinen, Anna Valldeoriola Cardó 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, Juha Sorva ja Jaakko Nakaza. 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; sitä 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 suunnitteluun ja toteutukseen on osallistunut useita opiskelijoita yhteistyössä O1-kurssin opettajien 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...