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 Eclipsen antamia virheilmoituksia?
Mitä käsitellään? Lisäharjoitusta if
-valintakäskystä, luokkien
laatimisesta ja muusta. Virheilmoituksia ja virheiden etsintää.
Scaladoc-dokumentit spesifikaatioina.
Mitä tehdään? Tutustutaan ohjelmiin ja ohjelmoidaan. Ohjelmat vaativat lukuisten aiheiden ymmärtämistä ja liittämistä yhteen.
Suuntaa antava vaativuusarvio:
Suuntaa antava työläysarvio:? Kolme tuntia.
Pistearvo: A105.
Oheisprojektit: Miscellaneous (uusi), Football1 (uusi).
Esimerkki: VendingMachine
Seuraava esimerkin teemana ovat virtuaaliset limuautomaatit. Toteutamme luokan, jollainen voisi hallinnoida yksinkertaisten limsanmyyntikoneiden toimintaa (leikisti; tässäkin alkeiskurssiesimerkissä on vedetty mutkat suoriksi).
Toteutamme VendingMachine
-luokan pala palalta Scaladoc-dokumenttina (luku 3.2) annetun
spesifikaation perusteella. Tässä hieman mutkikkaammassa luokassa yhdistyvät paitsi tuore
if
-asia myös moni muu aiempi teema.
Varsinaisesti uutta asiaa esimerkissä ei tule; jos koet ymmärtäneesi tähänastiset kurssin
asiat mainiosti, etkä ole väärässä, niin voit ohittaakin tämän esimerkin ja siirtyä
alla oleviin tehtäviin. (Voit myös vapaaehtoisena tehtävänä koettaa toteuttaa
VendingMachine
-luokan itse ennen kuin luet ratkaisun!)
Lue NYT dokumentaatiota
Tutustu Miscellaneous-projektin luokan o1.soda.VendingMachine
Scaladoc-dokumentaatioon.
Varmista, että ymmärrät millaisia metodeita
VendingMachine
-luokassa on ja miten niitä käytetään.
Jatka vasta sitten eteenpäin tässä luvussa.
Luokka on toteutettu valmiiksi projektiin Miscellaneous, joten voit myös kokeilla sen käyttöä REPLissä, jos dokumentaatiosta jää epäselväksi, mitä metodit saavat aikaan. Toteutus esitellään alla.
Huomaa pakkaukset nyt ja tulevaisuudessa
Tähän asti kurssilla olemme sijoittaneet lähes kaiken koodimme
huolettomasti yleispakkaukseen o1
. Yleisesti ottaen on parempi
järjestää eri hankkeiden koodi omiin pakkauksiinsa, ja tästä
eteenpäin teemmekin niin.
Huomaat pakkausjaon esimerkiksi Miscellaneous-projektin sisältöä
selatessasi. Muista asia myös REPLissä kokeillessasi: esimerkiksi
VendingMachine
-luokka on pakkauksessa o1.soda
ja pitää siis
ottaa käyttöön asianmukaisella import
-käskyllä. Sama koskee
myös pakkauksen o1.football1
sisältöä myöhemmin tässä luvussa.
Limuautomaatin tila ja sen alustaminen
Katsotaan ensiksi konstruktoriparametreja ja ilmentymämuuttujia:
class VendingMachine(var bottlePrice: Int, private var bottleCount: Int) {
private var earnedCash = 0
private var insertedCash = 0
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:
override def toString = {
"earned " + this.earnedCash / 100.0 + " euros, " +
"inserted " + this.insertedCash + " cents, " +
(if (this.isSoldOut) "SOLD OUT" else this.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.)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 kirjoitettu pääsääntöisesti englanniksi.
Yksityiset muuttujat Scaladoc-pohjaisissa tehtävissä
Kertaus luvusta 3.2: yksityiset muuttujat eivät kuulu luokan julkiseen rajapintaan eivätkä siksi tule mukaan Scaladoc-dokumentteihin, joiden tehtävä on kuvata luokan käyttötapaa luokan ulkopuolelta katsottuna.
Niissä kurssin tehtävissä, joissa annetaan Scaladoc-muotoinen spesifikaatio, tämä
tarkoittaa käytännössä sitä, että dokumenteissa näkyvät ilmentymämuuttujat sinunkin tulee
määritellä julkisiksi laatimaasi ohjelmakoodiin (kuten bottlePrice
äsken). Toisaalta
muita julkisia ilmentymämuuttujia kuin dokumentaation määräämät ei luokalla tulisi olla.
Ei ole epätavallista, että tarvitset luokan sisäiseen tilakirjanpitoon muita muuttujia
kuin scaladocin kuvaamat julkiset muuttujat, mutta tee noista lisämuuttujista yksityisiä
(kuten esim. bottleCount
- ja earnedCash
-muuttujista äsken).
Pikkutehtävä
Pääseekö private
-tietoihin siis jotenkin käsiksi?
(Seuraavaa ei tarvitse osata kurssilla. Tai usein muutenkaan.)
private
-määre estää muuttujan (tai metodin) tavallisen käytön luokan
ulkopuolelta, ja sillä merkitään, ettei muuttujaa pitäisi käyttää
ulkopuolelta missään normaaliolosuhteissa. Kuitenkin kuten äskeinen
tehtäväkin antoi ymmärtää, private
ei ole aivan ehdoton este.
Tarkastellaan vaikkapa VendingMachine
-luokkaa. Yksi sen yksityisistä
muuttujista oli earnedCash
, jonka arvoa säätelivät julkiset metodit.
Tämä ei onnistu, kuten ei pidäkään:
val machine = new VendingMachine(250, 10)machine: o1.soda.VendingMachine = earned 0.0 euros, inserted 0 cents, 10 bottles left machine.earnedCash = 123456<console>:9: error: variable earnedCash in class VendingMachine cannot be accessed in o1.soda.VendingMachine machine.earnedCash = 123456 ^
Mutta seuraava onnistuu.
val accessToCurrentValue = machine.getClass.getDeclaredField("earnedCash")accessToCurrentValue: java.lang.reflect.Field = private int o1.soda.VendingMachine.earnedCash accessToCurrentValue.setAccessible(true)accessToCurrentValue.set(machine, 123456)machine.emptyCashbox()res0: Int = 123456
Tässä siis vaihdoimme earnedCash
-muuttujalle mielivaltaisen arvon
ohittamalla rajapinnan pienellä kikkailulla. Tätä ei kuitenkaan tule
tehtyä vahingossa.
Tehtävä: tunnista vääriä
Nouda Eclipseen projekti Football1, joka kuvaa jalkapallo-ottelujen tuloskirjanpitoa.
Projekti sisältää kaksi luokkaa — Match
ja Club
— sekä käynnistysolion
MatchTest
.
Luokka Match
käyttää luokkaa Club
: kuhunkin otteluun liittyy kaksi ottelevaa seuraa.
MatchTest
sisältää testikoodia, jolla voi koekäyttää luokkia.
Tehtävänanto
Tutustu projektin Scaladoc-dokumentaatioon sekä ohjelmakoodiin. (Muistutus: löydät
dokumentaation Eclipsessä projektin sisältä doc
-kansiosta sekä selaimella luvun
alun linkistä.)
Koodia tutkiessa osoittautuu, että:
- Siinä on syntaksivirheitä ("kielioppivirheitä"; ks. luku 1.8), jotka estävät luokkien käytön tyystin.
- Osa dokumentaation kuvaamista metodeista puuttuu. Eikä siinäkään vielä kaikki:
- Koodissa on myös toiminnallisia bugeja. Osa
Match
-luokan metodeista on kieliopillisesti oikein muttei toimi niin kuin dokumentaatio kuvaa.
Tehtäväsi on:
- korjata virheet ja täydentää puuttuvat metodit luokista
Match
jaClub
siten, että luokat vastaavat annettua spesifikaatiota, - korjata
MatchTest
-testiohjelma toimivaksi eli sellaiseksi, että se käyttääMatch
-luokkaa korrektisti ja ohjelmassa olevien kommenttien mukaisesti, - täydentää tätä testiohjelmaa siten, että se testaa luokkia kattavammin (parhaaksi katsomallasi tavalla), ja
- varmistaa testiohjelman avulla, että kaikki pelaa.
Ohjeita ja vinkkejä
Virheiksi on valittu sellaisia, joita ohjelmoinnin opiskelijat joskus tekevät. Niiden korjaamiseen kannattaa suhtautua ajatuksella, jotta osaat välttää tai ainakin korjata vastaavat tulevissa tehtävissä, joissa kirjoitat enemmän omaa koodia.
Alla on vaiheittainen opastus tehtävään.
Luithan varmasti Scaladoc-dokumentaation ensin? Silmäilitkö myös annettua koodia?
Vaihe 1/11: Club
Katso Eclipsen Problems-välilehteä. Aluksi siellä näkyy ainoastaan luokkaan
Club
liittyviä virheilmoituksia. Älä anna tämän hämätä; virheitä on kyllä muuallakin.
Nyt vain on käynyt niin, että Club
-luokassa oleva virhe on sen sorttinen, että Eclipse
ei edes luettele tuolla välilehdellä muita ongelmia ennen kuin Club
korjataan. Ei ole
harvinaista, että ohjelmassa oleva virhe peittää taakseen toisia virheitä.
Aloitetaan siis Club
-luokasta. Eclipsen mukaan luokassa on jopa kolme virhettä, joista
osa on class
-alkuisella rivillä ja osa ihan tiedoston lopussa. Tätä ei kannata tulkita
näin kirjaimellisesti. Kun ohjelmointityökalut yrittävät jäsentää ohjelmaa osiinsa, ja
jokin menee pieleen (koska ohjelma ei ole sääntöjen mukainen), niin virhe usein häiritsee
myös ohjelman loppuosan jäsentämistä. Yksikin jäsennysvirhe voi saada koko ohjelman
vaikuttamaan tietokoneen näkökulmasta monin tavoin virheelliseltä. Erityisesti sulkeiden
ja muiden välimerkkien virheellinen käyttö sotkee jäsennyksen monesti täysin.
Club
-luokassakin on oikeastaan vain yksi pieni virhe, joka tosin toistuu kahdesti.
Eclipsen virheilmoitus ':' expected but ',' found antaa osviittaa siitä, mistä on kyse.
Virheilmoituksen voi vapaasti suomentaa: "Tähän piti tulla kaksoispiste, mutta vastaan
tulikin (jo) pilkku." Punainen korostus osoittaa konstruktoriparametrien val name,
val stadium
välistä pilkkua.
Miksi rivillä pitäisi olla kaksoispiste? Mitä muuta siitä vielä puuttuu? Keksinet sen itsekin. Jos et, niin vertaa tätä määrittelyä aiempin esimerkkien luokkamäärittelyihin. Korjaa nyt virhe, niin jatketaan.
Monesti virheilmoitukset ovat vähemmän selviä, ja niitä voi joutua pohtimaan vähän pidempään tai selvittelemään vaikkapa googlitse. Harjoitus tekee mestarin.
Vaihe 2/11: isHigherScoringThan
Tallenna. Virheilmoitusluettelo päivittyy. Nyt punaista näyttää tulevan sekä luokasta
Match
että testiohjelmasta MatchTest
.
Aloitetaan Match
-luokasta. Klikkaa vaikkapa kyseistä Problems-välilehdellä
näkyvää virheilmoitusta, niin löydät rivin, josta se on peräisin.
Virheellinen koodi on Eclipsen mukaan this.totalGoals(anotherMatch)
. Tämä metodikutsu
on tosiaan virheellinen. Huomaatko miksi? Korjaa virhe ja toinen vastaava samasta
metodista.
Entä se virheilmoitus? Eclipsehän valitti tuosta metodikutsusta, että Int does not take parameters. Virheilmoitus voi äkkiseltään vaikuttaa vielä oudommalta kuin itse virheellinen koodinpätkä, mutta löytyy siitä tolkku, jos sen osaa tulkita.
totalGoals
-metodihan palauttaa kokonaisluvun. Metodi on parametriton, joten
this.totalGoals
on lauseke, jonka arvona on jokin Int
-tyyppinen arvo.
Scala-työkalusto siis tulkitsee, että this.totalGoals(anotherMatch)
on yritys
"kutsua Int
-arvoa parametrilla anotherMatch
", missä ei ole järkeä; siksi yllä
mainittu virheilmoitus.
Tämä korjattuasi — ja muista taas tallentaa — näyttäisi siltä, että
Match
on kunnossa. Mutta älä vielä huokaise helpotuksesta siltäkään osin.
Vaihe 3/11: MatchTest
ja otteluiden luominen
Yksi valituksista kohdistuu käskyyn new Match(club2, club1)
yksittäisoliossa
MatchTest
. Mitäs vikaa siinä on? Sehän on aivan oikeaoppisen näköinen.
Tämä on hyvä esimerkki siitä, että virheilmoitus ei aina osoita siihen paikkaan, jossa
varsinainen virhe on. Virheilmoitus osoittaa nyt MatchTest
-luokan ohjelmakoodiin
ja ilmoittaa, että se ei toimi. Ja eihän se toimikaan, mutta syy on siinä, että
Match
-luokka on määritelty virheellisesti ja MatchTest
yrittää käyttää sitä tavalla,
jolla käytön pitäisi onnistua.
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.
Vaihe 4/11: 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?
Tämänlaisen virheilmoituksen saadessaan kannattaa aina tarkistaa oikeinkirjoitus. Kirjoitusvirhe voi olla kutsuvassa koodissa tai kutsutussa koodissa.
Vaihe 5/11: homeCount
ja awayCount
Sitten: variable awayCount in class Match cannot be accessed ja sama homeCount
-muuttujalle.
Tämäkin virheilmoitus on varsin onnistunut. Se kertoo, ettei mainittuihin Match
-olion
muuttujiin pääse käsiksi MatchTest
-oliosta.
Jos et jo aiemmin huomannut, miten Match
-luokassa on muuttujat homeCount
ja
awayCount
sekä metodit homeGoals
ja awayGoals
, niin kiinnitä siihen huomiota nyt.
Millaisesta virheestä on kyse? Onko virhe Match
in vai MatchTest
in koodissa?
Korjaa virhe.
Vaihe 6/11: totalGoals
Pari virheilmoitusta liittyy match1.totalGoals()
-metodikutsuihin. Ilmoitus on
tutunlainen (hieman ylempää): Int does not take parameters; samoin sen syy on
tuttu, vaikka tässä ei metodille tarjota varsinaisia parametreja vaan vain tyhjä
parametriluettelo (luku 2.6).
Korjaa.
Vaihe 7/11: Match
-olion luominen taas
Eräs MatchTest
-ohjelman riveistä saa Eclipsen parkaisemaan: not found: value Match.
Tämä tarkoittaa suunnilleen: "En osaa määrittää arvoa lausekkeelle Match
."
Virheilmoitus ei ole sieltä selkeimmästä päästä tällä rivillä olevan virheen
selventäjänä. Se kuitenkin antaa vihjeen siitä, että Scala-työkalusto on yrittänyt
tulkita yksittäistä sanaa Match
lausekkeena (esim. muuttujan tai metodin nimenä).
Niinhän tässä ei haluta tapahtuvan, vaan olisi tarkoitus luoda uusi Match
-olio.
Osannet helposti korjata tilanteen.
Vaihe 8/11: isGoalless
Virhe: type mismatch; found: AnyVal, required: Boolean. Ilmoitus osoittaa
isGoalless
-kutsuihin 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 ja samalla yksinkertaistettua metodin toteutusta? Jos ei meinaa
onnistua, katso alempaa Lisää virhetilanteita: arvojen palauttaminen ja valintakäsky;
tuo kappale käsittelee juuri tällaisia virheitä.
Vaihe 9/11: location
Viimeiset käännösaikaiset virheet oliosta MatchTest
johtuvat jälleen Match
-luokan
vajavaisuudesta. Ilmoitus value location is not a member of o1.football1.Match kertoo
tässä, että location
-metodi on jäänyt Match
-luokasta määrittelemättä. Ilmoitus on
samanlainen kuin yllä addAwayGoal
-metodista, mutta syy on nyt erilainen: tämä metodi
tosiaan puuttuu.
Kirjoita toteutus location
-metodille.
Vaihe 10/11: Testaa
Ohjelman pitäisi nyt olla ajokunnossa. Aja MatchTest
. Mieti, mitä sen pitäisi tulostaa
ja mitä se todellisuudessa tulostaa.
Huomaat, että osa tulosteista on kunnossa, mutta kaikki ei ole hyvin. Osa Match
-luokan
metodeista toimii edelleen dokumentaation vastaisesti, vaikka Eclipse ei luokasta enää
virheitä löydäkään.
Alla on vinkkejä puutteiden korjaamiseksi.
Vaihe 11/11: Korjaa ja täydennä
Korjaa Match
-luokkaan jääneet virheet. Kirjoita scaladocien mukaiseksi myös puuttuva
toString
-metodi.
Ohjeita ja vinkkejä:
- Tulosteeseen ilmestyy
()
, kun tulostetaanUnit
-palautusarvo (ks. luku 1.6). - Muokkaa
MatchTest
-ohjelmaa parhaaksi katsomallasi tavalla virheitä etsiessäsi. - Muistutus (viimeisen kerran tästä): Scaladoceista on syytä avata täydet kuvaukset metodien nimiä klikkaamalla.
- Jos
toString
-metodin toteuttaminen tuottaa vaikeuksia, kannattaa:- Varmistaa, että luit metodin koko Scaladocin.
- Varmistaa, että käytit
override
-sanaa alussa. - 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.
Palauttaminen
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Vähän varoitusilmoituksista
Monet ohjelmointityökalut, Eclipse mukaanlukien, huomauttavat joskus epäilyttävästä koodista varoituksilla (warning). Eclipse merkitsee varoitukset kuten käännösaikaiset virheilmoituksetkin, paitsi että värinä on keltainen eikä punainen. Et ole ehkä vielä törmännyt varoitusilmoitukseen, mutta ennemmin tai myöhemmin epäilemättä törmäät.
Varoitus tarkoittaa, että koodissa on luultavasti (muttei ihan 100-prosenttisen varmasti) jotain, joka on syytä korjata. Varoituksilla Eclipse sanoo ohjelmoijalle: "Ei taida kannattaa tehdä noin?" Kun Eclipse varoittaa jostakin, se on erittäin usein oikeassa ainakin siinä, että jotain ohjelmassa kannattaa muuttaa. Automaattinen varoitusilmoitus ei vain välttämättä suoraan ilmaise sitä, mitä kannattaa muuttaa.
Käytännössä hyvä nyrkkisääntö on suhtautua varoitusilmoituksiin samalla vakavuudella kuin virheilmoituksiinkin.
Lisää virhetilanteita: muuttujat ja valintakäsky
Tutkitaan luvun lopuksi lisää koodia, joka ei toimi. Seuraavat esimerkit voivat parantaa
ymmärrystäsi if
-käskyistä ja vähän muustakin. Ne voivat myös auttaa välttämään yleisiä
virheitä.
Eräs taannoinen kurssinkävijä laati harjoituksen vuoksi tällaisen min
-funktiota
muistuttavan koodinpätkän ja ihmetteli saamaansa virheilmoitusta. Moni muukin on tehnyt
vastaavan virheen.
def palautaPienempi(eka: Int, toka: Int) = {
if (eka < toka) {
var palautetaan = eka
} else {
var palautetaan = toka
}
palautetaan
}
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)<console>:12: error: overloaded method value + ... cannot be applied to (AnyVal) 7 + kokeilu(10) ^
Virheilmoitus voidaan suomentaa suunnilleen niin, että "plus-operaattoria ei ole
määritelty tietotyypille AnyVal
". Mikä ihmeen AnyVal
?
Ongelman selitys
Virheilmoitus on vähän outo mutta aiheellinen.
Katsotaan koodiamme tarkasti ja muistaen, että palautusarvo on viimeiseksi evaluoitavan lausekkeen arvo.
def kokeilu(luku: Int) = {
if (luku > 0) luku * luku
if (luku <= 0) 0
}
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 sulut
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, jos ilmoituksessa näkyy sana Any ja olet käyttänyt if
-valintakäskyä.
REPLissä tyypit näkyvät helposti tulosteissa. Eclipsen koodieditorissa voit pitää hiiren
kursoria ohjelman osan päällä, niin näkyviin tulee tuon osan tietotyyppi. Seuraavassa
editorista napatussa kuvassa hiiri lepää kokeilu
-sanan päällä, ja esiin tulee tietoja
funktion parametreista ja palautusarvon tyypistä:
Lisäkikka: ohjelmointityökalut osaavat joissakin tapauksissa antaa selkeämpiä virheilmoituksia, jos kirjaat funktiollesi palautusarvojen tyypin erikseen (luvun 1.8 mukaisesti):
def kokeilu(luku: Int): Int = // jne.
Tyypin saa kirjata näin vaikka kaikille funktioille. Palautusarvojen vapaaehtoinen kirjaaminen voi muutenkin selkiyttää ohjelmia ja vähentää virheitä. Monet Scala-ohjelmoijat merkitsevät palautusarvon tyypin kaikkiin julkisiin metodeihin (mitä sinun kuitenkaan ei ole pakko O1:ssä tehdä).
Yhteenvetoa
- Kurssilla Scaladoc-dokumentteja käytetään usein tehtävänannoissa.
- Ohjelmointityökalujen antamat virheilmoitukset ovat usein epäselviä, mutta niitä voi tulkita ja harjoittelu auttaa.
- Kun
if
-käskyn haarat kattavat kaikki mahdolliset tapaukset, on syytä käyttääelse
-sanaa ilman ehtolauseketta, jotta asia tulee ohjelmointityökaluillekin selväksi. - Lukuun liittyviä termejä sanastosivulla: dokumentaatio, Scaladoc;
if
; käännösaikainen virhe, syntaksivirhe.
Palaute
Huomaathan, että tämä on henkilökohtainen osio! Vaikka olisit tehnyt lukuun liittyvät tehtävät parin kanssa, täytä palautelomake itse.
Tekijät
Tämän oppimateriaalin kehitystyössä on käytetty apuna tuhansilta opiskelijoilta kerättyä palautetta. Kiitos!
Kierrokset 1–13 ja niihin liittyvät tehtävät ja viikkokoosteet on laatinut Juha Sorva.
Kierrokset 14–20 on laatinut Otto Seppälä. Ne eivät ole julki syksyllä, mutta julkaistaan ennen kuin määräajat lähestyvät.
Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.
Tehtävien automaattisen arvioinnin ovat toteuttaneet: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä ja Aleksi Vartiainen.
Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.
Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista ovat suunnitelleet Juha Sorva ja Teemu Sirkiä. Niiden teknisen toteutuksen ovat tehneet Teemu Sirkiä ja Riku Autio käyttäen Teemun toteuttamia Jsvee- ja Kelmu-työkaluja.
Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset on laatinut Juha Sorva.
O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.
Opetustapa, jossa käytämme O1Libraryn työkaluja (kuten Pic
) yksinkertaiseen graafiseen
ohjelmointiin on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi
oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.
Oppimisalusta A+ on luotu Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Pääkehittäjänä toimii tällä hetkellä Jaakko Kantojärvi, jonka lisäksi järjestelmää kehittävät useat tietotekniikan ja informaatioverkostojen opiskelijat.
Kurssin tämänhetkinen henkilökunta on kerrottu luvussa 1.1.
Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.
bottlePrice
-muuttuja on rooliltaan tuoreimman säilyttäjä (luku 2.6); koska hintaa on tarkoitus muuttaa sijoituskäskyillä, niin otetaan se talteenvar
-muuttujaan (kuten dokumentaatiokin sanoo).