Kurssin viimeisimmän version löydät täältä: O1: 2024
Luku 9.2: Vertaillaan, järjestellään ja ryhmitellään
Tästä sivusta:
Pääkysymyksiä: Miten saan pyöriteltyä dataa erilaisiin muotoihin sitä analysoidakseni? Miten järjestän alkiokokoelman jollakin kriteerillä?
Mitä käsitellään? Vertaileminen ja järjestäminen; Scala API:n valmiita metodeita näihin tarkoituksiin. Hakurakenteita, taas, erityisesti niiden muodostamista toisessa muodossa olevasta datasta.
Mitä tehdään? Luetaan ja ohjelmoidaan.
Suuntaa antava työläysarvio:? Viisi tuntia.
Pistearvo: B75 + C45.
Oheismoduulit: Tehtävissä on jatko-osia useaan aiempaan ohjelmaan: Stars, CitySim, Election. Esimerkissä myös: MovieStats (uusi).
Johdanto
Olet nähnyt useita esimerkkejä yleisestä ohjelmoijan tarpeesta: on etsittävä luvuista, merkkijonoista tai muista olioista se, joka on suurin, pienin, paras, huonoin tai muuten sopivin jonkin kriteerin perusteella.
Toinen yleinen tarve on arvojen järjestäminen jonkin kriteerin perusteella, esimerkiksi suurimmasta pienimpään tai toisinpäin. Jos mietit vaikkapa yleisiä käyttämiäsi sovellusohjelmia ja web-palveluita, keksit varmasti heti itsekin tilanteita, joissa järjestämisestä on hyötyä.
Kuten jo tiedät:
Kurssin tarkoituksena ei ole kahlata läpi koko Scala API:a vaan poimia sieltä ohjelmoinnin perusteiden kannalta opettavaisia osia. Tässäkin luvussa käsitellään vain osa Scala API:n runsaasta valikoimasta vertailemiseen ja järjestämiseen sopivia työkaluja.
Sekä sopivimman arvon etsimiselle että järjestämiselle on ominaista, että arvoja on vertailtava jollakin kriteerillä. Siksi aloitamme tämän luvun pohtimalla erilaisia vertailutapoja.
Käymme jälleen Scala API:n työkalupakilla.
Arvojen vertaileminen "luonnollisesti"
Aloitetaan tutkimalla konkreettista koodia.
Kokoelmien max
- ja min
-metodit
Kokoelmilla on max
-niminen metodi, joka etsii ja palauttaa suurimman alkion. Kun
kyseessä ovat lukualkiot, suurin alkio määräytyy luonnolliseen tapaan:
Vector(3, 5, -50, 20, 1, 5).maxres0: Int = 20
Joissakin aiemmissa luvuissa olemme vertailleet merkkijonoja Unicode-aakkoston mukaan.
Samaan nojautuu myös max
, joka palauttaa aakkosissa viimeisen merkkijonon kokoelmasta:
Vector("a", "bb", "b", "abc", "ba").maxres1: String = bb
min
-metodi toimii aivan vastaavasti. Sillä voi selvittää esimerkiksi vektorin pienimmän
luvun tai aakkosissa ensimmäisen merkkijonon.
Luonnollinen järjestys
Luvuilla ja merkkijonoilla on ns. luonnollinen järjestys, johon min
- ja max
-metodit
perustuvat. Asiaa voi ajatella vaikka näin: jos arvo ei ole isompi tai pienempi kuin
toinen, niin onko kyseessä sama arvo kuin toinen? Tämähän pätee esimerkiksi luvuille
niiden suuruuden mukaan vertaillessa: jos luku ei ole isompi tai pienempi, niin kyseessä
on sama lukuarvo. Sama pätee merkkijonoille aakkosjärjestyksen mukaan vertaillessa.
Luonnollisella järjestyksellä siis viitataan siihen, että arvojen järjestys on kahlittu
siihen, mistä nimenomaisesta arvosta on kysymys.
Esimerkki ei-luonnollisesta järjestyksestä on merkkijonojen järjestäminen pituuden mukaan: kaksi samanmittaista merkkijonoa eivät suinkaan välttämättä ole sama merkkijono. Toinen esimerkki on GoodStuff-ohjelmasta tuttu kokemusolioiden vertailu arvosanan mukaan. Vaikka kokemuksilla on sama arvosana, eivät ne välttämättä ole sama kokemus.
Ordered
-piirreluokka
Luonnollinenkin järjestys täytyy tietysti määritellä tietokoneelle. Scalassa tämä on
tehty määrittelemällä Ordered
-piirreluokka. Se edustaa keskenään luonnollisella
tavalla vertailukelpoisia olioita. Moniin Scalan valmiisiin tietotyyppeihin on liitetty
Ordered
-piirre. Näihin kuuluvat esimerkiksi Int
, Double
, merkkijonot ja totuusarvot.
(Ordered
-piirreluokan voi liittää myös itse määrittelemääsi luokkaan, mutta sitä emme
tee tämän kurssin puitteissa.)
Ordered
-piirreluokassa on abstrakti metodi, joka määrittelee sen perusteen, jolla
arvoja vertaillaan. Konkreettiset tietotyypit toteuttavat tämän metodin kukin omalla
tavallaan. Esimerkiksi kokonaisluvuille se on toteutettu lukujen suuremmuusvertailuna
ja merkkijonoille aakkosvertailuna.
Tärkein hyöty Ordered
-piirreluokasta on, että se toimii yläkäsitteenä kaikille
sellaisille tietotyypeille, joilla on luonnollinen järjestys. Niinpä voidaan
määritellä metodeita, jotka käsittelevät Ordered
-tyyppisiä arvoja ja siis toimivat
kaikenlaisille olioille, joita voi vertailla luonnollisesti keskenään: kokonaisluvuille,
desimaaliluvuille, merkkijonoille jne.
Tätä potentiaalia onkin hyödynnetty Scalan ohjelmakirjastoissa. Alkiokokoelmia kuvaavilla
luokilla on metodeita, jotka toimivat Ordered
-tyyppisille alkioille. Yllä esitellyt
max
ja min
toimivat kuvatulla tavalla vain silloin, kun kokoelman alkiot ovat
luonnollisesti järjestettävissä. Esimerkiksi Pos
-oliot eivät ole:
Vector(Pos(10, 5), Pos(7, 12)).max<console>:9: error: No implicit Ordering defined for o1.Pos.
Alkioiden luonnollista järjestystä hyödynnämme myös seuraavaksi, kun kokeilemme kokoelman järjestämistä.
Kokoelman järjestäminen: sorted
sorted
-metodi järjestää alkiot — joiden tulee olla keskenään vertailukelpoisia —
kasvavaan järjestykseen:
Vector(3, 5, -50, 20, 1, 5).sortedres2: Vector[Int] = Vector(-50, 1, 3, 5, 5, 20) Vector("a", "bb", "b", "abc", "ba").sortedres3: Vector[String] = Vector(a, abc, b, ba, bb)
sorted
-metodi palauttaa uuden kokoelman, jossa samat alkiot ovat eri järjestyksessä.
Se ei muuta alkuperäistä kokoelmaa. Vektoriahan ei edes voisi muuttaa.
Jos haluamme päinvastaisen tuloksen, voimme esimerkiksi käyttää reverse
-metodia
(luvusta 4.2):
Vector(3, 5, -50, 20, 1, 5).sorted.reverseres4: Vector[Int] = Vector(20, 5, 5, 3, 1, -50)
Miten järjestäminen toimii?
Järjestämisalgoritmit eli lajittelualgoritmit (sorting algoritms) ovat klassinen algoritmitutkimuksen ala. On kehitetty paljon tehokkuusominaisuuksiltaan toisistaan poikkeavia järjestämisalgoritmeja.
Myös Scala API tarjoaa valikoiman erilaisia toteutuksia järjestämiselle. Toteutuksiin ei kuitenkaan tutustuta tällä kurssilla. Nyt riittää, että käytät tiettyjä valmiiksi määriteltyjä järjestämismetodeita.
Jos haluat, voit miettiä, miten toteuttaisit itse funktion, joka järjestää vaikkapa parametriksi annetun kokonaislukuja sisältävän puskurin.
Internetistä löytyy yllin kyllin lisätietoa järjestämisestä. Aihetta käsitellään myös Aallon jatkokursseilla.
Erikoistapaus: Double
jen vertailu
Äskeisissä max
-, min
- ja sorted
-esimerkeissä vertailtiin kokonaislukuja ja
merkkijonoja. Saman voi tehdä Double
-arvoillekin, mutta Scala vaatii tällöin yhden
lisätoimenpiteen.
Vector(1.1, 3.0, 0.0, 2.2).sorted!warning: ... There are multiple ways to order Doubles (Ordering.Double.TotalOrdering, Ordering.Double.IeeeOrdering). Specify one by using a local import, assigning an implicit val, or passing it explicitly. See their documentation for details.
Kuten varoitusviesti mainitsee, Double
ille ei ole yhtä ensisijaista vertailutapaa.
Tarjolla on kuitenkin pari valmista vaihtoehtoa, joista valita: TotalOrdering
ja
IeeeOrdering
, joista voi helposti import
ata jommankumman:
TotalOrdering
vs. IeeeOrdering
TotalOrdering
ja IeeeOrdering
eroavat toisistaan vain siinä, miten
niissä käsitellään erikoisarvo NaN
eli “not a number”, jollainen syntyy kun esimerkiksi jaetaan
Double
-arvo 0.0 itsellään. Ne on kuvattu API-dokumentaatiossa.
Useimmissa ohjelmissa — ja kaikissa O1:n ohjelmissa — ei ole väliä, kumman näistä kahdesta valitset.
import scala.Ordering.Double.TotalOrderingimport scala.Ordering.Double.TotalOrdering Vector(1.1, 3.0, 0.0, 2.2).sortedres5: Vector[Double] = Vector(0.0, 1.1, 2.2, 3.0) Vector(1.1, 3.0, 0.0, 2.2).maxres6: Double = 3.0
Myöhemmissä tämän luvun esimerkeissä oletetaan, että tuollainen import
-käsky on
voimassa.
Vertaileminen mielivaltaisella kriteerillä
Metodit max
, min
ja sorted
toimivat somasti, kun alkioilla on luonnollinen järjestys.
Vaan entä jos alkiotyypillä ei olekaan piirrettä Ordered
(esim. kun halutaan etsiä
Shape
-tyyppisistä kuvio-olioista pinta-alan mukaan suurin). Tai vaikka olisikin,
saatamme haluta käyttää jotakin muuta kuin arvojen luonnollista järjestystä (esim. kun
halutaan merkkijonoista lyhin eikä aakkosissa ensimmäinen).
Metodit maxBy
, minBy
ja sortBy
vastaavat usein tarpeeseen näppärimmin.
Seuraavissa esimerkeissä käytetään alkioina merkkijonojen ja kokonaislukujen lisäksi kuvio-olioita (luvusta 7.2). Tässä pikakertaus kuvioluokkien oleellisista osista:
trait Shape {
def area: Double
// ...
}
class Circle(val radius: Double) extends Shape {
def area = math.Pi * this.radius * this.radius
// ...
}
class Rectangle(val sideLength: Double, val anotherSideLength: Double) extends Shape {
def area = this.sideLength * this.anotherSideLength
// ...
}
(Lisäksi näihin luokkiin on laadittu toString
-metodit, jotta tulosteet REPLissä
ovat selkeämmät.)
maxBy
- ja minBy
-metodit
Tässä etsitään pisin merkkijono ja neliöltään suurin luku:
Vector("a", "bb", "b", "abc", "ba").maxBy( _.length )res7: String = abc Vector(3, 5, -50, 20, 1, 5).maxBy( math.pow(_, 2) )res8: Int = -50
Ajatuksena on siis, että maxBy
-metodille annetaan parametriksi funktio, jota kutsutaan
kullekin alkiolle. Funktion palautusarvojen täytyy olla keskenään vertailukelpoisia, kuten
esimerkiksi jonojen pituudet ja lukujen toiset potenssit ovat. Metodi määrittää suurimman
arvon näiden palautusarvojen eikä alkuperäisten alkioiden perusteella.
maxBy
-toimii luonnollisesta järjestyksestä riippumattomasti ja siis myös vaikkei alkiolla
luonnollista järjestystä olisikaan. Shape
-olioihin ei ole liitetty Ordered
-piirrettä
laisinkaan, mutta pinta-alaltaan suurimman kuvion etsiminen sujuu helposti:
val kuvioita = Vector(new Circle(5), new Rectangle(5, 11), new Rectangle(30, 5))kuvioita: Vector[Shape] = Vector(CIRC[ r=5.0 a=78.53981633974483 ], RECT[ 5.0*11.0 a=55.0 ], RECT[ 30.0*5.0 a=150.0 ]) kuvioita.maxBy( _.area )res9: Shape = RECT[ 30.0*5.0 a=150.0 ]
minBy
toimii vastaavasti.
Entä jos alkioita ei ole? (maxByOption
jne.)
Tässä yksi tapa toteuttaa GoodStuff-ohjelman Category
-luokan
suosikkiuskirjanpito, jota on versioitu pitkin kurssia:
def favorite = if (this.experiences.isEmpty) None else Some(this.experiences.maxBy( _.rating ))
Tuo on esimerkki monissa ohjelmissa toistuvasta tarpeesta: halutaan
isoin alkio tai None
, jos alkioita ei ole. Niinpä siihen on
räätälöity metodi, maxByOption
. Tämä tekee ihan saman kuin äskeinen
koodi:
def favorite = this.experiences.maxByOption( _.rating )
Vastaavasti on olemassa metodi minByOption
sekä luonnolliseen
järjestykseen perustuvat maxOption
ja minOption
.
sortBy
-metodi
sortBy
-metodi on samansuuntainen. Esimerkiksi merkkijonot voi järjestää pituuden
perusteella, luvut toisen potenssin perusteella ja kuviot pinta-alan perusteella:
Vector("a", "bb", "b", "abc", "ba").sortBy( _.length )res10: Vector[String] = Vector(a, b, bb, ba, abc) Vector(3, 5, -50, 20, 1, 5).sortBy( math.pow(_, 2) )res11: Vector[Int] = Vector(1, 3, 5, 5, 20, -50) Vector(new Circle(5), new Rectangle(5, 11), new Rectangle(30, 5)).sortBy( _.area )res12: Vector[Double] = Vector(RECT[ 5.0*11.0 a=55.0 ], CIRC[ r=5.0 a=78.53981633974483 ], RECT[ 30.0*5.0 a=150.0 ])
sortBy
-metodilla voi tuottaa myös käänteisen normaalijärjestyksen (vrt. reverse
edellä). Tässä ensin pidempi ja sitten lyhyempi versio:
Vector(3, 5, -50, 20, 1, 5).sortBy( n => -n )res13: Vector[Int] = Vector(20, 5, 5, 3, 1, -50) Vector(3, 5, -50, 20, 1, 5).sortBy( -_ )res14: Vector[Int] = Vector(20, 5, 5, 3, 1, -50)
Käänteinen tulos saatiin järjestämällä luvut niiden itsensä sijaan niiden vastalukujen perusteella.
Tähtikarttatehtävä, osa 4/4: tähtikuvioita (B30)
Luvussa 4.4 tutustuit Stars-ohjelmaan. Luvussa 5.2 saimme taivaan tähdet näkyviin. Täydennetään nyt ohjelmaa piirtämällä taivaalle myös tähtikuvioita.
Tehtävänanto
Tutustu luokan Constellation
dokumentaatioon ja puutteelliseen koodiin.
Huomaa erityisesti:
- Koodista puuttuvat
stars
,minCoords
jamaxCoords
. - Tähtikuvio rakennetaan vektorillisesta pareja.
- Muuttujan
stars
tyyppi onSet[Star]
eli joukko tähtiä.- Joukko on alkiokokoelma. Toisin kuin esimerkiksi vektorissa tai puskurissa, joukossa ei voi koskaan olla useita kopioita samasta arvosta.
- Joukon alkioilla ei myöskään ole järjestysnumeroita (indeksejä).
- Joukon voi muodostaa
toSet
-metodikutsulla (esim.vektori.toSet
). Jos alkuperäisessä kokoelmassa oli sama arvo useasti, syntyvässä joukossa se on kuitenkin vain kerran.
Täydennä ohjelma:
- Toteuta luokka
Constellation
dokumentaation mukaiseksi. - Toteuta myös
SkyPic
-oliolta puuttuva metodiplaceConstellation
. - Muokkaa
SkyPic
-olioncreate
-metodia niin, että se laittaa palauttamaansa kuvaan (tähtien päälle) myösStarMap
-parametrinsa tähtikuviot.
Suositellut työvaiheet ja vinkkejä
Toteuta muuttuja
stars
luokkaanConstellation
. Mahdollisia työvälineitä:Toteuta samaan luokkaan
minCoords
jamaxCoords
. Käytä tämän luvun esittelemiä metodeita.Aja ohjelma. Tähtikuviot eivät vielä piirry, mutta eräiden tähtikuvioiden nimien pitäisi tulla tekstinä näkyviin, kun siirtelet hiiren kursoria kuvan päällä.
- Annettu käyttöliittymä lisää nuo tekstit,
jos kursori on tähtikuvion
minCoords
in jamaxCoords
in välisellä alueella. - Ohjelma tuntee muutaman tähtikuvion, jotka
on määritetty
northern
-kansiossa. (MuuttujanstarDataFolder
arvo pitää ollaStarryApp
issä kunnossa, jotta tähdet tulevat näkyviin ja kuviot tunnistetaan. )
- Annettu käyttöliittymä lisää nuo tekstit,
jos kursori on tähtikuvion
Täydennä
SkyPic.placeConstellation
.Voit kutsua
Pic
-luokanplace
-metodia aiempaankin tyyliin. Toisaalta tuosta metodista on myös versio, jolle voi antaa parametreiksi pareja, ja jota ehkä haluat kokeilla. Tähän tapaan:taustakuva.place(kuva1 -> sijainti1, kuva2 -> sijainti2, kuva3 -> sijainti3)
Viivan kuvan saa aikaan helposti
line
-metodilla. Tuo metodi on aiemmin mainittu vain luvun 3.1 valinnaisessa materiaalissa, mutta sen käyttö on helppoa. Tässä pikkuesimerkki:val viiva = line(new Pos(0, 0), new Pos(150, 100), Red)viiva: Pic = line-shape
foldLeft
-metodi (luku 6.4) on tässä kätevä: tarkoitushan on koostaa kuva useasta kuvasta.for
-silmukallakin pärjää.
Täydennä
SkyPic.create
lisäämään myös tähtikuviot.Kuvioiden pitäisi nyt piirtyä näkyviin, kun ajat ohjelman.
- Jos eivät näy, varmista vielä, että sinulla
on
:file:`StarryApp.scala
ssa valittuna kartaksi"northern"
eikä"test"
.
- Jos eivät näy, varmista vielä, että sinulla
on
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Hakurakenteen luominen olemassa olevasta datasta: toMap
Usein haluamme luoda hakurakenteen olemassa olevista olioista niin, että tietty noiden olioiden ominaisuus toimii avaimena.
Esimerkin johdanto
Olkoon käytössä sama luokka Member
, jota aiemmissa luvuissakin olemme käyttäneet:
class Member(val id: Int, val name: String, val yearOfBirth: Int, val yearOfDeath: Option[Int]) {
override def toString = this.name + "(" + this.yearOfBirth + "-" + this.yearOfDeath.getOrElse("") + ")"
}
Sanotaan vielä, että meillä on käytössä luettelo jäsentietoja vektorissa. Tässä esimerkissä tiedot syötetään REPLissä, mutta ne olisi voitu lukea vaikkapa jostakin tallennetusta tiedostosta:
val jasenluettelo = Vector(new Member(123, "Madonna", 1958, None), new Member(321, "Elvis", 1935, Some(1977)), new Member(555, "Michelangelo", 1475, Some(1564)))jasenluettelo: Vector[Member] = Vector(Madonna(1958-), Elvis(1935-1977), Michelangelo(1475-1564))
Miten saisimme tämän vektorin perusteella helposti luotua hakurakenteen, josta jäseniä voi poimia jäsennumeron perusteella ja jossa olisi vektorin sisältö?
Pohjustusvaihe: pareja vektoriin
Ensinnäkin on pystyttävä muodostamaan avain–arvo-pareja, joissa avaimina on jäsennumerot ja arvoina jäsenoliot.
jasenluettelo.map( jasen => jasen.id -> jasen )res15: Vector[(Int, Member)] = Vector((123,Madonna(1958-)), (321,Elvis(1935-1977)), (555,Michelangelo(1475-1564)))
map
annetaan nimetön funktio, joka ottaa
parametrikseen jäsenolion, ja...Nuolella ->
merkitty pari on siis tuossa osa nuolella =>
merkittyä funktiota.
Parit hakurakenteeksi toMap
-metodilla
Äsken syntyi siis vektorillinen avain–arvo-pareja. Näistä pareista voi muodostaa
Map
-olion vaivattomasti toMap
-metodilla. Lisätään vain toMap
-kutsu edellisen
lausekkeen perään:
val jasenhakemisto = jasenluettelo.map( jasen => jasen.id -> jasen ).toMapjasenhakemisto: Map[Int,Member] = Map(123 -> Madonna(1958-), 321 -> Elvis(1935-1977), 555 -> Michelangelo(1475-1564)) jasenhakemisto.get(321)res16: Option[Member] = Some(Elvis(1935-1977))
toMap
-metodi toimii mielekkäästi, kunhan kokoelma sisältää avain–arvo-pareja ja kukin
avain on erilainen kuin toiset (kuten jäsennumerot yllä ovat).
toMap
-metodin synnyttämä uusi hakurakenne on tilaltaan muuttumaton.
zip
plus toMap
zip
-metodi (luku 8.4) on usein kätevä hakurakenteita muodostaessa. Tässä pieni esimerkki,
jossa hakurakenne muodostetaan kahdesta erillisestä olemassa olevasta vektorista, joista
toisessa on avaimet (lajien nimet) ja toisessa niitä vastaavat arvot (korkeudet sentteinä):
val elaimet = Vector("laama", "alpakka", "vikunja")elaimet: Vector[String] = Vector(laama, alpakka, vikunja) val korkeudet = Vector(180, 80, 60)korkeudet: Vector[Int] = Vector(180, 80, 60)
Yhdistetään kokoelmat zip
-metodilla pareja sisältäväksi vektoriksi, niin voimme kutsua
toMap
-metodia:
elaimet.zip(korkeudet).toMapres17: Map[String,Int] = Map(laama -> 180, alpakka -> 80, vikunja -> 60)
Myös jäsenesimerkissä olisimme voineet käyttää zip
-metodia ja korvata tämän:
jasenluettelo.map( jasen => jasen.id -> jasen )
tällä saman asian hoitavalla koodilla:
jasenluettelo.map( _.id ).zip(jasenluettelo)
Hakurakenteen luominen olemassa olevasta datasta: groupBy
Nähdyssä esimerkissä käytimme avaimena jäsennumeroa, joka on kullakin jäsenellä eri luku. Voimme myös käyttää avaimena sellaista ominaisuutta, joka ei ole oliolle ainutlaatuinen. Toisin sanoen: voimme ryhmitellä olioita tietyn kriteerin perusteella.
Tällaiseen tarpeeseen sopii metodi groupBy
. Samassa hengessä kuin metodeille sortBy
,
maxBy
ja minBy
, myös tälle metodille välitetään parametriksi funktio, jota se käyttää
arvojen keskinäiseen vertailemiseen. Ryhmitellään jäsenemme vaikkapa syntymävuosisadan
perusteella:
jasenluettelo.groupBy( _.yearOfBirth / 100 )res18: Map[Int,Vector[Member]] = Map(14 -> Vector(Michelangelo(1475-1564)), 19 -> Vector(Madonna(1958-), Elvis(1935-1977)))
groupBy
-kutsulla, jolle
on annettu parametriksi jäsenen syntymävuosisadan selvittävä
pikkufunktio. groupBy
-metodi kutsuu tätä funktiota jokaiselle
jäsenelle jäsenluettelossa. Se luo ja palauttaa hakurakenteen,
jossa...partition
ja groupBy
Luku 8.4 esitteli partition
-metodin, jolla saatoimme esimerkiksi
ryhmitellä lämpötilat negatiivisiin ja ei-negatiivisiin. Tuo
metodi sopii tilanteisiin, joissa jaetaan tasan kahteen ryhmään
Boolean
-arvon palauttavan funktion perusteella. Kun tilanne ei
ole niin yksinkertainen, on groupBy
tarpeen.
Myös groupBy
-metodin luoma uusi hakurakenne on tilaltaan muuttumaton.
Tehtävä: monenlaisia kaupunkilaisia simulaattorissa (C5)
Luvussa 7.5 toteutit kaupunkisimulaattorin, jossa oli kaksi kansanosaa, sininen ja punainen (esimerkkiratkaisu). Muokataan simulaattoria niin, että kansanosia voi olla useampia.
Tehtävänanto
Tämä muutos ei vaadi sinulta paljon: täydennä vain Simulator
-luokkaan metodi
residents
. Se käy helposti, kun käytät groupBy
-metodia ja CityMap
-luokkaa.
Annettu käyttöliittymä huomioi tekemäsi muutoksen automaattisesti ja osaa piirtää erivärisiä ruutuja kartalle, kun ajat ohjelman.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Esimerkki: ohjaajatilastot
Johdanto
Yksi melko tyypillinen käyttötarkoitus hakurakenteille on esiintymien laskeminen.
Palataan suosittujen ohjaajien teemaan ja selvitetään hakurakenteen avulla, kuinka monta elokuvaa kullakin ohjaajalla on parhaimpien elokuvien luettelossa. Pistetään vielä ohjaajat tämän kriteerin mukaan järjestykseen. (Luvussa 1.6 käytit valmiina annettua funktiota samantapaiseen tarkoitukseen.)
Seuraava ohjelmakoodi löytyy myös kurssin oheismoduulista MovieStats.
Olkoon käytössä luokka Movie
, joka kuvaa yhtä merkintää parhaiden elokuvien luettelossa.
Se on yksinkertainen:
class Movie(val name: String, val year: Int, val position: Int,
val rating: Double, val directors: Vector[String]) {
override def toString = this.name
}
Lisäksi käytämme luokkaa MovieListFile
, joka osaa lukea ja jäsentää tietyssä muodossa
olevia tekstitiedostoja. Siitä ei tarvitse nyt tietää muuta kuin sen käyttötapa:
val movieFile = new MovieListFile("omdb_movies_2015.txt")movieFile: MovieListFile = omdb_movies_2015.txt (contains: 250 movies) val allMovies = movieFile.moviesallMovies: Vector[o1.movies.Movie] = Vector(Das Boot, Amadeus, Heat, The Secret in Their Eyes, ...
Esiintymien laskeminen hakurakenteella
Miten saataisiin kunkin ohjaajan yleisyys luettelossa?
Ennen kuin ohjaajien esiintymät lasketaan, on järkevää muodostaa luettelo kaikista ohjaajista.
Koska kullakin elokuvalla voi olla useita ohjaajia, ei pelkkä allMovies.map( _.directors )
tuota haluttua tulosta (vaan luettelon elokuvakohtaisista ohjaajaluetteloista). Käytetään
mieluummin flatMap
iä:
val allDirectors = allMovies.flatMap( _.directors )allDirectors: Vector[String] = Vector(Wolfgang Petersen, Milos Forman, Michael Mann, Juan José Campanella, ...
Otetaan tavoitteeksi muodostaa sellainen hakurakenne, jossa avaimina ovat ohjaajien nimet ja arvoina elokuvien lukumäärät. Yksi tapa päästä tähän tavoitteeseen on seuraava:
- Etsitään kunkin ohjaajan esiintymät vektorissa ryhmittelemällä vektorin sisältö ohjaajan nimen mukaan. Siis: laitetaan kaikki yhden ohjaajan esiintymät yhteen "nippuun".
- Lasketaan muodostuneiden "nippujen" koot ja laitetaan ne hakurakenteeseen, avaimena kyseisen ohjaajan nimi.
Ajatus selvinnee parhaiten REPListä:
val groupedByDirector = allDirectors.groupBy( dir => dir )groupedByDirector: Map[String,Vector[String]] = Map(Paul Thomas Anderson -> Vector(Paul Thomas Anderson), Peter Weir -> Vector(Peter Weir), Wim Wenders -> Vector(Wim Wenders), Wolfgang Petersen -> Vector(Wolfgang Petersen), Giuseppe Tornatore -> Vector(Giuseppe Tornatore), Robert Rossen -> Vector(Robert Rossen), Dean DeBlois -> Vector(Dean DeBlois), Charles Chaplin -> Vector(Charles Chaplin, Charles Chaplin, Charles Chaplin, Charles Chaplin, Charles Chaplin), Ridley Scott -> Vector(Ridley Scott, Ridley Scott, Ridley Scott), Sean Penn -> ...
groupBy
-kutsulla, joka ryhmittelee
alkuperäisessä vektorissa olleet ohjaajanimet niiden itsensä
mukaan (niin hassulta kuin se saattaa kuulostaakin). Lisää tästä
alla.Identiteettifunktio
Ainoan parametriarvonsa sellaisenaan palauttavaa funktiota kuten
dir => dir
sanotaan identiteettifunktioksi (identity
function). Identiteettifunktio on myös määritelty Scalaan
valmiiksi: äskeisessä koodissa funktioliteraalin dir => dir
olisi voinut korvata sanalla identity
. Oman maun mukaan.
Muista: groupBy
-metodi kutsuu parametrifunktiotaan ohjaajaluettelon jokaiselle
merkkijonolle ja luo uuden "ryhmän" jokaiselle erilaiselle arvolle, joka
parametrifunktiolta saadaan palautusarvona.
Käytetään siis ryhmittelyn perustana funktiota, joka palauttaa juuri sen merkkijonon, joka sille annetaan parametriksikin. Näin saadaan muodostettua ryhmä jokaiselle erilaiselle alkuperäisen kokoelman sisältämälle merkkijonolle.
Nyt olemme enää kukonaskelen päässä alkuperäisen ongelman ratkaisusta. groupBy
-metodilla
muodostetusta hakurakenteesta voi luoda ohjaajalaskurit helposti, kun käytämme vielä
map
-metodia (luku 8.4). Vaihdetaan vain kunkin nimen kohdalle tallennetun vektorin
tilalle kyseisen vektorin koko:
val countsByDirector = groupedByDirector.map( pari => pari._1 -> pari._2.size )countsByDirector: Map[String,Int] = Map(Paul Thomas Anderson -> 1, Peter Weir -> 1, Wim Wenders -> 1, Wolfgang Petersen -> 1, Giuseppe Tornatore -> 1, Robert Rossen -> 1, Dean DeBlois -> 1, Charles Chaplin -> 5, Ridley Scott -> 3, Sean Penn -> 1, Danny Boyle -> 1, Gore Verbinski -> 1, Joel Coen -> 3, John Sturges -> 1, ...
Siistimmän tulosteen saamme vaikkapa näin:
countsByDirector.foreach(println)(Paul Thomas Anderson,1) (Peter Weir,1) (Wim Wenders,1) (Wolfgang Petersen,1) (Giuseppe Tornatore,1) (Robert Rossen,1) (Dean DeBlois,1) (Charles Chaplin,5) (Ridley Scott,3) (Sean Penn,1) (Danny Boyle,1) ...
Ohjaajat järjestykseen
Tehdään vielä luettelo ohjaajista sen mukaisessa järjestyksessä, montako elokuvaa heillä on kyseisessä luettelossa, suurimmasta lukumäärästä alkaen. Toisin sanoen: muodostetaan luettelo hakurakenteen avain–arvo-pareista arvojen mukaisessa käänteisessä numerojärjestyksessä.
Tarvitsemme järjestämismetodia. Ensin on kuitenkin syytä ottaa käyttöön hakurakenteen sijaan kokoelma, jossa alkioilla on mielekäs järjestys. Hakurakenteessahan järjestys on toteutusriippuvainen, eikä rakennetta voi järjestää arvojen mukaan. Vektori taas kelpaa:
val directorCountPairs = countsByDirector.toVectordirectorCountPairs: Vector[(String, Int)] = Vector((Paul Thomas Anderson,1), (Peter Weir,1), (Wim Wenders,1), (Wolfgang Petersen,1), (Giuseppe Tornatore,1), (Robert Rossen,1), (Dean DeBlois,1), (Charles Chaplin,5), (Ridley Scott,3), (Sean Penn,1), (Danny Boyle,1), (Gore Verbinski,1), (Joel Coen,3), (John Sturges,1), ...
Nyt siis directorCountPairs
-muuttuja sisältää viittauksen vektoriin, jossa on pareja.
Kullakin parilla on jäsenet _1
ja _2
, kuten luku 8.4 näytti. Ennen kuin järjestämme
ohjaajat, tässä muistutukseksi poimitaan ensimmäisestä parista sekä ohjaaja että leffamäärä:
directorCountPairs(0)._1res19: String = Paul Thomas Anderson directorCountPairs(0)._2res20: Int = 1
Ja ei kun järjestämään:
val directorsSorted = directorCountPairs.sortBy( pair => -pair._2 )directorsSorted: Vector[(String, Int)] = Vector((Alfred Hitchcock,8), (Stanley Kubrick,8),
(Martin Scorsese,7), (Christopher Nolan,7), (Quentin Tarantino,6), (Steven Spielberg,6), ...
Tämänkin funktioliteraalin voi kirjoittaa lyhennysmerkinnällä, joten seuraavakin ratkaisu toimii. Se tosin näyttää tärähtäneeltä hymiöltä, kunnes opit muistamaan, mitä alaviivat eri yhteyksissä tarkoittavat:
val directorsSorted = directorCountPairs.sortBy( -_._2 )directorsSorted: Vector[(String, Int)] = Vector((Alfred Hitchcock,8), (Stanley Kubrick,8), (Martin Scorsese,7), (Christopher Nolan,7), (Quentin Tarantino,6), (Steven Spielberg,6), ...
Kaunistetaan tulostetta vähän. Alla on käytetty luvussa 8.4 esiteltyä tapaa käydä läpi
pareja sisältävä kokoelma for
-silmukalla sekä luvussa 1.4 mainittua mahdollisuutta
upottaa lausekkeiden arvoja merkkijonoihin.
for ((director, count) <- directorsSorted) { println(s"$director: $count movies") }Alfred Hitchcock: 8 movies Stanley Kubrick: 8 movies Martin Scorsese: 7 movies Christopher Nolan: 7 movies Quentin Tarantino: 6 movies Steven Spielberg: 6 movies ...
Tehtävä: Election-ohjelman parantelua (osa 1/2; B45)
Luvuissa 5.6, 6.3 ja 6.4 työstit Election-moduulia. Siitä jäi kuitenkin puuttumaan useita metodeita, jotka toteutetaan tässä ja seuraavassa tehtävässä.
Tehtävänanto
- Tutustu (taas) Election-ohjelman Scaladoc-dokumentaatioon.
Luokkaan
District
on määritelty useita metodeita, joita et toteuttanut aiemmissa tehtävissä. - Korvaa luvussa 5.6 tehty
topCandidate
-toteutus yksinkertaisemmalla. - Täydennä puuttuvat metodit
candidatesByParty
,topCandidatesByParty
javotesByParty
. Muut luokasta puuttuvat metodit toteutetaan vaiheessa 2 alla.
Ohjeita ja vinkkejä
- Jos et tehnyt aiempien lukujen tehtäviä, tee ne tai käytä pohjana
esimerkkiratkaisuja. Viimeistään nyt kannattaa tehdä myös luvussa
5.6 ehdotettu
countVotes
-apumetodi. - Tässä tehtävässä käytät tilaltaan muuttumattomia hakurakenteita,
jotka ovat oletusarvoisesti käytössä ilman lisämäärittelyjä.
Älä ota muuttuvatilaista
Map
-luokkaa käyttöön pakkauksestascala.collection.mutable
. - Uusien metodien toteutus sujuu luultavasti helpoimmin, kun
teet sen tässä järjestyksessä:
candidatesByParty
,topCandidatesByParty
ja viimeiseksivotesByParty
. Hyödynnä aiemmin laatimiasi metodeita mahdollisuuksien mukaan. - Hyötyä on paitsi tässä luvussa esitellystä metodista ja muistakin kokoelmien metodeista. Kun valitset sopivat työkalut, ei koodia tarvita kuin pieni määrä.
- Testaa ratkaisuasi annetulla
ElectionTest
-ohjelmalla. Löydät tähän tehtävään liittyvän osionElectionTest
in lopusta. Poista kommenttimerkit osion ympäriltä.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Tehtävä: Election-ohjelman parantelua (osa 2/2; C40)
Tehtävänanto
Toteuta vielä puuttuvat District
-luokan metodit:
rankingsWithinParties
rankingOfParties
distributionFigures
electedCandidates
Ohjeita ja vinkkejä
- Kannattanee toteuttaa metodit yllä luetellussa järjestyksessä. Hyödynnä aiemmin laatimiasi metodeita mahdollisuuksien mukaan.
- Inspiraatiota voit hakea tämän ja edellisten lukujen esimerkeistä, etenkin yllä olevasta ohjaajatilastoesimerkistä.
- Vaihda hakurakenne vektoriksi (
toVector
) ja vektori hakurakenteeksi (toMap
) tarvittaessa. - Tämä tehtävä on yksi tilaisuus kokeilla paikallisen funktion
(luku 6.4) määrittelemistä metodin sisään. Voit siis kirjoittaa
def
indef
in sisään.- Voisiko esimerkiksi yksittäisen ehdokkaan
vertailuluvun määrittelemisestä tehdä oman
apufunktionsa
distributionFigures
-metodin sisään?
- Voisiko esimerkiksi yksittäisen ehdokkaan
vertailuluvun määrittelemisestä tehdä oman
apufunktionsa
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Yhteenvetoa
- Kokoelman alkioiden vertaileminen ja järjestäminen ovat ohjelmissa yleisiä toimenpiteitä.
- Scala API tarjoaa monipuolisen metodivalikoiman "suurimpien" ja "pienimpien" etsimiseen sekä alkioiden järjestämiseen.
- Hakurakenteita voi muodostaa muiden kokoelmien sisältämästä datasta, niiden alkioita ryhmitellen. Hakurakennetta voi näin käyttää muun muassa esiintymien laskemiseen.
Palaute
Huomaathan, että tämä on henkilökohtainen osio! Vaikka olisit tehnyt lukuun liittyvät tehtävät parin kanssa, täytä palautelomake itse.
Tekijät
Tämän oppimateriaalin kehitystyössä on käytetty apuna tuhansilta opiskelijoilta kerättyä palautetta. Kiitos!
Materiaalin luvut tehtävineen ja viikkokoosteineen on laatinut Juha Sorva.
Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.
Tehtävien automaattisen arvioinnin ovat toteuttaneet: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä ja Aleksi Vartiainen.
Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.
Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista suunnittelivat Juha Sorva ja Teemu Sirkiä. Teemu Sirkiä ja Riku Autio toteuttivat ne apunaan Teemun aiemmin rakentamat työkalut Jsvee- ja Kelmu.
Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset laati Juha Sorva.
O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.
Tapa, jolla käytämme O1Libraryn työkaluja (kuten Pic
) yksinkertaiseen graafiseen
ohjelmointiin, on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi
oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.
Oppimisalusta A+ luotiin alun perin Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Nykyään tätä avoimen lähdekoodin projektia kehittää Tietotekniikan laitoksen opetusteknologiatiimi ja tarjoaa palveluna laitoksen IT-tuki. Pääkehittäjänä on tällä hetkellä Markku Riekkinen, jonka lisäksi A+:aa ovat kehittäneet kymmenet Aallon opiskelijat ja muut.
A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen ovat luoneet Nikolai Denissov, Olli Kiljunen ja Nikolas Drosdek yhteistyössä Juha Sorvan, Otto Seppälän, Arto Hellaksen ja muiden kanssa.
Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.
Lisäkiitokset tähän lukuun
Stars-ohjelma on mukaelma Karen Reidin suunnittelemasta ohjelmointiharjoituksesta. Se käyttää tähtidataa VizieR-palvelusta.
Thomas Schellingin laatimaan käyttäytymismalliin perustuva tehtävä on mukaelma Frank McCownin suunnittelemasta ohjelmointiharjoituksesta.