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.
Oheisprojektit: Tehtävissä jatko-osia useaan aiempaan projektiin: 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-projektista 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 lukutyypit kuten Int
ja Double
,
merkkijonot, totuusarvot, jne. (Ordered
-piirreluokka on mahdollista 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.
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 )res5: String = abc Vector(3, 5, -50, 20, 1, 5).maxBy( math.pow(_, 2) )res6: 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 )res7: Shape = RECT[ 30.0*5.0 a=150.0 ]
minBy
toimii vastaavasti.
Ja tässäpä vielä yksi nätti tapa toteuttaa GoodStuff-projektin Category
-luokan
suosikkiuskirjanpito, jota on versioitu pitkin kurssia:
def favorite = if (this.experiences.isEmpty) None else Some(this.experiences.maxBy( _.rating ))
Rivillä lukee Scalaksi suoraan käännettynä juuri se, miten ajattelemme asiaa: "jos kokemuksia ei ole, ei ole suosikkiakaan; muuten suosikki on tämän kategorian kokemuksista se, joka on suurin arvosanaltaan".
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 )res8: Vector[String] = Vector(a, b, bb, ba, abc) Vector(3, 5, -50, 20, 1, 5).sortBy( math.pow(_, 2) )res9: Vector[Int] = Vector(1, 3, 5, 5, 20, -50) Vector(new Circle(5), new Rectangle(5, 11), new Rectangle(30, 5)).sortBy( _.area )res10: 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 )res11: Vector[Int] = Vector(20, 5, 5, 3, 1, -50) Vector(3, 5, -50, 20, 1, 5).sortBy( -_ )res12: 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-projektiin. 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ä projekti:
- 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 metodit
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:import o1._import o1._ 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
Palauttaminen
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Hakurakenteen luominen olemassa olevasta datasta: toMap
Usein haluamme luoda hakurakenteen olemassa olevien olioiden perusteella siten, 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 )res13: Vector[(Int, Member)] = Vector((123,Madonna(1958-)), (321,Elvis(1935-1977)), (555,Michelangelo(1475-1564)))
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)res14: 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).toMapres15: 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 )res16: 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.
Palauttaminen
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 listalla. 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 oheisprojektista MovieStats.
Olkoon käytössä luokka Movie
, joka kuvaa yhtä merkintää parhaiden elokuvien listalla.
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 listalla?
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 jo enää kukonaskelen päässä alkuperäisen ongelman ratkaisusta. groupBy
-metodilla
muodostetun hakurakenteen perusteella voimme luoda ohjaajalaskurit helposti, kun käytämme
vielä mapValues
-metodia (luku 8.4). Vaihdetaan vain kunkin nimen kohdalle tallennetun
vektorin tilalle kyseisen vektorin koko:
val countsByDirector = groupedByDirector.mapValues( _.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 kyseisellä listalla, 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)._1res17: String = Paul Thomas Anderson directorCountPairs(0)._2res18: 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 5.2 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-projektin parantelua (osa 1/2; B45)
Luvuissa 5.6, 6.3 ja 6.4 työstit Election-projektia. Siitä jäi kuitenkin puuttumaan useita metodeita, jotka toteutetaan tässä ja seuraavassa tehtävässä.
Tehtävänanto
- Tutustu (taas) Election-projektin 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ä.
Palauttaminen
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Tehtävä: Election-projektin 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ä, erityisesti 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
Palauttaminen
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!
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.
Lisäkiitokset tähän lukuun
Stars-projekti 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.
map
annetaan nimetön funktio, joka ottaa parametrikseen jäsenolion, ja...