Luku 3.5: Limsaa, jalkapalloa ja virheitä
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
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 vain 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 if–else-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:
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.
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ä
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äynnistysfunktion
testClubAndMatch.
Luokka Match käyttää luokkaa Club: kuhunkin otteluun liittyy kaksi ottelevaa seuraa.
Käynnistysfunktio sisältää testikoodia, jolla voi koekäyttää näitä 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 luokista
MatchjaClubniin, että luokat vastaavat annettua spesifikaatiotatäydentää puuttuvat metodit mainittuihin luokkiin
korjata
testClubAndMatch-testiohjelma toimivaksi eli sellaiseksi, että se käyttääMatch-luokkaa ohjelmassa olevien kommenttien mukaisestitä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/16: 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 testiohjelma testClubAndMatchiin.
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? Ehkä huomasit sen jo itsekin. Jos et, 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/16: 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äselvempiä, ja niitä voi joutua pohtimaan pidempään tai selvittelemään vaikkapa googlitse. Harjoitus tekee mestarin.
Vaihe 3/16: Match-luokka: varoituksia ja virheilmoituksia
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
.
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 ensimmäistä varoitusta, jonka IntelliJ antaa Match-luokasta:
Line is indented too far to the right, or a `{` or `:` is missing. Tuplaklikkaa
varoitusta, niin näet, että se osoittaa riviin, jolla lukee private var homeCount.
Huoli on aiheellinen, mutta varsinainen ongelma on eri rivillä.
Varoitusilmoitus ehdottaa, että kyseessä saattaisi olla puuttuva välimerkki, ja sellainen
tosiaan puuttuu pari riviä ylempää class-alkuiselta riviltä. Lisää se rivin loppuun.
Käännä koodi uudestaan F10llä. Luettelo Match-luokan ongelmista päivittyy.
Varoituksen juurisyy oli siis puuttuva kaksoispiste, jonka vuoksi tietokone ei tulkinnut
homeCount-muuttujan määrittelyn — eikä sitä alempienkaan rivien — olevan osa Match-luokkaa. Se tulkitsi ne Match-luokasta erillisiksi määrittelyiksi, jotka oli kuitenkin
sisennetty kuin ne olisivat Match-luokan sisältöä, mikä haiskahti ohjelmointivirheeltä.
Vaihe 4/16: Match-luokka: lisää varoituksia
Match-luokasta — tarkemmin sanoen isGoalless-metodista — varoitellaan muutakin.
Varoitus sanailee, että discarded non-Unit value of type Boolean ja ehdottaa vielä,
että ehkäpä siihen kohtaan kuuluisi kirjoittaa Unit.
Tällekin varoitukselle on aihetta, sillä tuossa metodissa on jotain pielessä. Ilmoitus
on kuitenkin harhaanjohtava ja ehdotus Unitin 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 5/16: goalDifference
Match-luokasta tulee virhe missing return type, jonka tuplaklikkaaminen vie
goalDifference-metodin kohdalle.
Virheilmoitus puuttuvasta paluuarvon tyypistä on teknisesti oikein muttei kerro selvästi, mikä varsinainen ohjelmointivirhe on tehty.
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 virhe.
Funktion rungossa olisi tarkoitus olla lauseke, joka määrää funktion paluuarvon. Tästä funktiomäärittelystä kuitenkin puuttuu välimerkki, minkä vuoksi funktion runko ei tule määritellyksi lainkaan.
Emme halua metodin palauttavan Unit vaan Int. Korjaukseksi riittää yhtäsuuruusmerkin
lisäys, jolloin metodi palauttaa runkona olevan lausekkeen arvon. (Jos haluat, voit myös
muuttaa koko metodin yksiriviseksi.)
Vaihe 6/16: isHigherScoringThan
Käännä koodi painamalla F10. Ongelmaluettelo päivittyy.
Match-luokan kohdalla näkyy vielä yksi virheilmoitus punaisella ja ne pari varoitusta
keltaisella. Tutkitaan viimeistä virheilmoitusta. (testClubAndMatch hälyttää myös, ja
kovasti hälyttääkin, mutta katsotaan sitä kohta.)
IntelliJ’n mukaan koodi this.totalGoals(anotherMatch) on viallinen, ja IntelliJ
on tässä ihan oikeassa. Huomaatko miksi? Virheilmoitus method totalGoals in class Match
does not take parameters on osuva.
Korjaa virhe ja toinen vastaava samalta riviltä. Käännä koodi uudestaan. Nyt näyttää
siltä, että Match olisi varoitusviestejä lukuunottamatta kunnossa. Älä kuitenkaan
vielä huokaise helpotuksesta.
Vaihe 7/16: Kaikki pielessä testiohjelmassa
Käynnistysfunktion testClubAndMatch koodista tulee virheitä solkenaan. Suurin osa
niistä on muotoa illegal start of toplevel definition, jollainen näkyy tulevan yhdestä
jos toisestakin tuon funktion println- ja if-rivistä.
Virheilmoitukset eivät ole selkeimmästä päästä mutta antavat vihjeen siitä, miten tietokone on koodin jäsentänyt.
Nämä ilmoitukset voi lukea ”koodirivi, joka on uloimmalla tasolla, ei voi alkaa noin”. Tässä ”uloin taso” tarkoittaa sellaista koodia, joka ei ole minkään funktion, yksittäisolion tai luokan sisällä.
Tokihan noiden virherivien pitäisi olla käynnistysfunktion testClubAndMatch sisällä.
Miksi IntelliJ sitten väittää, että tuo koodi olisi sen sijaan ”uloimmalla tasolla” ja
siksi epäkelpoa?
Syy löytyy katsomalla ensimmäisiä rivejä, joilta tuo virhe tulee. Tarkka silmäys kertoo,
että tuo println-rivi on sisennetty väärin, kuten on seuraavakin. Ne tosiaan ovat
”uloimmalla tasolla”, jonne ne eivät kuulu. Virhesisennys ikään kuin katkaisee
käynnistysfunktion määrittelyn ja aiheuttaa siten alempienkin rivien vastaavat
virheilmoitukset, vaikka ne rivit onkin sisennetty oikein.
Korjaa. Paina tämänkin tehtyäsi F10 ja muista sama myös edetessäsi seuraaviin vaiheisiin.
Vaihe 8/16: Otteluiden luominen
Yksi valituksista kohdistuu käskyyn Match(club2, club1) testiohjelmassa testClubAndMatch.
Mutta sehän näyttää olevan aivan kunnossa. Se on aivan kunnossa.
Tämä on hyvä esimerkki siitä, ettei virheilmoitus aina osoita lainkaan siihen paikkaan,
jossa varsinainen virhe on. Virheilmoitus osoittaa nyt testiohjelman ohjelmakoodiin ja
ilmoittaa, ettei se toimi. Ja eihän se toimikaan, mutta syy on siinä, että Match-luokka
on määritelty virheellisesti ja testiohjelma 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 9/16: 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 10/16: 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 sanoo, ettei mainittuihin Match-olion muuttujiin pääse käsiksi testClubAndMatch-ohjelmasta.
Jos et jo aiemmin huomannut, miten Match-luokassa on sekä muuttujat homeCount ja
awayCount että metodit homeGoals ja awayGoals, niin kiinnitä siihen huomiota nyt.
Millaisesta virheestä on kyse? Onko virhe Matchin vai testiohjelman koodissa?
Korjaa virhe.
Vaihe 11/16: 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 12/16: 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ä testClubAndMatch ei tarjoa metodille varsinaisia parametreja vaan vain
tyhjän parametriluettelon.
Korjaa.
Vaihe 13/16: isGoalless kuntoon
Virhe: Found: Unit Required: Boolean. Tämä type mismatch -virhe osoittaa
isGoalless-kutsuihin testiohjelmassa.
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, poistuvat myös aiemmin huomaamamme varoitusviestit.
Vaihe 14/16: location
Viimeiset käännösaikaiset virheet oliosta testClubAndMatch johtuvat jälleen siitä, että
Match-luokka on vajavainen.
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 15/16: Testaa
Ohjelman pitäisi nyt olla ajokunnossa. Aja testClubAndMatch. 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 16/16: Korjaa ja täydennä
Korjaa Match-luokkaan jääneet puutteet: yksi virheellinen metodi ja puuttuva
toString.
Ohjeita ja vinkkejä:
Muokkaa
testClubAndMatch-ohjelmaa parhaaksi katsomallasi tavalla virhettä etsiessäsi.Jos
toString-metodin toteuttaminenMatch-luokkaan tuottaa vaikeuksia:Varmista, että luit metodin koko Scaladocin. Haluttu toiminta on kuvattu siellä esimerkkien kanssa.
Varmista, että käytit
override-sanaa alussa.Katso
VendingMachine-luokantoString-metodi, josta on parikin eri toteutusta yllä.Lue jo tässä välissä loput tästä luvusta. Kyse voi olla jostakin alempana esitellystä
if-käskyihin liittyvästä virhetilanteesta.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?
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 loppuunelse-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 kun sivulla ei ole toisin mainittu.
Tehtävien automaattisen arvioinnin ovat toteuttaneet Aallon opiskelijat (aakkosjärjestyksessä) Riku Autio, Kai Bukharenko, Nikolas Drosdek, Kaisa Ek, Rasmus Fyhrqvist, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Kaappo Raivio, Timi Seppälä, Teemu Sirkiä, Konsta Suuriniemi, Onni Tammi, Joel Toppinen, Anna Valldeoriola Cardó, Aleksi Vartiainen ja Oiva Ylönen.
Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat piirsi 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. Opetustekstit animaatioihin kirjoitti Juha Sorva.
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ää ja tarjoaa palveluna Tietotekniikan 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.

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 talteenvar-muuttujaan (kuten dokumentaatiokin sanoo).