Kurssin viimeisimmän version löydät täältä: O1: 2024
Luku 3.5: Limsaa, jalkapalloa ja virheitä
Tästä sivusta:
Pääkysymyksiä: Millaisessa muodossa kurssilla jatkossa annetaan ohjelmointitehtävien tehtävänannot? Miten korjaan yleisiä virheitä Scala-koodista? Miten tulkitsen IntelliJ’n antamia virheilmoituksia?
Mitä käsitellään? Lisäharjoitusta if
-valintakäskystä, luokkien
laatimisesta ja muusta. Virheilmoituksia ja virheiden etsintää.
Scaladoc-dokumentit spesifikaatioina.
Mitä tehdään? Tutustutaan ohjelmiin ja ohjelmoidaan. Ohjelmat vaativat lukuisten aiheiden ymmärtämistä ja liittämistä yhteen.
Suuntaa antava vaativuusarvio:
Suuntaa antava työläysarvio:? Kolme tuntia.
Pistearvo: A105.
Oheismoduulit: Miscellaneous (uusi), Football1 (uusi).
Esimerkki: VendingMachine
Seuraava esimerkin teemana ovat virtuaaliset limuautomaatit. Toteutamme luokan, jollainen voisi hallinnoida yksinkertaisten kolikoilla toimivien limsanmyyntikoneiden toimintaa (leikisti; tässäkin alkeiskurssiesimerkissä on vedetty mutkat suoriksi).
Toteutamme VendingMachine
-luokan pala palalta Scaladoc-dokumenttina (luku 3.2) annetun
spesifikaation perusteella. Tässä hieman mutkikkaammassa luokassa yhdistyvät paitsi tuore
if
-asia myös moni muu aiempi teema.
Varsinaisesti uutta asiaa esimerkissä ei tule. Jos koet ymmärtäneesi tähänastiset kurssin
asiat mainiosti, etkä ole väärässä, niin voit ohittaa tämän esimerkin ja siirtyä alla
oleviin tehtäviin. (Voit myös vapaaehtoisena tehtävänä koettaa toteuttaa VendingMachine
-luokan
itse ennen kuin luet ratkaisun!)
Lue NYT dokumentaatiota
Tutustu Miscellaneous-moduulin luokan o1.soda.VendingMachine
Scaladoc-dokumentaatioon.
Varmista, että ymmärrät millaisia metodeita
VendingMachine
-luokassa on ja miten niitä käytetään.
Jatka vasta sitten eteenpäin tässä luvussa.
Luokka on toteutettu valmiiksi Miscellaneous-moduuliin, joten voit myös kokeilla sen käyttöä REPLissä, jos dokumentaatiosta jää epäselväksi, mitä metodit saavat aikaan. Toteutus esitellään alla.
Huomaa pakkaukset
Tähän asti olemme sijoittaneet lähes kaiken koodimme huolettomasti
yleispakkaukseen o1
. Yleisesti ottaen on parempi järjestää eri
hankkeiden koodi omiin pakkauksiinsa, ja tästä eteenpäin teemmekin
niin.
Huomaat pakkausjaon esimerkiksi Miscellaneous-moduulin sisältöä selatessasi.
Limuautomaatin tila ja sen alustaminen
Katsotaan ensiksi konstruktoriparametreja ja ilmentymämuuttujia:
class VendingMachine(var bottlePrice: Int, private var bottleCount: Int) {
private var earnedCash = 0
private var insertedCash = 0
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
}
Välimerkkikä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) "SOLD OUT" else s"$bottleCount bottles left")
}
if
-lauseke
toimii suuremman lausekkeen osana.if
-lausekkeen ympärillä ovat tarpeen, jotta tulee
oikein määritellyksi, missä else
-haara päättyy. (Vrt.
pikkutehtävät luvun 3.4 alussa.)Toinen toteutustapa
Alla on vertailun vuoksi toinen tapa toteuttaa tuo toString
. Tässä
käytetään paikallisia apumuuttujia ja upotetaan ne osaksi kuvausta.
override def toString = {
val earnings = this.earnedCash / 100.0
val bottleStatus = if (this.isSoldOut) "SOLD OUT" else s"$bottleCount bottles left"
s"earned $earnings euros, inserted $insertedCash cents, $bottleStatus"
}
sellBottle
-metodin toteutus
Tässä eräs toimiva toteutustapa sellBottle
-metodille, jonka tehtävänä on "myydä pullo"
eli muuttaa automaatin raha- ja pullotietoja sekä palauttaa joko vaihtorahan määrä tai
negatiivinen luku myynnin epäonnistumisen merkiksi.
def sellBottle() = {
if (this.isSoldOut) {
-1
} else if (!this.enoughMoneyInserted) {
-1
} else {
this.earnedCash = this.earnedCash + this.bottlePrice
this.bottleCount = this.bottleCount - 1
val changeGiven = this.insertedCash - this.bottlePrice
this.insertedCash = 0
changeGiven
}
}
isSoldOut
-metodilla.
Loppuunmyynnin tapauksessa päädytään palauttamaan -1.else if
-ketjutus.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:
- 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 val
ista var
iksi
tarkoittaa, että tuohon muuttujaan voi sijoittaa arvon luokan ulkopuolelta. Sekin siis
muuttaa julkista rajapintaa. Jos dokumentaatio pyytää val
in, tee val
. Yksityisten
muuttujien osalta voit valita val´\in tai `var
in 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. Kuitenkin kuten äskeinen
tehtäväkin antoi ymmärtää, private
ei ole aivan ehdoton este.
Tarkastellaan vaikkapa VendingMachine
-luokkaa. Yksi sen yksityisistä
muuttujista oli earnedCash
, jonka arvoa säätelivät julkiset metodit.
Tämä ei onnistu, kuten ei pidäkään:
val machine = new VendingMachine(250, 10)machine: o1.soda.VendingMachine = earned 0.0 euros, inserted 0 cents, 10 bottles left machine.earnedCash = 123456<console>:9: error: variable earnedCash in class VendingMachine cannot be accessed in o1.soda.VendingMachine machine.earnedCash = 123456 ^
Mutta seuraava onnistuu.
val accessToCurrentValue = machine.getClass.getDeclaredField("earnedCash")accessToCurrentValue: java.lang.reflect.Field = private int o1.soda.VendingMachine.earnedCash accessToCurrentValue.setAccessible(true)accessToCurrentValue.set(machine, 123456)machine.emptyCashbox()res0: Int = 123456
Tässä siis vaihdoimme earnedCash
-muuttujalle mielivaltaisen arvon
ohittamalla rajapinnan pienellä kikkailulla. Tätä ei kuitenkaan tule
tehtyä vahingossa.
Tehtävä: tunnista vääriä
Nouda IntelliJ’hin moduuli Football1, joka kuvaa jalkapallo-ottelujen tuloskirjanpitoa.
Tämä moduuli sisältää kaksi luokkaa — Match
ja Club
— sekä käynnistysolion
MatchTest
.
Luokka Match
käyttää luokkaa Club
: kuhunkin otteluun liittyy kaksi ottelevaa seuraa.
MatchTest
sisältää testikoodia, jolla voi koekäyttää luokkia.
Tehtävänanto
Tutustu moduulin Scaladoc-dokumentaatioon sekä ohjelmakoodiin. (Muistutus: löydät
dokumentaation IntelliJ’stä moduulin doc
-kansiosta sekä selaimella luvun alun
linkistä.)
Koodia tutkiessa osoittautuu, että:
- Siinä on syntaksivirheitä ("kielioppivirheitä"; ks. luku 1.8), jotka estävät luokkien käytön tyystin.
- Osa dokumentaation kuvaamista metodeista puuttuu. Eikä siinäkään vielä kaikki:
- Koodissa on myös toiminnallisia bugeja. Osa
Match
-luokan metodeista on kieliopillisesti oikein muttei toimi niin kuin dokumentaatio kuvaa.
Tehtäväsi on:
- korjata virheet ja täydentää puuttuvat metodit luokista
Match
jaClub
niin, että luokat vastaavat annettua spesifikaatiota, - korjata
MatchTest
-testiohjelma toimivaksi eli sellaiseksi, että se käyttääMatch
-luokkaa korrektisti ja ohjelmassa olevien kommenttien mukaisesti, - täydentää tätä testiohjelmaa niin, että se testaa luokkia kattavammin (parhaaksi katsomallasi tavalla), ja
- varmistaa testiohjelman avulla, että kaikki pelaa.
Ohjeita ja vinkkejä
Virheiksi on valittu sellaisia, joita ohjelmoinnin opiskelijat joskus tekevät. Niiden korjaamiseen kannattaa suhtautua ajatuksella, jotta osaat välttää tai ainakin korjata vastaavat tulevissa tehtävissä, joissa kirjoitat enemmän omaa koodia.
Alla on vaiheittainen opastus tehtävään.
Luithan varmasti Scaladoc-dokumentaation ensin? Silmäilitkö myös annettua koodia?
Vaihe 1/12: Club
Paina Football1-moduulin tai jonkin sen tiedostoista ollessa valittuna F10
(tai
valikosta Build → Build Module 'Football1'). IntelliJ’n
Messages-välilehti ponnahtaa esiin.
Aluksi siellä näkyy ainoastaan pari luokkaan Club
liittyvää virheilmoitusta sekä yksi
varoitus luokasta Match
. Älä anna tämän hämätä; virheitä on kyllä muuallakin. Nyt vain
on käynyt niin, että Club
-luokassa oleva virhe on sen sorttinen, että IntelliJ ei edes
luettele muita ongelmia ennen kuin Club
korjataan. Ei ole harvinaista, että ohjelmassa
oleva virhe peittää taakseen toisia virheitä.
Aloitetaan siis Club
-luokasta. IntelliJ’n mukaan toinen virheistä on class
-alkuisella
rivillä ja toinen ihan tiedoston lopussa. Tätä ei kannata tulkita näin kirjaimellisesti.
Kun ohjelmointityökalut yrittävät jäsentää ohjelmaa osiinsa, ja jokin menee pieleen
(koska ohjelma ei ole sääntöjen mukainen), niin virhe usein häiritsee myös ohjelman
loppuosan jäsentämistä. Yksikin jäsennysvirhe voi saada koko ohjelman vaikuttamaan
tietokoneen näkökulmasta monin tavoin virheelliseltä. Etenkin sulkeiden ja muiden
välimerkkien virheellinen käyttö sotkee jäsennyksen monesti täysin.
Club
-luokassakin on oikeastaan vain yksi pieni virhe, joka tosin toistuu kahdesti
samalla rivillä. IntelliJ’n virheilmoitus ':' expected but ',' found antaa osviittaa
siitä, mistä on kyse. Virheilmoituksen voi vapaasti suomentaa: "Tähän piti tulla
kaksoispiste, mutta vastaan tulikin (jo) pilkku." Punainen korostus osoittaa
konstruktoriparametrien val name, val stadium
välistä pilkkua.
Miksi rivillä pitäisi olla kaksoispiste? Mitä muuta siitä vielä puuttuu? Keksinet sen itsekin. Jos et, niin vertaa tätä määrittelyä aiempin esimerkkien luokkamäärittelyihin. Korjaa nyt virhe, niin jatketaan.
Monesti virheilmoitukset ovat vähemmän selviä, ja niitä voi joutua pohtimaan vähän pidempään tai selvittelemään vaikkapa googlitse. Harjoitus tekee mestarin.
Vaihe 2/12: isHigherScoringThan
Käännä koodi uudestaan (F10
). Virheilmoitusluettelo päivittyy. Nyt punaista
näkyy tulevan luokasta Match
ja erityisesti testiohjelmasta MatchTest
.
Aloitetaan Match
-luokasta. Tuplaklikkaa jompaakumpaa Messages-välilehdellä
näkyvää Error-ilmoitusta, niin löydät rivin, jolta ne molemmat ovat peräisin.
IntelliJ’n mukaan koodi this.totalGoals(anotherMatch)
on virheellinen, ja niinhän se
onkin. Huomaatko miksi? Korjaa virhe ja toinen vastaava samasta metodista.
Entä se virheilmoitus? IntelliJ’hän valitti tuosta metodikutsusta, että Int does not take parameters. Valitus voi äkkiseltään vaikuttaa vielä oudommalta kuin itse virheellinen koodinpätkä, mutta löytyy siitä tolkku, jos sen osaa tulkita.
totalGoals
-metodihan palauttaa kokonaisluvun. Metodi on parametriton, joten
this.totalGoals
on lauseke, jonka arvona on jokin Int
-tyyppinen arvo.
Scala-työkalusto siis tulkitsee, että this.totalGoals(anotherMatch)
on yritys
"kutsua Int
-arvoa parametrilla anotherMatch
", missä ei ole järkeä; siksi yllä
mainittu virheilmoitus.
Vaihe 3/12: goalDifference
Varoitusilmoituksista
Monet ohjelmointityökalut, IntelliJ mukaanlukien, huomauttavat joskus epäilyttävästä koodista varoituksilla (warning). IntelliJ’n Messages-välilehdellä varoitukset näyttävät pitkälti samalta kuin virheilmoituksetkin; ne erottaa keltaisesta huutomerkkikolmiosta .
Varoitus tarkoittaa, että koodissa on luultavasti (muttei 100-prosenttisen varmasti) jotain, joka on syytä korjata. Varoituksilla työkalu sanoo ohjelmoijalle: "Ei taida kannattaa tehdä noin?"
Kun IntelliJ varoittaa jostakin, se on usein oikeassa ainakin siinä, että jotain ohjelmassa kannattaa muuttaa. Automaattinen varoitusilmoitus ei vain välttämättä suoraan tai oikein ilmaise sitä, mitä kannattaa muuttaa.
Käytännössä hyvä nyrkkisääntö on suhtautua varoitusilmoituksiin samalla vakavuudella kuin virheilmoituksiinkin.
IntelliJ antaa Match
-luokasta varoituksen procedure syntax is deprecated ja ehdottaa
Unit
-sanan lisäämistä goalDifference
-metodin määrittelyyn, jotta olisi selvempää,
että metodi palauttaa Unit
in. Varoitus on aiheellinen mutta ehdotus huono.
Laita hiiren kursori goalDifference
-nimen päälle. Esiin tulevasta kuvauksesta näet
metodin nyt palauttavan Unit
. Toisin kuin varoitusilmoituksen ehdotuksessa oletettiin,
emme kuitenkaan halua metodin palauttavan Unit
vaan Int
. Ongelman syy on aiemmista
tehtävistä tuttu välimerkkivirhe metodin määrittelyssä.
Korjaa ja käännä koodi uudestaan. Nyt näyttäisi siltä, että Match
on kunnossa.
Mutta älä vielä huokaise helpotuksesta siltäkään osin.
Vaihe 4/12: MatchTest
ja otteluiden luominen
Yksi valituksista kohdistuu käskyyn new Match(club2, club1)
yksittäisoliossa
MatchTest
.
Mutta sehän näyttää olevan aivan kunnossa. Se on aivan kunnossa.
Tämä on hyvä esimerkki siitä, ettei virheilmoitus aina osoita siihen paikkaan, jossa
varsinainen virhe on. Virheilmoitus osoittaa nyt MatchTest
-luokan ohjelmakoodiin ja
ilmoittaa, ettei se toimi. Ja eihän se toimikaan, mutta syy on siinä, että Match
-luokka
on määritelty virheellisesti ja MatchTest
yrittää käyttää sitä tavalla, jolla käytön
pitäisi onnistua.
Scalan virheilmoituksista
Kaikki nykyisten Scala-työkalujen antamat virheilmoitukset eivät ole kovinkaan informatiivisia. Eräs hyvä puoli on kuitenkin se, että Scalan tyyppijärjestelmästä (luku 1.8) johtuen monet ohjelmoijan kömmähdykset aiheuttavat type mismatch -virheitä ja vastaavia tyyppeihin liittyviä ilmoituksia, joiden selvittäminen on suhteellisen helppoa (silloin, kun ei ole kyse tyyppijärjestelmän mutkikkaammista ominaisuuksista).
Type mismatch on virheilmoitus, josta on usein helppo päätellä, missä virhe on.
Tässäkin tapauksessa virheilmoitus kertoo meille varsin selvästi, mistä on kyse.
Lue se ja vertaa keskenään virheilmoituksen osoittamaa riviä, Match
-luokan
Scaladoc-dokumentaatiota ja annettua Match
-toteutusta.
Korjaa virhe. Paina tämänkin tehtyäsi F10
ja muista sama myös edetessäsi
seuraaviin vaiheisiin.
Vaihe 5/12: addAwayGoal
Tarkastellaan seuraavaksi virheilmoituksia, jotka kertovat, että value addAwayGoal
is not a member of o1.football1.Match. Siis: "Mitään addAwayGoal
ia ei löydy
Match
-luokasta."
Häh? Onhan siellä koodissa tuo metodi; katso vaikka itse.
Katsoitko tarkkaan? Ja huomasitko virheilmoituksen kysymyksen: did you mean AddAwayGoal?
Tämänlaisen virheilmoituksen saadessaan kannattaa aina tarkistaa oikeinkirjoitus. Kirjoitusvirhe voi olla kutsuvassa koodissa tai kutsutussa koodissa.
Vaihe 6/12: homeCount
ja awayCount
Sitten: variable awayCount in class Match cannot be accessed ja sama homeCount
-muuttujalle.
Tämäkin virheilmoitus on varsin onnistunut. Se kertoo, ettei mainittuihin Match
-olion
muuttujiin pääse käsiksi MatchTest
-oliosta.
Jos et jo aiemmin huomannut, miten Match
-luokassa on muuttujat homeCount
ja
awayCount
sekä metodit homeGoals
ja awayGoals
, niin kiinnitä siihen huomiota
nyt. Millaisesta virheestä on kyse? Onko virhe Match
in vai MatchTest
in koodissa?
Korjaa virhe.
Vaihe 7/12: totalGoals
Pari virheilmoitusta liittyy match1.totalGoals()
-metodikutsuihin. Ilmoitus on
tutunlainen (hieman ylempää): Int does not take parameters. Samoin sen syy on
tuttu, vaikka tässä ei metodille tarjota varsinaisia parametreja vaan vain tyhjä
parametriluettelo (luku 2.6).
Korjaa.
Vaihe 8/12: Match
-olion luominen taas
Eräs MatchTest
-ohjelman riveistä saa IntelliJ’n parkaisemaan: not found: value Match.
Tämä tarkoittaa suunnilleen: "En osaa määrittää arvoa lausekkeelle Match
."
Virheilmoitus ei ole sieltä selkeimmästä päästä tällä rivillä olevan virheen
selventäjänä. Se kuitenkin antaa vihjeen siitä, että Scala-työkalusto on yrittänyt
tulkita yksittäistä sanaa Match
lausekkeena (esim. muuttujan tai metodin nimenä).
Niinhän tässä ei haluta tapahtuvan, vaan olisi tarkoitus luoda uusi Match
-olio.
Osannet korjata asian.
Vaihe 9/12: isGoalless
Virhe: type mismatch; found: AnyVal, required: Boolean. Ilmoitus osoittaa
isGoalless
-kutsuihin MatchTest
issä.
Selvästikin if
-käskyyn tarvitaan Boolean
-tyyppinen lauseke. Jostain syystä
isGoalless
ei näytä sellaista tuottavan, vaan saamme jotakin AnyVal
-tyyppistä,
mitä se sitten tarkoittaakin.
Pystytkö selvittämään tai arvaamaan, mikä isGoalless
-metodissa on vikana? Saatko
ongelman korjattua? Saatko toteutuksesta hyvin yksinkertaisen?
Jos ongelman ymmärtäminen ei meinaa onnistua, katso alempaa Lisää virhetilanteita: arvojen palauttaminen ja valintakäsky; tuo kappale käsittelee juuri tällaisia virheitä.
Vaihe 10/12: location
Viimeiset käännösaikaiset virheet oliosta MatchTest
johtuvat jälleen Match
-luokan
vajavaisuudesta. Ilmoitus value location is not a member of o1.football1.Match kertoo
tässä, että location
-metodi on jäänyt Match
-luokasta määrittelemättä. Ilmoitus on
samanlainen kuin yllä addAwayGoal
-metodista, mutta syy on nyt erilainen: tämä metodi
tosiaan puuttuu.
Kirjoita toteutus location
-metodille.
Vaihe 11/12: Testaa
Ohjelman pitäisi nyt olla ajokunnossa. Aja MatchTest
. Mieti, mitä sen pitäisi tulostaa
ja mitä se todellisuudessa tulostaa.
Huomaat, että osa tulosteista on kunnossa, mutta kaikki ei ole hyvin. Match
-luokka
toimii edelleen osin dokumentaation vastaisesti, vaikka IntelliJ ei luokasta enää
virheitä löydäkään.
Alla on vinkkejä puutteiden korjaamiseksi.
Vaihe 12/12: Korjaa ja täydennä
Korjaa Match
-luokkaan jääneet puutteet: yksi virheellinen metodi ja puuttuva
toString
.
Ohjeita ja vinkkejä:
- Muokkaa
MatchTest
-ohjelmaa parhaaksi katsomallasi tavalla virhettä etsiessäsi. - Muistutus (viimeisen kerran tästä): Scaladoceista on syytä avata täydet kuvaukset metodien nimiä klikkaamalla.
- Jos
toString
-metodin toteuttaminen tuottaa vaikeuksia, kannattaa:- Varmistaa, että luit metodin koko Scaladocin.
- Varmistaa, että käytit
override
-sanaa alussa. - Katsoa
VendingMachine
-luokantoString
-metodi, josta on parikin eri toteutusta yllä. - Lukea jo tässä välissä loput tästä luvusta.
Kyse voi olla jostakin alempana esitellystä
if
-käskyihin liittyvästä virhetilanteesta. - Pyytää apua, jos nuo vinkit eivät auttaneet.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Lisää virhetilanteita: muuttujat ja valintakäsky
Tutkitaan luvun lopuksi lisää koodia, joka ei toimi. Seuraavat esimerkit voivat parantaa
ymmärrystäsi if
-käskyistä ja vähän muustakin. Ne voivat myös auttaa välttämään yleisiä
virheitä.
Eräs taannoinen kurssinkävijä laati harjoituksen vuoksi tällaisen min
-funktiota
muistuttavan koodinpätkän ja ihmetteli saamaansa virheilmoitusta. Moni muukin on tehnyt
vastaavan virheen.
def palautaPienempi(eka: Int, toka: Int) = {
if (eka < toka) {
var palautetaan = eka
} else {
var palautetaan = toka
}
palautetaan
}
palautetaan
ei ilmeisesti
olisi määritelty tuossa viimeisellä rivillä.palautetaan
-muuttuja on sinne määritelty,
peräti kahteen kertaan?Ongelma on tämä: palautetaan
on määritelty if
-käskyn haarojen sisällä. Kukin
tällainen määrittely kattaa vain kyseisen haaran, eikä (kumpikaan) muuttuja ole
käytettävissä if
-käskyn ulkopuolella.
Jos muuttujan haluaa olevan käytettävissä myös if
-käskyn jälkeen, se on
määriteltävä if
-käskyn ulkopuolella. Esimerkiksi tämä toimii:
def palautaPienempi(eka: Int, toka: Int) = {
val palautetaan = if (eka < toka) eka else toka
palautetaan
}
Tähän teemaan eli muuttujien käyttöalueisiin (scope) palataan luvussa 5.6.
Äskeisessä esimerkissähän ei tosin edes ole välttämätöntä tallentaa välitulosta muuttujaan. Tässä vielä yksinkertaisempi toteutus:
def pienempi(eka: Int, toka: Int) = if (eka < toka) eka else toka
Lisää virhetilanteita: arvojen palauttaminen ja valintakäsky
Miksei tämä toimi?
Sanotaan, että haluamme laatia funktion, joka palauttaa parametriksi saamansa
kokonaisluvun neliön, jos annettu luku on positiivinen. Negatiivisen parametriarvon
tapauksessa neliön sijaan palautetaankin nolla. Funktion parametrin tyyppi olisi Int
ja paluuarvon samaten.
Seuraava toteutus on aloittelijalle varsin tyypillinen. Siinä on selkeästi ilmaistu
molemmat tapaukset erillisillä if
-käskyillä:
def kokeilu(luku: Int) = {
if (luku > 0) luku * luku
if (luku <= 0) 0
}
Onko tässä sitten jotain vikaa? Kokeillaan REPL:issä:
def kokeilu(luku: Int) = { if (luku > 0) luku * luku if (luku <= 0) 0 }kokeilu: (luku: Int)AnyVal
Funktion määrittely on laillista Scalaa. Mutta hyvin ei käy, kun koetamme vaikkapa käyttää funktion palautusarvoa laskutoimituksessa:
7 + kokeilu(10)7 + kokeilu(10) ^ <console>:12: error: overloaded method value + ... cannot be applied to (AnyVal)
Virheilmoitus voidaan suomentaa suunnilleen niin, että "plus-operaattoria ei ole
määritelty tietotyypille AnyVal
". Mikä ihmeen AnyVal
?
Ongelman selitys
Virheilmoitus on vähän outo mutta aiheellinen.
Katsotaan koodiamme tarkasti ja muistaen, että palautusarvo on viimeiseksi evaluoitavan lausekkeen arvo.
def kokeilu(luku: Int) = {
if (luku > 0) luku * luku
if (luku <= 0) 0
}
if
-käskyllä "ei ole arvoa", eli sen arvo on
sisällötön Unit
-arvo!Int
-arvo nolla tai Unit
.if
-rivi oli itse asiassa ihan merkityksetön.Lisätieto
else
-osaton käskymme if (luku <= 0) 0
on käytännössä lyhennetty
versio käskystä if (luku <= 0) 0 else ()
, missä tyhjät sulkeet
tarkoittavat Unit
-arvoa.
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
if
-käsky,
jonka arvo Int
-tyyppinen valittiin sitten kumpi haara vain.Lisää AnyVal
ista aikanaan luvussa 7.3.
Entä tämä ratkaisu?
Pari käytännön vinkkiä virheenetsintään
Jos saat hämmentäviä virheilmoituksia, kannattaa katsoa tarkasti, mitkä ovat kunkin
lausekkeen ja palautusarvon tietotyypit. Tämä kannattaa usein muutenkin mutta erityisesti
silloin, jos ilmoituksessa näkyy sana Any ja olet käyttänyt if
-valintakäskyä.
REPLissä tyypit näkyvät helposti tulosteissa. IntelliJ’n koodieditorissa voit pitää hiiren kursoria ohjelman osan päällä, niin näkyviin tulee tuon osan tietotyyppi. Kokeile! (Liikuta hiirtä koodin päällä ja katso tyyppitietoja.)
Lisäkikka: ohjelmointityökalut osaavat joissakin tapauksissa antaa selkeämpiä virheilmoituksia, jos kirjaat funktiollesi palautusarvojen tyypin erikseen (luvun 1.8 mukaisesti):
def kokeilu(luku: Int): Int = // jne.
Tyypin saa kirjata näin vaikka kaikille funktioille. Palautusarvojen vapaaehtoinen kirjaaminen voi muutenkin selkiyttää ohjelmia ja vähentää virheitä. Monet Scala-ohjelmoijat merkitsevät palautusarvon tyypin kaikkiin julkisiin metodeihin (mitä sinun kuitenkaan ei ole pakko O1:ssä tehdä).
Yhteenvetoa
- Kurssilla Scaladoc-dokumentteja käytetään usein tehtävänannoissa.
- Ohjelmointityökalujen antamat virheilmoitukset ovat usein epäselviä, mutta niitä voi tulkita ja harjoittelu auttaa.
- Kun
if
-käskyn haarat kattavat kaikki mahdolliset tapaukset, on syytä käyttääelse
-sanaa ilman ehtolauseketta, jotta asia tulee ohjelmointityökaluillekin selväksi. - Lukuun liittyviä termejä sanastosivulla: dokumentaatio, Scaladoc;
if
; käännösaikainen virhe, syntaksivirhe.
Palaute
Huomaathan, että tämä on henkilökohtainen osio! Vaikka olisit tehnyt lukuun liittyvät tehtävät parin kanssa, täytä palautelomake itse.
Tekijät
Tämän oppimateriaalin kehitystyössä on käytetty apuna tuhansilta opiskelijoilta kerättyä palautetta. Kiitos!
Materiaalin luvut tehtävineen ja viikkokoosteineen on laatinut Juha Sorva.
Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.
Tehtävien automaattisen arvioinnin ovat toteuttaneet: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä ja Aleksi Vartiainen.
Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.
Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista suunnittelivat Juha Sorva ja Teemu Sirkiä. Teemu Sirkiä ja Riku Autio toteuttivat ne apunaan Teemun aiemmin rakentamat työkalut Jsvee- ja Kelmu.
Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset laati Juha Sorva.
O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.
Tapa, jolla käytämme O1Libraryn työkaluja (kuten Pic
) yksinkertaiseen graafiseen
ohjelmointiin, on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi
oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.
Oppimisalusta A+ luotiin alun perin Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Nykyään tätä avoimen lähdekoodin projektia kehittää Tietotekniikan laitoksen opetusteknologiatiimi ja tarjoaa palveluna laitoksen IT-tuki. Pääkehittäjänä on tällä hetkellä Markku Riekkinen, jonka lisäksi A+:aa ovat kehittäneet kymmenet Aallon opiskelijat ja muut.
A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen ovat luoneet Nikolai Denissov, Olli Kiljunen ja Nikolas Drosdek yhteistyössä Juha Sorvan, Otto Seppälän, Arto Hellaksen ja muiden kanssa.
Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.
Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.
bottlePrice
-muuttuja on rooliltaan tuoreimman säilyttäjä; koska hintaa on tarkoitus muuttaa sijoituskäskyillä, niin otetaan se talteenvar
-muuttujaan (kuten dokumentaatiokin sanoo).