Tämä kurssi on jo päättynyt.

Kurssin viimeisimmän version löydät täältä: O1: 2024

Luku 4.4: Olemattomuusharjoituksia

Tästä sivusta:

Pääkysymyksiä: Saisinko vielä lisäharjoitusta luokkien laatimisessa, kiitos? Miten käytän itse Option-tyyppiä? Miten käytän itse match-käskyä?

Mitä käsitellään? Ensisijaisesti edellisen luvun aiheita.

Mitä tehdään? Sarja pieniä ohjelmointitehtäviä. Lopussa vapaaehtoista lisämateriaalia luettavaksi.

Suuntaa antava työläysarvio:? Kolme tai neljä tuntia.

Pistearvo: A140.

Oheisprojektit: Miscellaneous, Stars (uusi), Football3 (uusi). Lisätehtävissä esiintyvät myös vanhat projektit Oliointro ja MoreApps.

../_images/person08.png

Tehtävä: pieni parannus limuautomaattiin

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

Tehtävänanto

Ota tuo luokka esiin projektista Miscellaneous ja muokkaa sitä. Korvaa sellBottle uusitulla versiolla, jonka palautusarvo 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.

Palauttaminen

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

Tehtävä: Member-luokka

Tehtävänanto

Samassa Miscellaneous-projektissa 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.

Vinkkejä

  • 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.
  • REPLissä kokeillessasi muista pakkaukset: import o1.people._.

Palauttaminen

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 konstruktoriparametrit 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 pakkausta Eclipsen Package Explorerissa hiiren oikealla napilla ja valitse New ‣ Scala Class. Avautuu pieni ikkuna.
    2. Eclipse osaa pohjustaa kooditiedoston kätevästi, kunhan syötät hieman lisätietoja. Kirjoita Name-kohtaan luokalle nimeksi o1.people.Passenger.
    3. Muita kohtia ei tarvitse muuttaa. Paina Finish. Editoriin avautuu uusi kooditiedosto, jossa on hieman alkua luokalle.
  • Metodien toteutukseen löytyy työkaluja luvusta 4.3.

Palauttaminen

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

Muokkaa sitten luokkaa seuraavasti:

  • Lisää Option[String]-tyyppinen konstruktoriparametri 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 palautusarvo 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 siten, 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 palautusarvon.)

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-projektin 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 projekti 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 kohden.
    • 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äsuuruiseen kuvaan tähtikartta mahdollisesti piirretään.
  2. Toisaalta Star-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ä olevan 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:

    import o1.stars._import o1.stars._
    val unnamedStar = new Star(28, new StarCoords(0.994772, 0.023164), 4.61, 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 = new Star(48915, new 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ä.

Palauttaminen

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

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

Tehtävänanto

Stars-projektiin kuuluu yksittäisolio o1.stars.io.SkyPic, jonka metodeilla voi muodostaa kuvia tähtikartoista. Tässä pikkutehtävässä keskitytään metodiin placeStar. Sille voi antaa kuvan ja tähden ja se palauttaa uusitun version kuvasta, johon myös kyseinen tähti on piirretty.

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

val unnamedStar = new Star(28, new StarCoords(0.994772, 0.023164), 4.61, None)unnamedStar: o1.stars.Star = 28 (x=0.99, y=0.02)
val namedStar = new Star(48915, new 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 = SkyPic.placeStar(darkBackground, unnamedStar)skyWithOneStar: Pic = combined pic
val skyWithTwoStars = SkyPic.placeStar(skyWithOneStar, namedStar)skyWithTwoStars: Pic = combined pic
skyWithTwoStars.show()

Tuo placeStar-metodi on kuitenkin toteuttamatta. Lue sen spesifikaatio dokumentista ja toteuta metodi tiedostoon SkyPic.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.
  • 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öä, erityisesti niistä löytyviä stars.csv-tiedostoja.
    • Palaamme asiaan luvussa 5.2.

Palauttaminen

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

../_images/project_football3.png

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

Ohjeita ja vinkkejä

  • Voit kopioida Match-luokan toteutuksen pohjaksi Football2-projektiin laatimasi koodin. Nyt laadittavaksi pyydetty versio poikkeaa siitä vain näin:
    • 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 laatiaksesi 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. Kun Season-luokka on tehty, ohjelma näyttää pääikkunassaan kauden tilastoja ja otteluluettelon.
  • Season-luokkaa toteuttaessasi voi olla apua lukujen 4.2 ja 4.3 ja GoodStuff-projektin tutkimisesta.
    • Suurimman voiton (biggestWin) selvittämisessä voit hyödyntää Option-tyyppistä ilmentymämuuttujaa sopivimman säilyttäjän roolissa samaan tapaan kuin teimme Category-luokassa.
    • Suuremmuusongelman voi ratkaista myös toisilla ohjelmointitekniikoilla, joita ei ole vielä kurssilla käsitelty (esim. toistokäskyt; luku 5.5). Niiden käyttö on kyllä sallittua, jos satut ennestään osaamaan, mutta ei välttämätöntä.
  • Huomasithan, että otteluiden goalDifference-metodi voi palauttaa myös negatiivisen luvun?

Palauttaminen

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

Pohdittavaa

Dokumentaatiossa erikseen sanottiin, että Season-olioon lisättävien otteluiden oletetaan jo päättyneen eikä niihin 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ä kohden 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 mikäli kuutio ei 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) "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) kun taas 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). Huomaa, että 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: Any) =
  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 Any, mikä tarkoittaa että sille voi antaa minkä tahansa tyyppisen arvon parametriksi. (Lisää Anystä luvussa 7.3.)
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" on mahdollista 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 konstruktoriparametrit 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 konstruktoriparametreja vastaavia osasia.

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

Lisämateriaalia: Option-olioiden get-metodista

Tässä vapaaehtoisessa kappaleessa esitellään eräs Option-olioiden metodi, joka voi vaikuttaa petollisen kätevältä mutta jonka käyttöä kannattaa välttää.

Vaarallinen get-metodi

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

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

Ainakaan sitä ei kannata rynnätä suin päin avaamaan kuten get tekee. Avatessa kannattaa varautua mahdolliseen pettymykseen, ettei tule itku.

On ihan hyvä tietää, että get-metodi on olemassa. Saatat nähdä sitä käytetyn muiden laatimissa ohjelmissa. Jätä itse 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!

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.

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