Luku 4.4: Olemattomuusharjoituksia

../_images/person08.png

Tehtävä: pieni parannus limuautomaattiin

Saatat muistaa, että luvun 3.5 lopussa hieman kyseenalaistettiin laaditun VendingMachine-luokan metodin sellBottle toteutusta.

Ota tuo luokka esiin Miscellaneous-moduulista ja muokkaa sitä. Korvaa sellBottle uusitulla versiolla, jonka paluuarvo on tyyppiä Option[Int]. Metodi ei siis enää palauta miinus ykköstä epäonnistuneen oston merkiksi. Sen sijaan metodin tulee palauttaa None, jos pullon myynti epäonnistui, ja Some-olioon kääritty vaihtorahan määrä, jos onnistui.

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

Tehtävä: Member-luokka

Samassa Miscellaneous-moduulissa on muista erillinen esimerkkiluokka o1.people.Member. Ota esiin sen Scaladoc-dokumentaatio ja ohjelmakoodi. Huomaat, että koodi ei vastaa dokumentaatiota: metodit isAlive ja toString puuttuvat. Toteuta ne.

Tehtävän voi ratkaista esimerkiksi match-käskyllä, mutta kätevämmin se ratkeaa, kun poimit pari sopivaa Option-olioiden metodia käyttöösi edellisestä luvusta 4.3.

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

Tehtävä: Passenger-luokka

Tehtävänanto

o1.people-pakkauksen dokumentaatiosta löytyy myös matkustajia kuvaava luokka Passenger. Sille ei kuitenkaan ole annettu lainkaan toteutusta. Toteuta luokka.

Ohjeita ja vinkkejä

  • Luokka hyödyntää toista luokkaa nimeltä TravelCard. Se on annettu valmiina. Älä muuta sitä.

  • Dokumentaatio kuvaa Passenger-luokan luontiparametrit ja (niitä suoraan vastaavat) julkiset ilmentymämuuttujat. Tällä kertaa et tarvitse yksityisiä ilmentymämuuttujia.

  • Kuten dokumentaatiokin kertoo, matkustajilla tulee olla Option[TravelCard]-tyyppinen ilmentymämuuttuja eli jokaisella matkustajalla on nolla tai yksi matkakorttia. Tässä siis kääritään Optioniin viittaus omatekoisen luokan TravelCard ilmentymään.

  • Passenger-luokalle ei ole tarjottu tiedostoa lainkaan, joten joudut luomaan sen tyhjästä pakkaukseen o1.people esimerkiksi näin:

    1. Klikkaa tuota pakkausta IntelliJ’n Project-näkymässä hiiren oikealla napilla ja valitse New → Scala Class/File. Avautuu pieni ikkuna.

    2. IntelliJ osaa pohjustaa kooditiedoston kätevästi, kunhan syötät hieman lisätietoja. Kirjoita Name-kohtaan luokalle nimeksi Passenger ja paina Enter.

    3. Editoriin avautuu uusi kooditiedosto, jossa on hieman alkua luokalle.

  • Metodien toteutukseen löytyy työkaluja luvusta 4.3.

  • Jos päädyt vaikeuksiin isValid-muuttujan käytön kanssa, voit katsoa lisävinkit alta.

Alkuvinkki isValid-muuttujasta

Käytettävissäsi on Option[TravelCard]-tyyppinen arvo. Sillä ei ole isValid-muuttujaa, vaan sen mahdollisesti sisältämällä TravelCard-oliolla on. Koodi this.card.isValid ei siis toimi.

Käsittele erikseen tapaukset, joissa Option on Some ja None. Käytä Some-olion sisällön isValid-muuttujaa.

Jatkovinkki isValidin käytöstä

Kun käsittelet Some-tapauksen match-käskyllä, saat "napattua" Some-olion sisällön muuttujaan, jonka nimen voit valita itse (luku 4.3). Tuon muuttujan kautta pääset käsiksi kyseisen TravelCard-olion isValid-muuttujaan: muuttujanNimi.isValid.

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

Tehtävä: Tilaus-luokan parantelu

Seuraava pieni tehtävä tarjoaa hieman lisäharjoitusta Option-olioiden käsittelyssä ja match-käskyn käytössä. Mitään uutta tässä ei tule, mutta voit tehdä tehtävän, jos äskeiset takeltelivat tai jos alempana olevia tehtäviä tehdessäsi toteat pienen esiharjoituksen olevan paikallaan.

Lisätehtävä

Palaa Oliointro-moduulin Tilaus-luokkaan.

Luvussa 2.6 oli pieni vapaaehtoinen tehtävä, jossa tilauksille tehtiin kuvaus-metodin sijaan toString-metodi.

Jos et tehnyt tuota muutosta silloin, tee se nyt: vaihda kuvaus-metodin nimeksi toString ja kirjoita override eteen. Tee sama muutos myös Asiakas-luokkaan.

Muokkaa sitten luokkaa seuraavasti:

  • Lisää Option[String]-tyyppinen luontiparametri osoite ja sitä vastaava val-ilmentymämuuttuja. Sitä käytetään tallentamaan (mahdollinen) osoite, johon tilaus toimitetaan asiakkaan osoitteen sijaan.

  • Lisää parametriton, vaikutukseton metodi toimitusosoite, jonka String-tyyppinen paluuarvo kertoo, mihin tilaus toimitetaan. Se on tilaukselle erikseen kirjattu osoite, mikäli sellainen on, tai tilaajan osoite, ellei tilauksella ole erillistä osoitetta.

  • Muokkaa toString-metodia niin, että sen palauttaman merkkijonon loppuun tulee lisäksi pilkku ja välilyönti ", " sekä joko merkkijono "asiakkaan osoitteeseen" tai "osoitteeseen X", missä X on tilaukselle erikseen kirjattu osoite.

    • (Huomaa, että toString-kuvauksen muodostuslogiikka on hieman erilainen kuin toimitusosoite-funktion paluuarvon.)

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

Tehtävä: kätevää nuolinäppäilyä

Seuraava vapaaehtoinen tehtävä on mielekäs lähinnä, jos olet jo tehnyt Trotter-tehtävän luvusta 3.6. Voit toki tehdä tuon aimman lisätehtävän nytkin.

Direction.fromArrowKey

o1-pakkauksessa on Direction-luokan lisäksi samanniminen yksittäisolio. Sillä on kätevä metodi fromArrowKey, joka toimii tähän tapaan:

val exampleKey = Key.UpexampleKey: o1.Key.Value = Up
Direction.fromArrowKey(exampleKey)res0: Option[Direction] = Some(Direction.Up)
Direction.fromArrowKey(Key.X)res1: Option[Direction] = None

Metodi siis palauttaa näppäintä vastaavan suunnan. Apuna se käyttää Option-luokkaa, koska vain osaa näppäimistä vastaa jokin suunta.

Muokkaa MoreApps-moduulin TrotterApp-ohjelman onKeyDown-metodia. Sen pitää toimia kuin ennenkin, mutta saanet tehtyä sille aiempaa yksinkertaisemman ja vähemmän toisteisen toteutuksen, kun käytät fromArrowKey-metodia.

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

Tähtikarttatehtävä, osa 1/4: tähden perustiedot

Johdanto: tähtiä koordinaatistoissa

Laaditaan ohjelma, jolla voi kuvata tähtikarttoja eli näkymiä tähtitaivaasta. Kuhunkin tähtikarttaan sisältyy tähtiä; lisäksi tähtiä voi (kunhan ohjelmamme aikanaan valmistuu) yhdistää toisiinsa muodostaen tähtikuvioita.

Tässä tehtäväsarjan ensimmäisessä osassa ei vielä piirretä mitään, vaan luodaan väline yksittäisten tähtien tietojen mallintamiseksi.

Nouda moduuli Stars. Nyt ajankohtaisia luokkia ovat Star ja StarCoords, joista ensimmäisen tulet itse toteuttamaan jälkimmäistä valmiina annettua luokkaa apuna käyttäen. Lue näiden luokkien dokumentaatio; älä välitä muista luokista.

Kuten dokumentaatiokin kertoo, tämä ohjelma käsittelee kahdenlaisia kaksiulotteisia koordinaatteja:

../_images/star_coords.png

StarCoords-koordinaatisto on rajattu miinus yhden ja plus yhden välille. Esimerkiksi jos x on +1.0 ja y 0.0, niin kyseessä on sijainti aivan kartan oikean reunan keskellä.

  1. Kunkin tähden sijainti tähtikartalla kuvataan StarCoords-tyyppisenä koordinaattiparina.

    • Tässä yhteydessä käytämme matematiikasta tuttua koordinaatistoa, jossa y-arvot kasvavat yläreunaa kohti.

    • Sekä x- että y-arvot on normalisoitu välille [-1.0...+1.0]; ks. kuva. Nämä arvot kuvaavat tähden sijaintia näkyvällä taivaalla riippumattomasti siitä, minkäsuuruisiin kuviin tähtikartta saatetaan piirtää.

  2. Toisaalta StarCoords-luokan metodi toImagePos-metodi osaa tuottaa Pos-tyyppisen koordinaattiparin, joka kuvaa tähden sijaintia tähtikartan kuvassa. Nämä koordinaatit kasvavat graafiselle ohjelmoinnille tyypilliseen tapaan kuvan vasemmassa yläkulmassa sijaitsevasta origosta oikealle ja alas. Ne kertovat suoraan, mihin pikseliin tähden kuvan keskipiste tulisi piirtää, jos tähtitaivasta kuvataan tietynkokoisena kuvana.

Tehtävänanto

  1. Varmista yllä annetun kuvauksen ja dokumentaation perusteella, että ymmärrät käytetyt kaksi koordinaatistoa. Varmista, että ymmärrät, mitä StarCoords-luokan toImagePos-metodi tekee.

  2. Toteuta sitten luokan Star puuttuvat metodit dokumentaation mukaisiksi.

Ohjeita ja vinkkejä

  • Star-luokan pitäisi siis lopulta toimia tähän tapaan:

    val unnamedStar = Star(28, StarCoords(0.994772, 0.023164), 0.1, None)unnamedStar: o1.stars.Star = #28 (x=0.99, y=0.02)
    unnamedStar.posIn(rectangle(100, 100, Black))res2: o1.world.Pos = (99.7386,48.8418)
    unnamedStar.posIn(rectangle(200, 200, Black))res3: o1.world.Pos = (199.4772,97.6836)
    val namedStar = Star(48915, StarCoords(-0.187481, 0.939228), -1.44, Some("SIRIUS"))namedStar: o1.stars.Star = #48915 SIRIUS (x=-0.19, y=0.94)
    
  • Sinun ei varsinaisesti tarvitse itse toteuttaa tarvittavaa matematiikkaa, kun hyödynnät StarCoords-luokkaa.

  • Sinun ei myöskään tarvitse pyöristää itse tähden koordinaatteja, vaikka toString-metodin tuleekin ne pyöristettyinä esittää. StarCoords-luokan toString-metodi hoitaa homman puolestasi, kunhan käytät sitä.

Desimaalilukujen muotoilua merkkijonoksi

Voit tutustua StarCoords-luokan toString-metodiin, joka muotoilee koordinaatteja kahden desimaalin tarkkuuteen. Lisätietoja siellä käytetystä f-alkuisesta merkinnästä löydät merkkijonoupotuksen eri muotoja kuvailevasta artikkelista.

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

Tähtikarttatehtävä, osa 2/4: tähdet kuvaksi

Tehtävänanto

Stars-moduuliin kuuluu pakkaus o1.stars.gui. Sen tiedostossa skypics.scala määrittelee funktioita, joilla voi muodostaa kuvia tähtikartoista.

Tässä pikkutehtävässä keskitymme funktioon placeStar. Sille voi antaa kuvan ja tähden, ja se palauttaa uusitun version kuvasta, johon myös kyseinen tähti on piirretty. (Piirrämme tähtitaivaan tähdet ympyröinä, ei sakarallisina.)

Sanotaan vaikkapa, että haluamme kuvan, johon on piirretty nämä kaksi tähteä:

val unnamedStar = Star(28, StarCoords(0.994772, 0.023164), 0.1, None)unnamedStar: o1.stars.Star = #28 (x=0.99, y=0.02)
val namedStar = Star(48915, StarCoords(-0.187481, 0.939228), -1.44, Some("SIRIUS"))namedStar: o1.stars.Star = #48915 SIRIUS (x=-0.19, y=0.94)

Homman pitäisi järjestyä näin:

val darkBackground = rectangle(500, 500, Black)darkBackground: Pic = rectangle-shape
val skyWithOneStar = placeStar(darkBackground, unnamedStar)skyWithOneStar: Pic = combined pic
val skyWithTwoStars = placeStar(skyWithOneStar, namedStar)skyWithTwoStars: Pic = combined pic
skyWithTwoStars.show()

Tuo placeStar-funktio on kuitenkin toteuttamatta. Lue sen spesifikaatio dokumentista ja toteuta funktio tiedostoon skypics.scala merkittyyn kohtaan.

Ohjeita ja vinkkejä

Tehtävässä ei ole mitään vaikeaa, kunhan hyödynnät edellisen tehtävän esittelemää työkalustoa.

Kun koodisi toimii, äskeinen esimerkki tuottaa kuvan yläosaan yhden tähden (namedStar) ja aivan oikeaan laitaan keskelle toisen pienemmän (unnamedStar).

Lopuksi

Tehtävän tehtyäsi voit todeta, että noin niitä yksittäisiä tähtiä tosiaan kuvaan saa, mutta kovin kätevää kuvan luominen ei ole, jos tähtiä pitäisi saada esiin muutamaa enemmän.

Kätevämpää olisi, jos tähtien tiedot voisi ladata kerralla jostakin, mihin ne on kirjattu. Niin pian teemmekin. (Voit jo silmäillä kansioiden test ja northern sisältöä. Katso etenkin niistä löytyviä stars.csv-tiedostoja. Palaamme asiaan luvussa 5.2.)

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

Football3-tehtävä

Futistulosohjelmamme (luvusta 3.5) meni jo kerran uusiksi (luvussa 4.2), ja nyt niin käy taas.

Tehtävänanto

Nouda moduuli Football3. Tutustu sen dokumentaatioon. Luokan Match spesifikaatio on nyt osin erilainen kuin ennen, ja luokka Season on uusi tuttavuus.

Laadi näille luokille dokumentaation mukaisesti toimivat toteutukset.

Tehtävä palautetaan kahdessa vaiheessa: ensin Match ja sitten Season.

../_images/module_football3.png

Kaavio keskeisistä yhteyksistä Football3-moduulin luokkien välillä.

Ohjeita ja vinkkejä: uusittu Match-luokka

  • Voit kopioida Match-luokan toteutuksen pohjaksi Football2-moduuliin laatimasi koodin. Varmista vain, että koodin alkuun tulee package o1.football3-pakkausmäärittely (eikä football2).

  • Aiemmin toteutetun Match-luokan voit säilyttää ennallaan, paitsi voittajametodit. Niihin tulee muutoksia:

    • winnerName-metodin lisäksi on metodi winner, joka palauttaa Option-arvon.

    • winningScorerName-metodia ei enää ole; sen tilalle tulee metodi winningScorer, joka palauttaa Option-arvon.

    • (Kun winner-metodisi toimii, kokeile käyttää sitä apuna, kun laadit yksinkertaisemman toteutuksen winnerName-metodille.)

  • Muista: Match isolla on ottelua kuvaavalle luokalle valittu nimi. match pienellä on Scalan käsky (jota ei voi käyttää nimenä; se on varattu sana). Näiden sotkeminen toisiinsa voi tuottaa jännittäviä virhetilanteita.

  • Voit jälleen käyttää testausapuna annettua FootballApp-ohjelmaa.

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

Ohjeita ja vinkkejä: Season-luokka

  • Kun Season-luokkakin on tehty, FootballApp-ohjelma näyttää pääikkunassaan kauden tilastoja ja otteluluettelon.

  • Season-luokkaa toteuttaessasi voi olla apua lukujen 4.2 ja 4.3 ja GoodStuff-moduulin tutkimisesta. Seasonin ja GoodStuff`in Category-luokan välillä on yhtäläisyyksiä.

  • Alta voit halutessasi avata muutaman lisävinkin.

Yleinen vinkki biggestWin-metodiin

GoodStuffin Category-luokassa pidettiin kirjaa kokemuksesta, jolla oli korkein arvosana. Season-luokassa taas pidetään kirjaa ottelusta, jolla on suurin voittomarginaali. Ohjelmat muistuttavat tässä suhteessa kovasti toisiaan.

Suurimman voiton (biggestWin) selvittämisessä voit hyödyntää Option-tyyppistä ilmentymämuuttujaa sopivimman säilyttäjän roolissa juuri samaan tapaan kuin teimme Category-luokassa. Voit päivittää sitä addResult-metodissa vastaavasti kuin Category-luokka tekee addExperience-metodissaan.

Vinkkejä otteluiden vertailemiseen keskenään

Huomasithan, että otteluiden goalDifference-metodi voi palauttaa myös negatiivisen luvun? Isoin voittomarginaali on ottelussa, jonka goalDifferencen itseisarvo on suurin.

Itseisarvon laskemiseen voit käyttää funktiota scala.math.abs.

Voit tehdä vertailun joko addResult-metodissa itsessään tai määrittää sitä varten apumetodin, jota kutsut.

Vinkki tilanteeseen, jossa ohjelmasi kaatuu IndexOutOfBounds-virheeseen

Indeksivirhe tarkoittaa, että olet käyttänyt liian suurta tai (tässä luultavammin) liian pientä indeksiä.

Huomioitko myös tapauksen, jossa otteluita ei vielä ole lainkaan?

Ellei ratkea, katso myös seuraava vinkki.

Vinkki matchNumber- ja latestMatch-metodeihin

Näiden metodien tulee palauttaa ottelu Option-kääreessä. Et tarvitse siihen if-käskyjä tai muutakaan monimutkaista. Luvussa 4.3 esiteltiin metodi, jolla voit poimia alkion kokoelmasta "turvallisesti" Option-muodossa.

Huomasitko muuten, että latestMatch on oikeastaan erikoistapaus matchNumberista? Voit toteuttaa edellisen kutsumalla jälkimmäistä.

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

Pohdittavaa

Dokumentaatiossa erikseen sanottiin, että Season-olioon lisättävien otteluiden oletetaan jo päättyneen. Niihin ei siis enää saa lisätä maaleja. Mitä tapahtuu, jos tätä oletusta rikotaan? Miten vastaavan sovelluksen voisi laatia niin, ettei tuota vaaraa ole?

Lisämateriaalia: monipuolinen match-käsky

Seuraavat laatikot kertovat lisää match-käskystä, jota olemme käyttäneet Option-olioiden käsittelyyn. Tämä ei ole kurssin kannalta keskeistä asiaa mutta kiinnostanee ainakin niitä aiemmin ohjelmoineita lukijoita, jotka haluavat nyt oppia Scala-kieltä mahdollisimman monipuolisesti. Aloitteleva ohjelmoija voi mainiosti ohittaa seuraavan ja opetella nämä asiat joskus myöhemmin.

match on hahmonsovituksen työkalu

match-käskyn yleinen muoto on:

lauseke L match
  case hahmo A => koodia, joka suoritetaan, jos L:n arvo sopii hahmoon A
  case hahmo B => koodia, joka suoritetaan, jos L:n arvo sopii hahmoon B (muttei A:han)
  case hahmo C => koodia, joka suoritetaan, jos L:n arvo sopii hahmoon C (muttei A:han tai B:hen)
  Ja niin edelleen. (Usein katetaan kaikki mahdolliset tapaukset.)

match-käskyllä voi tutkia minkä tahansa lausekkeen arvoa. Teknisemmin sanoen kyseessä on hahmonsovitus (pattern matching): lausekkeen arvoa verrataan...

... ns. hahmoihin (pattern), joilla kuvataan erilaisia tapauksia. Toistaiseksi olemme määritelleet tapaukset käyttäen None- ja Some-hahmoja, mutta tässä kohdassa voi käyttää monenmoisia muitakin eri hahmoja, joista eräitä on esitelty alla.

Alkeellista matchäystä literaaleilla

Oletetaan, että on Int-tyyppinen muuttuja nimeltä luku. Tutkitaan vaikkapa lausekkeen luku * luku * luku arvoa match-käskyllä:

val kuutionKuvaus = luku * luku * luku match
  case 0         => "luku on nolla ja niin sen kuutiokin"
  case 1000      => "kympistä tulee tuhat"
  case muuKuutio => "luku " + luku + ", jonka kuutio on " + muuKuutio

Tässä esimerkissä on kolme eri hahmoa, joihin lausekkeen arvoa yritetään sovittaa järjestyksessä, kunnes sopiva löytyy.

Hahmona voi käyttää literaalia; tässä on käytetty Int-literaaleja. Näistä tapauksista ensimmäinen valitaan, jos luvun kuutio oli nolla, toinen jos se oli tuhat.

Hahmoksi voi myös kirjoittaa uuden muuttujanimen; tässä on valittu nimi muuKuutio. Tällainen tapaus sopii yhteen minkä tahansa arvon kanssa ja tulee siis tässä valituksi ellei kuutio ollut nolla eikä tuhat.

Kun tällainen tapaus kohdataan, syntyy uusi paikallinen muuttuja, jonka arvoksi tapaukseen "osunut" arvo tallentuu. Muuttujan nimeä voi käyttää tapauksen koodissa.

Tässä vielä vastaava esimerkki, jossa Int-literaalien sijaan käsitellään Boolean-literaaleja. Seuraavat koodinpätkät saavat aikaan ihan saman:

if luku < 0 then "negatiivinen" else "ei negatiivinen"
luku < 0 match
  case true  => "negatiivinen"
  case false => "ei negatiivinen"

Palataan kuutionKuvaus-esimerkkiin. Vaihdetaan kahden viimeisen tapauksen järjestystä näin:

val kuutionKuvaus = luku * luku * luku match
  case 0         => "luku on nolla ja niin sen kuutiokin"
  case muuKuutio => "luku " + luku + ", jonka kuutio on " + muuKuutio
  case 1000      => "kympistä tulee tuhat"

Mitkä kaikki seuraavista väitteistä pitävät paikkansa (kun oletetaan, että luku-muuttujalla on jokin Int-tyyppinen arvo)?

Entä seuraava koodi?

val verrokki = 27
val kuutionKuvaus = luku * luku * luku match
  case 0         => "luku on nolla ja niin sen kuutiokin"
  case verrokki  => "luvun kuutio on sama kuin verrokkimuuttujan arvo eli " + verrokki
  case 1000      => "kympistä tulee tuhat"
  case muuKuutio => "luku " + luku + ", jonka kuutio on " + muuKuutio

Tutki esimerkiksi REPLissä, mitkä kaikki seuraavista väitteistä pitävät paikkansa.

Kaiken äskeisen saa aikaan luvusta 3.4 tutuilla ifelse-ketjuillakin. Näin yksinkertaisesti käytettynä match ei vielä ihan pääse oikeuksiinsa. Mutta luepa alta lisää.

Kysyttyä: onko match-käsky suunnilleen sama kuin esim. Javan switch?

Javassa ja joissakin muissa kielissä on switch-niminen käsky, jolla voi valita yhden useasta vaihtoehdosta sillä perusteella, mikä nimenomainen arvo tietyllä lausekkeella on.

Scalan matchillä on yhtäläisyyksiä tuon käskyn kanssa. Kuitenkin switch pystyy ainoastaan valitsemaan tapauksen, joka vastaa yksittäistä arvoa (kuten kuutionKuvaus-esimerkissämme). match tarjoaa monipuolisempia mahdollisuuksia hahmonsovitukseen. Erityisen huomionarvoista on, että match-käskyllä voi

  • tehdä valinnan tyypin perusteella, ja

  • "purkaa" tarkasteltavan olion hahmon mukaisesti ja poimia olion osia paikallisiin muuttujiin.

Muun muassa näistä mahdollisuuksista on esimerkkejä alla.

Lisäehto tapaukselle

Hahmon yhteyteen voi määritellä lisäehdon (pattern guard), jonka pitää myös täyttyä, jotta kyseinen tapaus tulisi valituksi.

val kuutionKuvaus = luku * luku * luku match
  case 0              => "luku on nolla ja niin sen kuutiokin"
  case 1000           => "kympistä tulee tuhat"
  case muu if muu > 0 => "positiivinen kuutio " + muu
  case muu            => "negatiivinen kuutio " + muu

Tämä lisäehto rajaa tapausta: kyseinen tapaus valitaan vain, jos kyseessä on arvo, joka on nollaa suurempi (eikä ole 1000, jonka edellinen tapaus jo kattoi). Tässä käytetään match-käskyn osana samaa if-sanaa kuin tutussa erillisessä if-valintakäskyssäkin.

Viimeiseen tapaukseen päädytään nyt vain silloin, jos kyseessä ei ole nolla, tuhat eikä mikään positiivinen luku.

Alaviiva match-hahmoissa

Alaviivaa voi käyttää match-käskyssä tarkoittamaan "mikä vain" tai "ei väliä mikä". Tässä pari esimerkkiä:

luku * luku * luku match
  case 0    => "luku on nolla ja niin sen kuutiokin"
  case 1000 => "kympistä tulee tuhat"
  case _    => "joku muu kuin nolla tai tuhat"

Alaviivahahmo sopii mihin tahansa arvoon ja tulee valituksi, jos kumpikaan edellisistä ei tullut. Tähän olisi voinut kirjoittaa uuden muuttujan nimen (kuten ylempänä tehtiinkin), mutta jos muuttujalle ei ole käyttöä, pelkkä alaviiva kelpaa.

matchäystä tyypin mukaan

Äskeisissä esimerkeissä hahmot vastasivat erilaisia mutta keskenään samantyyppisiä arvoja. Seuraavassa hahmot ovat keskenään erityyppisiä:

def kokeilu(jonkinlainenArvo: Matchable) =
  jonkinlainenArvo match
    case jono: String          => "kyseessä on merkkijono " + jono
    case luku: Int if luku > 0 => "kyseessä on positiivinen kokonaisluku " + luku
    case luku: Int             => "kyseessä on ei-positiivinen kokonaisluku " + luku
    case vektori: Vector[?]    => "kyseessä on vektori, jossa on " + vektori.size + " alkiota"
    case _                     => "kyseessä on jokin sekalainen arvo"

Esimerkkifunktiomme parametrityyppi on Matchable, mikä tarkoittaa että sille voi antaa minkä tahansa "match-kelpoisen" arvon parametriksi.

Hahmoihin on kirjattu tietotyyppi. Kukin näistä hahmoista tärppää vain, jos tutkittava arvo on kyseistä tyyppiä.

Hahmojen määrittelemillä muuttujilla on vastaavat tyypit. Esimerkiksi vektori-niminen muuttuja on Vector-tyyppinen, ja sen kautta voimme esimerkiksi kutsua vektorien size-metodia.

Olion "purkaminen" match-käskyllä

Yksi match-käskyn näppärimmistä ominaisuuksista on, että hahmossa voi "purkaa" tutkittavan olion, mikäli se sopii kyseiseen hahmoon, ja poimia sen osia talteen muuttujiin. Yksinkertainen esimerkki tästä on tuttu Some-kääreen "purkaminen":

lukuvektori.lift(4) match
  case Some(kaaritty) => "luku " + kaaritty
  case None           => "ei lukua"

Hahmossa määritellään rakenne: jos kyseessä on Some, niin sen sisällä on jokin arvo. Tuo arvo "puretaan esiin" ja poimitaan muuttujaan kaaritty.

Tätä tekniikkaa sopii yhdistellä muihin esiteltyihin. Alla puretaan Option ja samalla koetetaan sovittaa sen mahdollinen sisältö johonkin useasta eri tapauksesta:

lukuvektori.lift(4) match
  case Some(100)                           => "nimenomaan luku sata"
  case Some(kaaritty) if kaaritty % 2 == 0 => "muu parillinen luku " + kaaritty
  case Some(pariton)                       => "pariton luku " + pariton
  case None                                => "ei lukua"

Olion "purkaminen" onnistuu vain, jos kyseiselle luokalle on määritelty, miten sentyyppiset olio puretaan. Tällainen määrittely on monilla Scalan valmiilla luokilla, esimerkiksi Some-olioilla. Vastaavasti O1-kirjaston Pos-tyypille on määritelty, että sen voi purkaa kahdeksi koordinaatiksi kuten tässä:

jokuPos match
  case Pos(x, y) if x > 0 => "koordinaatit, joiden x on positiivinen ja y on " + y
  case Pos(_, y)          => "muu koordinaattipari, jossa y on " + y

"Jos kyseessä on Pos, se on purettavissa kahdeksi luvuksi. Tallenna ne paikallisiin muuttujiin x ja y. Jos näistä x on positiivinen, valitaan tämä tapaus."

"Pura Pos kahdeksi luvuksi, joista ensimmäisellä voi heittää vesilintua ja toinen tallennetaan paikalliseen muuttujaan y." Tämä hahmo sopii kaikkiin mahdollisiin Pos-arvoihin ja tulee väistämättä valituksi, jos ensimmäinen tapaus ei tärpännyt.

Miten purkamistapa sitten määritellään? Helpoin tapa on tehdä luokasta ns. tapausluokka (case class), mistä on tässä yksinkertainen esimerkki:

case class Album(val name: String, val artist: String, val year: Int):
  // ...

Luokkamäärittely on ihan tavallinen paitsi että alussa on sana case.

Tapausluokan luontiparametrit määräävät samalla, millaisiin osiin match-käskyllä voi olion purkaa. Albumiolion voi purkaa nimeksi, artistiksi ja vuodeksi.

Käyttöesimerkki:

jokinAlbumiolio match
  case Album(_, _, vuosi) if vuosi < 2000 => "muinainen"
  case Album(nimi, tekija, vuosi)         => tekija + ": " + nimi + " (" + vuosi + ")"

Hahmot määritellään käyttäen tapausluokan nimeä ja luokan luontiparametreja vastaavia osasia.

Lisätietoja löytyy netistä esimerkiksi hakusanoilla Scala pattern matching ja Scala case class. Kuten todettu, tässä esiteltyjä Scala-kielen ominaisuuksia ei ole O1-kurssilla pakko osata käyttää.

Lisämateriaalia: Option-olioiden get-metodista

Tässä vapaaehtoisessa kappaleessa esitellään eräs Option-olioiden metodi, get. Tämä metodi voi vaikuttaa kätevältä, mutta se on petollinen ja sen käyttöä on syytä välttää. Ja itse asiassa sen käyttö O1:n varsinaisissa ohjelmointitehtävissä on sekä tarpeetonta että kiellettyä. Metodi esitellään tässä vain varoituksena, koska opiskelijat joskus törmäävät siihen muissa verkkomateriaaleissa ja sitten käyttävät sitä aivan turhaan ja koodin laatua huonontaen.

Vaarallinen get-metodi

Yksi tapa avata Option-kääre on kutsua sen parametritonta get-metodia. Kokeile itse REPLissä.

Oletetaan annetuiksi seuraavat käskyt:

val eka = Some(10)
val toka = None

Mikä on lausekkeen eka.get arvo?

Entä mikä on lausekkeen toka.get arvo?

Esimerkiksi Category-luokan addExperience-metodissa olisimme periaatteessa voineet käyttää tätä metodia match-käskyn sijaan:

def addExperience(newExperience: Experience) =
  this.experiences += newExperience
  val newFave =
    if this.fave.isEmpty then
      newExperience
    else
      newExperience.chooseBetter(this.fave.get)
  this.fave = Some(newFave)

Tutkitaan vanhan suosikin olemassaoloa isEmpty-metodilla.

Poimitaan vanha suosikki Option-kääreestä. Tämä tehdään vain else-haarassa, joten on varmaa, että kyseessä ei ole None-arvo.

Option-olion get-metodissa kuitenkin piilee osa samasta vaarasta kuin null-viittauksissakin: jos metodia kutsuu None-arvolle, syntyy ajonaikainen poikkeustilanne. Jää ohjelmoijan muistettavaksi, että ennen get-metodin kutsumista pitää aina varmistaa, että Option-arvolla todella on sisältö. Tämä unohtuu helposti.

Opiskelijan sanoin:

Käärettä ei voi avata, jos karkki puuttuu.

Älä ainakaan ryntää suin päin avaamaan sitä kuten get tekee. Varaudu avatessa mahdolliseen pettymykseen, ettei tule itku.

On ihan hyvä tietää, että get-metodi on olemassa. Saatat nähdä sitä käytetyn muiden laatimissa ohjelmissa. Kaikki muiden kirjoittamat ohjelmat eivät ole laadukasta koodia. Jätä itse tämä metodi käyttämättä. Sen voi aina korvata paremmalla ratkaisulla, esimerkiksi match-käskyllä tai getOrElse-metodikutsulla.

Palaute

Huomaathan, että tämä on henkilökohtainen osio! Vaikka olisit tehnyt lukuun liittyvät tehtävät parin kanssa, täytä palautelomake itse.

Tekijät

Tämän oppimateriaalin kehitystyössä on käytetty apuna tuhansilta opiskelijoilta kerättyä palautetta. Kiitos!

Materiaalin luvut tehtävineen ja viikkokoosteineen on laatinut Juha Sorva.

Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.

Tehtävien automaattisen arvioinnin ovat toteuttaneet: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Joel Toppinen, Anna Valldeoriola Cardó ja Aleksi Vartiainen.

Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.

Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista suunnittelivat Juha Sorva ja Teemu Sirkiä. Teemu Sirkiä ja Riku Autio toteuttivat ne apunaan Teemun aiemmin rakentamat työkalut Jsvee ja Kelmu.

Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset laati Juha Sorva.

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

Tapa, jolla käytämme O1Libraryn työkaluja (kuten Pic) yksinkertaiseen graafiseen ohjelmointiin, on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.

Oppimisalusta A+ luotiin alun perin Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Nykyään tätä avoimen lähdekoodin projektia kehittää Tietotekniikan laitoksen opetusteknologiatiimi ja tarjoaa palveluna laitoksen IT-tuki; sitä ovat kehittäneet kymmenet Aallon opiskelijat ja muut.

A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen suunnitteluun ja toteutukseen on osallistunut useita opiskelijoita yhteistyössä O1-kurssin opettajien kanssa.

Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.

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

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