Kurssin viimeisimmän version löydät täältä: O1: 2024
Luku 6.3: Kokoelmia ja madonruokaa
Tästä sivusta:
Pääkysymyksiä: Miten käsittelen alkiokokoelmia kätevästi korkeamman asteen metodeilla?
Mitä käsitellään? Valikoima Scalan kokoelmien metodeita. Edellisen luvun funktioliteraalit ovat runsaassa käytössä.
Mitä tehdään? Luetaan ja ohjelmoidaan.
Suuntaa antava työläysarvio:? Kolme, neljä tuntia.
Pistearvo: A30 + B100.
Johdanto
Tämä luku esittelee valikoiman metodeita, joita voi käyttää silmukoiden sijaan tai lisäksi kokoelmia käsitellessä. Metodeille on yhteistä se, että ne löytyvät Scalan valmiilta kokoelmatyypeiltä sekä se, että ne vastaanottavat parametreina funktioita, jotka täsmentävät sitä, mitä kokoelman alkioilla tehdään. Metodit vastaavat muun muassa näihin tarpeisiin:
- Miten toistetaan tietty toimenpide kullekin alkiolle kokoelmassa (esim. tulostaminen)?
- Miten tutkitaan kokoelman yleisiä ominaisuuksia (esim. ovatko kaikki alkiot tietynlaisia)?
- Miten valikoidaan alkioita kokoelmasta (esim. kaikki alkiot, jotka ovat tietynlaisia)?
- Miten muodostetaan tulos yhdistelemällä kokoelman alkioita (esim. lukujen neliöiden summa tai kuvia päällekkäin sijoittamalla saatu yhdistelmä)?
- Miten muodostetaan toinen alkiokokoelma, jonka kukin alkio saadaan tietyllä "kaavalla" alkuperäisestä kokoelmasta (esim. henkilöolioiden luettelosta henkilötunnusten luettelo)?
Näiden korkeamman asteen metodien käyttö on usein kätevää; niitä käyttäen kirjoitettu koodi on usein lyhyttä ja selkeää. Esitellyt metodit ovat erityisen ominaisia funktionaaliselle ohjelmointityylille (mistä lisää luvussa 10.2), kun taas imperatiivisessa ohjelmointitavassa silmukat ovat yleisempiä.
Enemmänkin löytyy
Scalan kokoelmakirjastot ovat hyvin runsaat: on paljon erilaisia kokoelmatyyppejä, ja kokoelmilla on paljon erilaisia metodeita. Kaikkia ei käsitellä tässäkään. Kokoelmakirjastot on koko komeudessaan kuvattu Scalan perus-API:n dokumentaatiossa.
Suuri osa luvusta koostuu pienistä irrallisista esimerkeistä, jotka esittelevät valmiiden metodien käyttöä. Tämä luku on eräänlainen jatko-osa luvulle 4.2, jossa esiteltiin eräitä kokoelmien ensimmäisen asteen metodeita.
Monissa seuraavista esimerkeistä käytetään selkeyden vuoksi kokonaislukuvektoreita. Huomaa silti, että aivan samat metodit toimivat myös muille alkiotyypeille kuin luvuille ja muunlaisille Scala-peruskirjastojen kokoelmille kuin vektoreille.
Esimerkeissä esiintyy paljon funktioliteraaleja — sekä tavallisia että alaviivoin lyhennettyjä — joten varmista, että olet ymmärtänyt edellisen luvun esittelemät merkintätavat.
Toimenpiteen toistaminen alkioille: foreach
Scala-kokoelmille määritellyistä korkeamman asteen metodeista tavallaan yleiskäyttöisin
on nimeltään foreach
. Koekäytetään sitä vektorin sisältämien lukujen neliöiden
tulostamiseen.
val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20) luvut.foreach( n => println(n * n) )100 25 16 25 400
foreach
nimensä mukaisesti suorittaa parametriksi annetun
funktion kertaalleen kullekin kokoelman sisältämälle alkiolle.
Tässä tapauksessa kustakin alkiosta lasketaan toinen potenssi
ja tulostetaan se.E
-kirjainta.Lienet jo huomannut, että foreach
on toiminnaltaan hyvin samankaltainen kuin
luvussa 6.1 laatimasi repeatForEachElement
-funktio. foreach
eroaa tuosta aiemmasta
funktiosta ennen muuta olemalla kokoelmaolioille valmiiksi määritelty metodi, jota voit
käyttää missä vain Scala-ohjelmassa.
Koska foreach
-metodille annetaan parametriksi arvoa palauttamaton funktio, niin sitä
käytetään parametrifunktion aiheuttamien vaikutuksien vuoksi. Jotta metodikutsu olisi
hyödyllinen, on parametrifunktion esimerkiksi tulostettava jotain (kuten yllä) tai
vaikutettava jonkin olion tilaan.
Silmukat vs. kokoelmien metodit
Kuten yllä mainittiin, tässä luvussa käsiteltävillä metodeilla voi tehdä samanlaisia
asioita kuin silmukoilla. Tämä korostuu erityisesti ensimmäisessä esimerkissämme eli
foreach
-metodissa. Voidaanhan kirjoittaa myös:
for (n <- luvut) {
println(n * n)
}
Tosiaan, nämä kaksi tekevät keskenään ihan saman:
for (alkio <- alkioita) { Tee jotain alkiolla. }
alkioita.foreach( alkio => Tee jotain alkiolla. )
Itse asiassa tällainen for
-silmukka on vain erilainen tapa merkitä foreach
-metodikutsu;
Scala-kääntäjä tulkitsee sen metodikutsuksi.
Tässä vielä esimerkki AuctionHouse
-luokan metodista, joka toteutettiin siellä näin:
def nextDay() = {
for (current <- this.items) {
current.advanceOneDay()
}
}
Olisi voitu myös kirjoittaa:
def nextDay() = {
this.items.foreach( _.advanceOneDay() )
}
Riippuu tilanteesta ja ohjelmoijan mieltymyksistä, kumpi tapa on luontevampi ja selkeämpi.
Kurssin tulevissa esimerkeissä käytetään foreach
-metodia varsin usein (mutta
for
-silmukkaa myös). Voit itse käyttää kumpaa haluat, mutta sivistyneen Scala-ohjelmoijan
on syytä tuntea molemmat tavat.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Välipuhe abstraktiotasoista
Kokoelman ominaisuuksien tutkiminen
exists
- ja forall
-metodit
exists
-metodilla voi kätevästi selvittää, toteutuuko tietty kriteeri minkään alkion
kohdalla:
val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20) luvut.exists( _ < 0 )res0: Boolean = true luvut.exists( _ < -100 )res1: Boolean = false
exists
-metodille annetaan parametriksi funktio, joka ottaa
parametrikseen kokoelman alkion ja palauttaa totuusarvon, joka
kertoo, toteuttaako kyseinen alkio kriteerin. Esimerkiksi tässä
välitetään ensin parametriksi funktio, joka kertoo, onko sen
parametriarvo negatiivinen.forall
-metodi vastaavasti tutkii, päteekö annettu kriteeri kaikille alkioille:
luvut.forall( _ > 0 )res2: Boolean = false luvut.forall( _ > -100 )res3: Boolean = true
Tässä tutkittiin ensin, ovatko kaikki alkiot positiivisia; eivät ole. Toisella
forall
-kutsulla selvisi, että kaikki alkiot ovat miinus satasta suurempia.
Pikkutehtäviä: count
, exists
ja forall
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Tehtävän jälkimaininki
Miksi tuossa tehtävässä oli muotoiltu koodi näin?
// Versio 1 (toimiva)
kokoelma.forall( !jokuEhto(_) )
Eikö olisi voinut lukea seuraavasti?
// Versio 2 (toimimaton)
kokoelma.forall( !jokuEhto )
Kuten luku 6.2 kertoi, on monia tapoja kirjoittaa funktioliteraaleja. Kuitenkaan tässä tapauksessa Versio 2 ei toimi, koska se ei määrittele mitään funktioliteraalia.
forall
-metodille, samoin kuin monille muille kokoelmien metodeille,
tulee antaa parametriksi funktio, jota voi soveltaa kuhunkin kokoelman
alkioon. Lausekkeen !jokuEhto
arvo ei kuitenkaan ole mikään funktio,
vaikka jokuEhto
-niminen funktio olisikin olemassa.
Jotta saamme tämän käänteisen tapauksen muotoiltua, pitää forall
-metodille
välittää parametriksi sellainen erikseen määritelty funktio, joka
kutsuu jokuEhto
-funktiota ja soveltaa negaatio-operaattoria !
saamaansa palautusarvoon. Sen voi tehdä alaviivalla kuten ylempänä
tai nuolinotaatiolla kuten tässä:
// Versio 3 (toimiva)
kokoelma.forall( alkio => !jokuEhto(alkio) )
Alkioiden valikoiminen kokoelmasta
find
-, filter
-, takeWhile
- ja dropWhile
-metodit
find
-metodi etsii ensimmäisen alkion, joka täyttää parametrifunktion määrittelemän
kriteerin. Esimerkiksi tässä etsitään ensimmäinen viitosta pienempi luku:
val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20) luvut.find( _ < 5 )res4: Option[Int] = Some(4)
Huomaa: find
palauttaa Option
-arvon, johon on "kääritty" löydetty alkio.
Jos haku oli "huti", saadaan None
:
luvut.find( _ == 100 )res5: Option[Int] = None
filter
-metodi on samansuuntainen kuin find
, mutta se palauttaa kokoelman kaikista
niistä alkioista, jotka täyttävät kriteerin. Esimerkiksi tässä vektorista löytyy neljä
positiivista lukua:
val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20) luvut.filter( _ > 0 )res6: Vector[Int] = Vector(10, 5, 4, 5)
Luvussa 4.2 esiteltiin kokoelmien take
-metodi, joka palauttaa osakokoelman, jossa on
parametriksi annettu lukumäärä alkioita alkuperäisen kokoelman alusta (esim. luvut.take(3)
).
Tämän metodin korkeamman asteen serkku on takeWhile
:
val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20) luvut.takeWhile( _ >= 5 )res7: Vector[Int] = Vector(10, 5)
Metodi siis poimi kokoelman alusta peräkkäisiä alkioita kunnes kohdataan sellainen, joka ei täytä parametrin määräämää kriteeriä.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Pikkutehtäviä: filter
vs. filterNot
, takeWhile
vs. dropWhile
Tehtävä: Election-ratkaisua uusiksi (osa 1/2)
[Tämä] tehtävä oli kyllä hyvä, lyhenipä koodi kummasti.
Luvussa 5.6 toteutit metodeita Election-projektin District
-luokkaan. (Jos et toteuttanut,
tee se nyt tai ota esiin sen tehtävän esimerkkiratkaisu.) Silloin käytit
silmukoita.
Toteuta metodeista nyt kaksi uusiksi: printCandidates
ja candidatesFrom
. Kumpaankin
löytyy yksinkertainen ratkaisu käyttämällä jotakin yllä esitellyistä metodeista. Käytä
niitä metodirungoissa for
-silmukoiden sijaan.
Palaamme District
-luokan muihin metodeihin vielä.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Alkioiden kuvaaminen toisiksi: map
- ja flatMap
-metodit
Eräs erittäin usein hyödyllinen toimenpide on alkiokokoelman kuvaaminen eli "mäppäys" toiseksi alkiokokoelmaksi.
map
-metodi palauttaa uuden kokoelman, jonka kukin alkio on saatu soveltamalla
parametrifunktiota alkuperäisen kokoelman alkioon. Ensimmäisessä esimerkissämme
map
-käsky laskee kustakin vektorin alkiosta itseisarvon ja palauttaa uuden vektorin,
jossa itseisarvot ovat alkuperäisiä alkioita vastaavassa järjestyksessä:
val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20) luvut.map( _.abs )res8: Vector[Int] = Vector(10, 5, 4, 5, 20)
Seuraava map
-käsky puolestaan selvittää kustakin alkiosta, onko se vähintään viisi.
Tulosvektorissa ovat järjestyksessä näin saadut totuusarvot:
luvut.map( _ >= 5 )res9: Vector[Boolean] = Vector(true, true, false, true, false)
map
-metodilla voi kohdistaa mitä moninaisimpia muutosfunktioita useaan alkioon
kerralla. Kuten muutkin tämän luvun esittelemät metodit, se löytyy myös muilta
alkiokokoelmilta kuin vektoreilta. Esimerkiksi 1 to 10
-lausekkeen arvoksi syntyvä
Range
-olio (luku 5.6) on kokoelma:
(1 to 10).map( n => n * n )res10: IndexedSeq[Int] = Vector(1, 4, 9, 16, 25, 36, 49, 64, 81, 100)
Tässä vielä yksi esimerkki, jossa alkioina on Double
-arvoja ja käytämme nimettyä
funktiota:
import scala.math.sqrtimport scala.math.sqrt val data = Vector(100.0, 25.0, 12.3, 2, 1.21)data: Vector[Double] = Vector(100.0, 25.0, 12.3, 2.0, 1.21) data.map(sqrt)res11: Vector[Double] = Vector(10.0, 5.0, 3.5071355833500366, 1.4142135623730951, 1.1)
Pieniä map
-tehtäviä
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
map
vs. transform
map
-metodin sukulainen on transform
-metodi, joka on ainoastaan
muuttuvatilaisilla kokoelmilla kuten puskureilla (muttei esim.
vektoreilla). Siinä missä map
palauttaa uuden kokoelman,
transform
vaihtaa alkuperäisen kokoelman alkiot toisiksi käyttäen
parametriksi saamaansa funktiota. Se siis muistuttaa tältä(kin) osin
luvussa 6.1 tekemääsi transformEachElement
-funktiota.
Luvun 6.1 esittelemä Pic
-olioiden transformColors
-metodi on
vahvasti map
ille sukua sekin: se "mäppää" värejä väreiksi.
flatMap
-metodi
Luvussa 6.1 näyttäytyi metodi flatten
, joka "litistää" kokoelmia sisältävän kokoelman:
val sisakkain = Vector(Vector(3, -10, -4), Vector(5, -10, 1), Vector(-1), Vector(4, 4))sisakkain: Vector[Vector[Int]] = Vector(Vector(3, -10, -4), Vector(5, -10, 1), Vector(-1), Vector(4, 4)) sisakkain.flattenres12: Vector[Int] = Vector(3, -10, -4, 5, -10, 1, -1, 4, 4)
On melko yleistä, että halutaan suorittaa peräkkäin map
ja flatten
: ensin "mäppäys"
ja sitten litistäminen. Tässä pieni kokeiluesimerkki:
val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20) val kokeilu = luvut.map( n => Vector(-n.abs, 0, n.abs) )kokeilu: Vector[Vector[Int]] = Vector(Vector(-10, 0, 10), Vector(-5, 0, 5), Vector(-4, 0, 4), Vector(-5, 0, 5), Vector(-20, 0, 20)) kokeilu.flattenres13: Vector[Int] = Vector(-10, 0, 10, -5, 0, 5, -4, 0, 4, -5, 0, 5, -20, 0, 20)
"Mäppäyksen" ja litistämisen voi yhdistää:
luvut.flatMap( n => Vector(-n.abs, 0, n.abs) )res14: Vector[Int] = Vector(-10, 0, 10, -5, 0, 5, -4, 0, 4, -5, 0, 5, -20, 0, 20)
Käsky kokoelma.flatMap(funktio)
siis ajaa saman asian kuin kokoelma.map(funktio).flatten
.
Saatat ihmetellä, onko moiselle tosiaan niin paljon tarvetta, että erillisestä
flatMap
-yhdistelmämetodista on hyötyä. On sille. Metodin hyödyllisyys korostuu,
kun ohjelmointikokemuksesi karttuu tämän kurssin aikana ja sen jälkeen.
Esimerkki: käyttäjiä ja osoitteita
Äskeistä hieman kiinnostavampia esimerkkejä map
- ja flatMap
-metodeista saadaan,
kun tarkastelemme seuraavaa pientä luokkaa.
class Kayttaja(val tunnus: String, val mailiosoitteet: Vector[String]) {
override def toString = this.tunnus + " <" + this.mailiosoitteet.mkString(",") + ">"
}
Oletetaan nyt, että ohjelma käsittelee useita tällaisia käyttäjiä, jotka on tallennettu vektoriin. Alla olevassa esimerkkikoodissa käyttäjäolioita luodaan vain kaksi, mutta niitä voisi olla kuinka paljon tahansa.
val kaikkiKayttajat = Vector( new Kayttaja("taina", Vector("taina.teekkari@aalto.fi", "taina.teekkari@iki.fi", "taina.teekkari@gmail.com")), new Kayttaja("megadestroyer", Vector("teemu.teekkari@aalto.fi", "teemuteekkari@gmail.com")) )kaikkiKayttajat: Vector[Kayttaja] = Vector(taina <taina.teekkari@aalto.fi,taina.teekkari@iki.fi,taina.teekkari@gmail.com>, megadestroyer <teemu.teekkari@aalto.fi,teemuteekkari@gmail.com>)
Mitä, jos halutaan selvittää kaikkien vektoriin tallennettujen käyttäjien käyttäjätunnukset? Tai muodostaa vektori, jossa on kaikkien käyttäjien kaikki sähköpostiosoitteet?
Silmukoita voisi tietysti käyttää, mutta mainituilla korkeamman asteen metodeilla homma onnistuu vaivatta.
Kaikkien käyttäjien käyttäjätunnukset saadaan kauniisti map
-metodilla, kun parametriksi
annetaan funktio, joka "mäppää" olion sen tunnus
-ilmentymämuuttujan arvoksi.
kaikkiKayttajat.map( _.tunnus )res15: Vector[String] = Vector(taina, megadestroyer)
Pelkällä map
-metodilla saadaan myös sähköpostiosoitteet. Ne tulevat sisäkkäisessä
kokoelmassa, mikä ei (tarkoituksesta riippuen) välttämättä ole kätevää:
kaikkiKayttajat.map( _.mailiosoitteet )res16: Vector[Vector[String]] = Vector(Vector(taina.teekkari@aalto.fi, taina.teekkari@iki.fi, taina.teekkari@gmail.com), Vector(teemu.teekkari@aalto.fi, teemuteekkari@gmail.com))
Käyttämällä map
in sijaan flatMap
-metodia saadaan haluttu "litteä" luettelo
kaikista kaikkien käyttäjien sähköpostiosoitteista:
kaikkiKayttajat.flatMap( _.mailiosoitteet )res17: Vector[String] = Vector(taina.teekkari@aalto.fi, taina.teekkari@iki.fi, taina.teekkari@gmail.com, teemu.teekkari@aalto.fi, teemuteekkari@gmail.com)
Seuraava ohjelmointiharjoitus ei käsittele pelkästään äskeisiä kokoelmien metodeita, mutta niillekin löytyy käyttöä.
Tehtävä: matopeli
Matopeli (Snake) on alun perin 1970-luvulta peräisin oleva klassikko, jossa pelaajan ohjaama "mato" tai "käärme" kääntyilee kaksiulotteisella kentällä ja etsii syötävää kasvaakseen. 1990-luvulla se teki näyttävän paluun kännyköihin, millä on viime aikoinakin retroiltu. Tätä lukiessasi peli saattaa jo olla uusioretrokitschiä.
Ohjelmointiharjoituksenakin matopeli on klassinen. Yhtykäämme perinteeseen.
Matopelin käsitteitä
Matopelin keskeiset osat ovat itse mato sekä ruoka, jota mato etsii. Kullakin hetkellä tarjolla on yksi ruoka jossakin päin pelikenttää. Kun mato syö sen eli madon pää on osumassa ruokaan, mato kasvaa ja pelikentälle ilmestyy uusi ruoka.
Pelialueen voi mieltää ruudukoksi (grid), jossa ruoka sijaitsee tietyssä ruudussa (ks. kuva). Mato koostuu "pätkistä" eli segmenteistä, joista kukin sijoittuu tiettyyn ruutuun.
Pelaajan ainoa toimi on kääntää madon liikkumasuuntaa nuolinäppäimillä. Suuntana voi olla jokin neljästä pääilmansuunnasta.
Pelin edetessä mato liikkuu: sen pää eli etummainen segmentti liikkuu viimeisimpään pelaajan valitsemaan suuntaan ja muut segmentit seuraavat perässä (ks. toinen kuva). Kuitenkin jos madon pää osuu ruokaan, mato syö sen ja kasvaa yhdellä segmentillä: ruoan kohdalle ilmestyy uusi segmentti ja kaikki aiemmat jäävät entiselleen.
Peli päättyy, kun madon pää osuu johonkin muuhun kuin ruokaan.
Snake-projektin luokat
Projektissa Snake on osittainen toteutus matopelille. Sen kokonaisrakenne on
samankaltainen kuin tutussa FlappyBug-projektissa: SnakeGame
-luokalla mallinnetaan
muuttuvia pelitilanteita, SnakeApp
pistää pelitilanteen näkyviin käyttöliittymään
ja vastaanottaa pelaajan komennot.
Luokan SnakeGame
ilmentymä on siis yksi matopelisessio. Olion tila muuttuu sen
metodeita kutsuttaessa. SnakeGame
-olio pitää kirjaa:
- seuraavasta tarjolla olevasta ruuasta: missä päin ruudukkoa se on?
- madon segmenttien sijainneista: missä päin ruudukkoa ne ovat?
- madon (pään) kulkusuunnasta: mihin neljästä pääsuunnasta se on viimeksi asetettu kulkemaan?
Luokan pitää siis käsitellä sijainteja ruudukolla sekä suuntia. Näihin tarpeisiin löytyy
välineitä o1
-kirjastostamme:
Apuluokkia: GridPos
ja CompassDir
Luokka GridPos
sopii kuvaamaan kahdesta kokonaisluvusta koostuvia koordinaatteja
ruudukossa.
- Se on samantapainen kuin tuttu
Pos
, mutta edustaa nimenomaan ruudukkokoordinaatteja ja tarjoaa siihen tarkoitukseen sopivia metodeita. - Näin erotamme toisistaan sen missä ruudussa jokin on (
GridPos
) siitä, mihin pisteeseen tietynkokoisessa kuvassa tuo jokin piirretään (Pos
).- Ruudukkosijainti on osa aihealueen logiikkaa (matopelin sääntöjä). Sijainti kuvassa liittyy vain käyttöliittymään.
- Vrt. Stars-projekti, jossa erotimme
toisistaan
StarCoords
-sijainnin kartalla jaPos
-sijainnin kuvassa.
Luokka CompassDir
kuvaa "ilmansuuntia" kuten pohjoinen (eli ylös)
tai länsi (eli vasemmalle).
- Se sopii hyvin madon liikkumasuunnan kirjaamiseen.
- Se toimii hyvin yhteen
GridPos
-luokan kanssa; voimme esimerkiksi kysyä tiettyä koordinaattiparia vastaavaltaGridPos
-oliolta, mikä sen itäinen naapurikoordinaatti on.
Molempien luokkien Scaladoc-dokumentaatio löytyy O1Library-projektista.
Tehtävänanto
Toteuta tiedostoihin SnakeGame.scala
ja SnakeApp.scala
puuttuviksi merkityt kohdat.
Suosittelemme seuraavaa järjestystä.
Vaihe 0/5: pohjustus
Aja annettu ohjelma. Esiin tulee pelimaailma, jossa on ruoka ja yhden segmentin mittainen matonen. (Yllä oleviin kuviin lisäksi piirretyn ruudukon ei ole tarkoituskaan näkyä.) Mato ei vielä liiku.
Tutustu SnakeGame
-luokkaan ja sen kumppaniolioon samassa tiedostossa. Huomaa puuttuvat
osat. Huomaa, miten annettu koodi käyttää GridPos
- ja CompassDir
-luokkia. Lue noiden
luokkien Scaladoc-dokumentaatiosta ainakin alun johdannot; kaikkia metodeita ei ole
tarpeen opetella. (Itse Snake-projektista ei ole scaladoceja, vaan tarvittavat tiedot on
annettu tässä luvussa ja koodissa.)
Voit silmäillä SnakeApp
-ohjelmaakin.
Käyttöliittymä on laadittu niin, että se kutsuu kellon tikittäessä SnakeGame
-olion
advance
-metodia, jonka pitäisi liikuttaa matoa. Annettu versio metodista ei kuitenkaan
tee mitään.
Vaihe 1/5: mato liikkeelle
Toteuta advance
ensin osittaisena: laita se siirtämään madon pää viereiseen ruutuun.
Älä vielä välitä madon kasvattamisesta tai ruoan sijainnista.
Vinkkejä:
segments
-muuttuja osoittaa yksialkioiseen vektoriin, jossa on madon (pään) sijainti. Kyseessä onvar
-muuttuja: korvaa entinen vektori uudella, jossa on vanhan sijainnin naapuri kulkusuunnassa.- Naapurin saa kätevästi
GridPos
-luokan metodilla.
Aja muokattu ohjelma. Yksisegmenttisen madon pitäisi nyt vipeltää ruudulla. Peli päättyy sen törmätessä pelialueen reunaan. Nuolinäppäimet (tai WASD-näppäimet) ohjaavat matoa. Mato menee läpi ruoista niitä nauttimatta.
Vaihe 2/5: ruoka
Kehitä advance
-metodia niin, että ruoalle arvotaan aina uusi sijainti, kun mato
on saapumaisillaan sen kohdalle eli kun madon ainoa segmentti on siirtymässä ruoan
kohdalle.
Tämä käy päivittämällä pelin nextFood
-muuttujaa. Arvo uusi sijainti annetulla
randomLocationOnGrid
-metodilla.
Pelin nopeus on määritelty SnakeApp
-käynnistysolion vakiossa GameSpeed
. Voit
säätää sitä mielesi mukaan. Jos peliä testatessasi vaihdat nopeudeksi esimerkiksi
1
, niin aika matelee hyvin hitaasti.
Vaihe 3/5: kasvu
Viimeistele advance
-metodi: kasvata matoa, kun pää on osumaisillaan ruokaan. Liikuta
muussa tapauksessa kutakin madon segmenttiä pelkän pään sijaan. Metodi siis joka
kutsukerralla joko kasvattaa tai liikuttaa matoa mutta ei molempia.
Nyt ei enää riitä korvata segments
-muuttujan arvoa uudella yksialkioisella vektorilla.
Poista se rivi ja kirjoita uutta koodia.
Vinkkejä:
- Korvaa nyt
segments
-muuttujan vanha arvo uudella vektorilla, jossa on kaikki madon segmenttien uudet sijainnit. Segmenttejä tulisi olla askelen jälkeen joko yhtä monta kuin ennen tai yksi enemmän. - Kasvata matoa etupäästä: kun mato on osumassa ruokaan, muu osa pysyy paikallaan mutta eteen ruoan kohdalle tulee yksi segmentti lisää uudeksi pääksi. (Aiemmin etummaisena ollut segmentti jää toiseksi. Myös viimeinen segmentti säilyy paikoillaan.)
- Uuden vektorin muodostamisessa kannattaa hyödyntää luvun 4.2
esittelemiä työkaluja. Erityisesti vektorin alkuun "lisäävä"
operaattori
+:
voi olla näppärä.uusiEkaAlkio +: entinenVektori
- Segmentit ovat identtisiä. Jokaista segmenttiä ei tarvitse erikseen siirtää. Kun mato liikkuu, kaikki keskimmäiset segmentit pysyvät paikoillaan: näennäinen liike syntyy, kunhan huolehdit madon etu- ja peräpäästä.
- Kokeile ohjelmaa tämän vaiheen lopuksi. Mato ei edelleenkään näytä kasvavan. Kasvaa se kyllä, mutta kasvu ei näy, koska käyttöliittymä ei piirrä näkyviin kuin madon pään.
- Alla on lisävinkkejä, jotka voit paljastaa halutessasi.
Vinkki vektorioperaattorista +:
Tässä kertaus luvun 4.2 esimerkistä:
val ekat = Vector("eka", "toka")ekat: Vector[String] = Vector(eka, toka) val isompi = "nyt eka" +: ekatisompi: Vector[String] = Vector(nyt eka, eka, toka)
Huomaa, että ekat
-muuttuja osoittaa operaation
jälkeenkin muuttumattomaan vektoriin. Jos haluat,
että sama muuttuja viittaa eri vaiheissa eri
vektoreihin, sijoita uudelleen var
-muuttujaan:
var sanat = Vector("eka", "toka")sanat: Vector[String] = Vector(eka, toka) sanat = "oikeesti eka" +: sanatsanat: Vector[String] = Vector(oikeesti eka, eka, toka)
Jatkovinkki vektorimetodeista
Madon edetessä syömättä sinun on muodostettava uusi vektori, jossa on madon pään uuden sijainnin lisäksi kaikki madon aiemmat sijainnit paitsi viimeinen.
Seuraavassa REPL-sessiossa käytetään metodeita take
, dropRight
ja init
. Voit poimia siitä haluamasi työkalun tehtävässä
sovellettavaksi.
val luvut = Vector(10, 20, 30, 40, 50, 60, 70)luvut: Vector[Int] = Vector(10, 20, 30, 40, 50, 60, 70) luvut.take(3)res18: Vector[Int] = Vector(10, 20, 30) luvut.dropRight(3)res19: Vector[Int] = Vector(10, 20, 30, 40) luvut.initres20: Vector[Int] = Vector(10, 20, 30, 40, 50, 60)
Vaihe 4/5: grafiikka kuntoon
Siirry SnakeApp
-ohjelmaan ja siellä makePic
-metodiin. Huomaa:
- Metodi lisää taustaa vasten vain yhden kappaleen
SegmentPic
-kuvaa (punertavaa madonosaa). makePic
käyttää samassa tiedostossa määriteltyä apufunktiotatoPixelPos
määrittääkseen ruudukkokoordinaattien perusteella pikselin, johonSegmentPic
piirtyy.makePic
in käyttämäPic
-luokan metodiplaceCopies
ottaa ensimmäiseksi parametrikseen kuvan ja toiseksi vektorillisen sijainteja (Pos
-olioita). Se toimii kuten tuttuplace
-metodi mutta sijoittaa taustaa vasten useita kopioita samasta kuvasta. Annettu koodi tosin antaa tuolle metodille parametriksi aina vain yksialkioisen vektorin.
Muokkaa makePic
-metodia niin, että se piirtää kaikki madon segmentit identtisinä
palluroina. Se käy näin:
Vinkki
GridPos
-sijainteja. Kokoelman, jossa on kutakin
niistä vastaava Pos
, saat helposti map
-metodilla.- Määritä kunkin segmentin kuvan sijainti
toPixelPos
-metodilla. Et tarvitse silmukkaa. Käytä apuna tämän luvun esittelemää kokoelmametodia. - Anna
placeCopies
-metodille vektori, jossa ovat kaikki segmenttien sijainnitPos
-olioina.
Mato kasvaa!
Vaihe 5/5: törmäykset
Vinkki
Matopelien perinteeseen kuuluu, että mato voi törmätä paitsi seiniin myös itseensä.
Huomioi tämä SnakeGame
-luokan isOver
-metodissa: pelin pitää päättyä myös, jos
madon pää on päätynyt samaan ruutuun jonkin muun segmentin kanssa.
Luvusta 4.2 löytyy taas työkaluja.
Erikoistapaus
Mitä pitäisi tapahtua, kun täsmälleen kaksisegmenttinen mato kääntyy 180 astetta? Ehtiikö häntä pois pään alta vai ei? Saat itse päättää; tehtävän automaattitesti ei huomioi tätä tapausta.
Palauttaminen
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Merkintätavoista Scalassa
Kun luet Scala-ohjelmia tämän peruskurssin ulkopuolella, törmäät pian ohjelmiin, joissa tämän luvun esittelemiä metodeita käytetään erinäköisesti kuin nyt olemme tehneet. Esimerkiksi tämä käsky:
luvut.filter( _ > 0 ).map( n => n * n ).count( _ % 2 == 0 )
voitaisiinkin kirjoittaa tähän tapaan:
luvut filter { _ > 0 } map { n => n * n } count { _ % 2 == 0 }
Scala-kielen syntaksi on joustava, ja tämä toinen kirjoitustapa perustuu kahteen kielen tarjoamaan mahdollisuuteen:
- Metodeita voi kutsua pistenotaation sijaan operaattorinotaatiolla. Osalla Scala-ohjelmoijista on tapana tehdä näin, kun kyseessä on vaikutukseton yksiparametrinen korkeamman asteen metodi.
- Jos metodi on yksiparametrinen, voi kaarisulkujen sijaan käyttää aaltosulkuja. Osalla Scala-ohjelmoijista on tapana tehdä näin, kun kyseessä on funktioparametri.
Kannattaa siis vähintään varautua henkisesti lukemaan joskus tällaistakin Scala-koodia.
Kuten luvussa 5.2 mainittiin, tämän kurssin materiaalissa pääsääntöisesti ei käytetä operaattorinotaatiota.
Yhteenvetoa
- Kokoelmille on Scala-kielen peruskirjastoissa määritelty koko joukko
korkeamman asteen metodeita, joilla kokoelmien sisältöä voi käsitellä.
- Yleisesti käytettyjä korkeamman asteen
metodeita ovat mm.
foreach
,exists
,forall
,find
,filter
,map
jaflatMap
. Lisää esitellään myöhemmin. - Nämä metodit tarjoavat käteviä ratkaisuvaihtoehtoja moniin sellaisiin tilanteisiin, joissa silmukan käyttö olisi toinen vaihtoehto.
- Tällainen kokoelmien käsittely on erityisen yleistä ns. funktionaalisessa ohjelmoinnissa, jota Scala voimakkaasti tukee.
- Yleisesti käytettyjä korkeamman asteen
metodeita ovat mm.
- Lukuun liittyviä termejä sanastosivulla: korkeamman asteen funktio; kokoelma; abstraktiotaso.
Tämän luvun merkityksestä
Useat tämän luvun esittelemistä metodeista ovat hyvin runsaassa käytössä tulevissa luvuissa. Niiden tunteminen helpottaa monien tulevien harjoitustehtävien tekemistä. Lisäksi sinun on kyettävä lukemaan annettua ohjelmakoodia, jossa näitä metodeita käytetään.
Kaikkien uusien metodien muistaminen ei varmasti heti onnistu, mutta ei tarvitsekaan. Voit palata kertaamaan esimerkiksi metodien nimiä ja muita yksityiskohtia tästä luvusta tai Scalaa kootusti -sivulta.
Tärkeintä on nyt muistaa perusajatus: kokoelmia voi käsitellä korkeamman asteen metodeilla, ja tarjolla on laaja valikoima erilaisia näppäriä metodeita.
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.
foreach
-metodille annetaan parametriksi funktio, jolle kelpaa parametriksi kokoelman alkioiden tyyppinen arvo (tässäInt
). Esimerkiksi tässä määritelty funktioliteraali siis on sopiva.