Kurssin viimeisimmän version löydät täältä: O1: 2024
Luku 8.4: Pareja ja hakuja
Tästä sivusta:
Pääkysymyksiä: Voiko funktio palauttaa kaksi arvoa? Miten voin helposti poimia kokoelmasta olion, joka vastaa toista oliota (esim. suomen sanaa vastaava englanninnos)? Miten voin tehdä kokoelman, josta on kätevää hakea alkioita tietyn hakuavaimen perusteella (esim. opiskelijaolio opiskelijanumeron perusteella)?
Mitä käsitellään? Parit. Avain–arvo-parit ja niiden muodostamat hakurakenteet.
Mitä tehdään? Luetaan ja ohjelmoidaan pieniä tehtäviä.
Suuntaa antava työläysarvio:? Kaksi tuntia.
Pistearvo: B25.
Oheisprojektit: Miscellaneous.
Johdanto: kaksi palautusarvoa?
Usein kysytty kysymys
Voiko funktio palauttaa kaksi arvoa? Voitaisiinko vaikkapa määritellä funktio
toMinsAndSecs
, jolle annettaisiin sekuntimäärä ja joka palauttaisi kerralla
kaksi kokonaislukua: kokonaiset minuutit ja yli jääneet sekunnit? Jotenkin näin:
toMinsAndSecs(187)res0: (Int, Int) = (3,7)
Funktiota kutsuessa voisi sitten esimerkiksi sijoittaa palautetut arvot muuttujiin
nimeltä min
ja s
ja tehdä niillä jotakin.
Ratkaisuvaihtoehtoja
Varsinaisesti funktio ei voi palauttaa kuin yhden arvon, mutta tätä rajoitusta voi kiertää, joten tavallaan se sittenkin voi.
Yksi kiertotapa on palauttaa kaksialkioinen kokoelma, vaikkapa Vector[Int]
-tyyppinen.
Tämä on siedettävä ratkaisu, joskin palautetusta kokoelmasta joutuu sitten erikseen
noukkimaan halutut arvot, esimerkiksi indeksien perusteella näin:
val palautettu = toMinsAndSecs(187)palautettu: Vector[Int] = Vector(3,7) val min = palautettu(0)min: Int = 3 val s = palautettu(1)s: Int = 7
Tämä vaihtoehto on myös epäkäytännöllinen tilanteissa, joissa halutaan palauttaa kaksi keskenään erityyppistä arvoa.
Toinen kiertotapa on laatia oma luokka, joka kuvaa palautettavien tietojen yhdistelmää.
Voitaisiin esimerkiksi laatia luokka Time
, jolla olisi muuttujat täysiä minuutteja
ja ylijäämäsekunteja varten, ja hoitaa asia luomalla tällainen aikaolio. Tämä tapa on
parhaimmillaan silloin, jos palautettava arvopari edustaa jotakin luokkana luontevasti
mallintuvaa käsitettä.
Kolmas tapa hyödyntää pareja (pair). Tämä tapa on kätevä, ja sen oppimisesta on laajemminkin iloa. Tarkastellaan pareja ensin yleisesti ja palataan sitten minuutteihin ja sekunteihin.
Parit
Parien peruskäyttö Scalalla
Parin voi muodostaa käyttämällä sulkuja ja pilkkua, kuten tässä:
("reppu", "reissumies")res1: (String, String) = (reppu,reissumies)
Parista voi poimia jommankumman jäsenen näin:
var pari = ("reppu", "reissumies")pari: (String, String) = (reppu,reissumies) pari._1res2: String = reppu pari._2res3: String = reissumies
_1
. (Huom. ei nolla
vaan yksi.) Toinen on vastaavasti _2
.Merkittävä ero parin ja vaikkapa kaksialkioisen vektorin välillä on, että parin jäsenillä voi olla eri staattiset tyypit:
val nimiJaKengannumero = ("Juha", 43)nimiJaKengannumero: (String, Int) = (Juha,43)
Parin osat voi sijoittaa kahteen eri muuttujaan näin helposti, jälleen sulkuja käyttämällä:
val (nimi, kengannumero) = nimiJaKengannumeronimi: String = Juha kengannumero: Int = 43
Pikkutehtävä: minuuteiksi ja sekunneiksi
Toteuta luvun alussa haviteltu funktio toMinsAndSecs
sellaiseksi, että sitä
voi käyttää näin:
import o1.misc._import o1.misc._ toMinsAndSecs(187)res4: (Int, Int) = (3,7) val (min, s) = toMinsAndSecs(200)min: Int = 3 s: Int = 20
Kirjoita ratkaisu Miscellaneous-projektin kansion o1
tiedostoon misc.scala
.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Pari funktion parametrina
Luonnollisesti voimme myös määritellä funktion, joka ottaa parametriksi parin. Tämä funktio tutkii, onko pari "järjestyksessä" eli onko ensimmäinen jäsen korkeintaan toisen suuruinen:
def isInOrder(pairOfNumbers: (Int, Int)) = pairOfNumbers._1 <= pairOfNumbers._2
Käyttöesimerkki:
val kokeilu = (10, 20)kokeilu: (Int, Int) = (10,20) isInOrder(kokeilu)res5: Boolean = true
Kokoelman jaottelu pariksi
Pareja on käytetty myös useassa Scala API:n valmiissa metodissa, mistä on alla pari esimerkkiä.
Kokoelmien metodi splitAt
palauttaa parin, jonka jäseninä on kaksi kokoelmaa:
val lampotilat = Vector(3, 6, -1, -5, 3, -1, 2)lampotilat: Vector[Int] = Vector(3, 6, -1, -5, 3, -1, 2) lampotilat.splitAt(3)res6: (Vector[Int], Vector[Int]) = (Vector(3, 6, -1),Vector(-5, 3, -1, 2)) val (arki, viikonloppu) = lampotilat.splitAt(5)arki: Vector[Int] = Vector(3, 6, -1, -5, 3) viikonloppu: Vector[Int] = Vector(-1, 2)
splitAt
ille annetaan parametri, joka osoittaa sijainnin
kokoelmassa.Myös metodi partition
jaottelee alkuperäisen kokoelman alkiot kahteen kokoelmaan ja
tuottaa parin, jossa on kaksi kokoelmaa:
val (pakkaset, lauhat) = lampotilat.partition( _ < 0 )pakkaset: Vector[Int] = Vector(-1, -5, -1) lauhat: Vector[Int] = Vector(3, 6, 3, 2)
Kuten esimerkistä voi arvata, tämä metodi jakaa alkiot soveltamalla niihin parametriksi
annettua funktiota ja katsomalla, tuottaako se true
vai false
.
Pikkutehtävä: kokoelman jaottelu
Projektin Miscellaneous pakkauksesta o1.people
löytyy yksittäisolio YoungAndOldApp
.
Tämän ohjelman toivottu toiminta on kuvattu sen ohjelmakoodin kommentissa. Täydennä
ohjelma toiveiden mukaiseksi käyttäen äsken esiteltyä metodia.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Kokoelmat vetoketjuina
Äsken parissa oli jäseninä kokoelmia. Kokoelmassa voi myös olla alkioina pareja.
Eräs näppärä kokoelmien metodi on zipWithIndex
. Se tuottaa alkuperäisen kokoelman
perusteella sellaisen kokoelman, joka sisältää pareja, joissa kussakin on alkio ja sen
indeksi:
val lajit = Vector("laama", "alpakka", "vikunja")lajit: Vector[String] = Vector(laama, alpakka, vikunja)
lajit.zipWithIndexres7: Vector[(String, Int)] = Vector((laama,0), (alpakka,1), (vikunja,2))
Kun pareja sisältävän kokoelman voi käydä läpi for
-silmukalla, voi sulkumerkintää
jälleen hyödyntää. Tässä kaksi tapaa tuottaa sama tuloste:
for (pari <- lajit.zipWithIndex) { println(pari._1 + " on numero " + pari._2) }laama on numero 0 alpakka on numero 1 vikunja on numero 2 for ((alkio, indeksi) <- lajit.zipWithIndex) { println(alkio + " on numero " + indeksi) }laama on numero 0 alpakka on numero 1 vikunja on numero 2
Metodi zipWithIndex
on erikoistapaus yleisemmästä metodista zip
, joka vetää yhteen
kaksi kokoelmaa alkiopareittain:
val korkeudet = Vector(180, 80, 60)korkeudet: Vector[Int] = Vector(180, 80, 60) val korkeudetJaLajit = korkeudet.zip(lajit)korkeudetJaLajit: Vector[(Int, String)] = Vector((180,laama), (80,alpakka), (60,vikunja))
Jos kokoelmat ovat eri mittaisia, jätetään pidemmästä loppuosa käyttämättä:
val iso = Vector("laama", "alpakka", "vikunja", "guanako", "wooly")iso: Vector[String] = Vector(laama, alpakka, vikunja, guanako, wooly) val vainKolmeParia = korkeudet.zip(iso)vainKolmeParia: Vector[(Int, String)] = Vector((180,laama), (80,alpakka), (60,vikunja))
Vetoketjun voi myös avata:
korkeudetJaLajit.unzipres8: (Vector[Int], Vector[String]) = (Vector(180, 80, 60),Vector(laama, alpakka, vikunja))
Tässä siis syntyi pari, jonka jäsenet ovat vektoreita.
Pikkutehtävä: onko kokoelma järjestyksessä?
Laadi funktio isAscending
, jolle annetaan parametriksi vektorillinen kokonaislukuja ja
joka kertoo, ovatko luvut nousevassa järjestyksessä. Tässä esimerkkejä:
isAscending(Vector(-10, 1, 5, 10))res9: Boolean = true isAscending(Vector(-10, 1, 5, 5, 10))res10: Boolean = true isAscending(Vector(-10, 1, 5, 9, 5, 10))res11: Boolean = false isAscending(Vector(10, 5, 1, -10))res12: Boolean = false
Hyödynnä tässä ja aiemmissa luvuissa opittuja kokoelmametodeita ovelasti, niin metodin rungoksi riittää yksi lyhyt rivi.
Kirjoita koodi jälleen tiedostoon misc.scala
projektissa Miscellaneous.
Voit olettaa, että vektorissa on ainakin yksi alkio.
Tarkempi vinkki hyödyllisistä metodeista
Metodit zip
, tail
ja forall
ovat käyttökelpoisia. Voit myös
hyödyntää aiemmin tässä luvussa laatimaamme funktiota isInOrder
,
jolle on toteutus valmiina tiedostossa.
Vaihtoehtoisesti voit kokeilla toteuttaa funktion sliding
-metodin
avulla. Se esiteltiin kuutoskierroksen drySpell
-tehtävän yhteydessä.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Parit ovat monikkoja
Pari on erikoistapaus monikon (tuple) käsitteestä. Monikossa voi olla useampiakin jäseniä kuin kaksi:
("Tässä monikossa on neljä erilaista jäsentä.", 100, 3.14159, false)res13: (String, Int, Double, Boolean) = (Tässä monikossa on neljä erilaista jäsentä.,100,3.14159,false)
Onko monikko siis kokoelma samassa mielessä kuin puskurit ja vektorit? Ei ihan.
Monikot ja kokoelmat eroavat toisistaan jo käyttötarkoituksessaan. Monikot ovat edukseen lähinnä silloin, kun 1) jäseniä on pieni vakiomäärä (esim. vain kaksi) ja 2) kullakin jäsenellä on oma erillinen merkityksensä (esim. yksi on minuutit, toinen sekunnit; yksi on nimi, toinen kengännumero), mutta 3) erillisen luokan laatiminen tuntuu silti tarkoitukseen nähden liialliselta.
Tässä keskeisimpiä teknisiä eroja monikoiden ja kokoelmien välillä:
Monikot ja tapausluokat
Luvun 4.4 vapaaehtoisessa materiaalissa esiteltiin lyhyesti Scalan tapausluokat (case class). Tapausluokat voidaan nähdä tiettyyn tarkoitukseen erikoistuneina monikkoina. Lisää tapausluokista kerrotaan keväisellä Ohjelmointistudio 2 -kurssilla.
- Monikossa kunkin jäsenen staattinen tyyppi määritellään erikseen, kokoelmassa kaikille alkioille yhteisesti.
- Monikot ovat muuttumattomia kooltaan ja sisällöltään, kokoelmia on sekä muuttuvatilaisia että muuttumattomia.
- Monikoilla ei ole kokoelmien metodeita (
size
,map
,indexOf
, tms.) eikä monikkoa voi käydä läpi esimerkiksifor
-silmukalla (ilman kommervenkkejä).
Hyödyllinen ja yleinen käyttötarkoitus monikoille ja nimenomaan pareille löytyy hakurakenteista. Siitä kohta lisää.
Toinen johdanto: avaimia ja arvoja
Miten toteuttaisit seuraavat? Mitä yhteistä niillä on?
- Sanakirja: halutaan etsiä annettua suomenkielistä sanaa vastaava englanninkielinen sana.
- Opiskelijahakemisto: halutaan valita opiskelijaolio opiskelijanumeron perusteella.
- Osoitekirja: halutaan, että henkilön nimen perusteella voi löytää henkilö- ja yhteystietoja kuvaavan olion.
- Tietosanakirja: halutaan, että tietyn hakusanamerkkijonon kautta pääsee käsiksi tiettyyn artikkeliolioon.
- Esiintymälaskuri: on olemassa olioita, joilla on tietty ominaisuus,
ja halutaan laskea kuinka yleisiä tuon ominaisuuden eri arvot ovat
olioiden joukossa.
- Esim. luvussa 1.6 esitelty teema: on joukko elokuvia listalta, kullakin elokuvalla on ohjaaj(i)a ja halutaan laskea kunkin ohjaajan yleisyys listalla.
Tutuistakin työkaluista on mahdollista löytää ratkaisuja. Tehdään vaikkapa alkeellinen sanakirja puskureilla:
val hakusanoja = Buffer("kissa", "laama", "tapiiri", "koira")hakusanoja: Buffer[String] = ArrayBuffer(kissa, laama, tapiiri, koira) val englanniksi = Buffer("cat", "llama", "tapir", "puppy")englanniksi: Buffer[String] = ArrayBuffer(cat, llama, tapir, puppy)
Näiden kahden puskurin perusteella voimme hakea sanakirjasta (muista indexOf
-metodi
luvusta 4.2):
val indeksi = hakusanoja.indexOf("tapiiri")indeksi: Int = 2 englanniksi(indeksi)res14: String = tapir englanniksi(hakusanoja.indexOf("koira"))res15: String = puppy
Voimme myös päivittää sanakirjaa vaihtamalla puskurin alkion toiseksi:
englanniksi(hakusanoja.indexOf("koira")) = "dog"englanniksi(hakusanoja.indexOf("koira"))res16: String = dog
Mietitään vielä esimerkkimme luonnetta. Sanakirjassa on suomenkielisiä sanoja, joiden
perusteella haetaan englanninkielisiä vastineita. Voidaan sanoa: suomenkieliset sanat
toimivat avaimina (key), joiden perusteella haetaan englanniksi
-puskurin
sisältämiä arvoja (value).
Sama perusajatus toistuu muissakin yllä luetelluissa tilanteissa: avaimina on opiskelijanumeroita, arvoina opiskelijaolioita; avaimina on ohjaajien nimiä, arvoina heidän elokuviensa lukumääriä; jne. Alla käytämme sanoja "avain" ja "arvo" tässä merkityksessä.
Puskuriratkaisumme perustuu ajatukseen, että puskurien sisältö on tietyssä järjestyksessä
ja avainten indeksit (hakusanoja
-puskurissa) ovat samat kuin niitä vastaavien arvojen
(englanniksi
-puskurissa). Haluttu toiminnallisuus saadaan kyllä toteutettua näinkin,
mutta ratkaisussamme on useita haittapuolia:
- Käytimme erillisiä puskureita avaimille ja arvoille, vaikka miellämme sanakirjan yhdeksi kokonaisuudeksi.
- Indekseillä räplääminen tuntuu vähän turhalta välivaiheelta. Eihän ketään varsinaisesti tässä kiinnosta, monentenako tietty arvo tai avain puskurissa on.
- Mitä jos avainta ei löydy?
- Esim.
englanniksi(hakusanoja.indexOf("Juhan af Grann"))
? - Tällöin
indexOf
palauttaa -1, ja negatiivisen luvun käyttöenglanniksi
-puskuriin indeksinä aiheuttaa ajonaikaisen virhetilanteen. - Ohjelmassa pitäisi siis aina muistaa tutkia
esimerkiksi
if
-käskyllä, onko indeksi negatiivinen vai ei. Näin tarjotaan ohjelmointivirheille mahdollisuus siitä, vaikka ehkäisymenetelmiäkin olisi käytettävissä.
- Esim.
- (Myös tehokkuudessa on parantamisen varaa:
indexOf
-metodi käy puskurin indeksejä läpi järjestyksessä yksitellen, kunnes haettu alkio löytyy. Tämä ei ole lainkaan optimaalista.)
Hakurakenteet
Rakkaasta lapsesta
Hakurakenteiksikin kutsutuilla kokoelmilla on monta nimeä. Englanniksi käytetään useimmiten jotakin termeistä map, dictionary tai associative array. Kunnolla vakiintunutta suomennostakaan ei ole, ja hakurakenteiden sijaan saatetaan puhua esimerkiksi "assosiaatiotauluista", "sanakirjarakenteista" tai "sanakirjoista". Usein kyllä sanotaan vain "mäppi" tai "dicti".
Käytetään siis sellaista kokoelmaa, johon tallennetaan sekä avaimet että arvot. Hakurakenne (map) on erittäin yleisesti käytetty ja kätevä kokoelmatyyppi, joka poikkeaa tähän mennessä kurssilla käsitellyistä indekseihin perustuvista kokoelmista kuten puskureista.
Hakurakenne sisältää avain–arvo-pareja (key–value pairs). Toisin kuin esimerkiksi puskurissa, joissa arvojen hakeminen perustuu järjestysnumeroihin, hakurakenteesta arvoja poimitaan avaimien perusteella.
Avainarvoina voi käyttää mitä tahansa olioita. Ne muodostavat joukon (set) sanan matemaattisessa mielessä, mikä käytännössä tarkoittaa sitä, että avainten on oltava erilaiset (ei kahta samaa suomenkielistä hakusanaa, ei kahta samaa opiskelijanumeroa, jne.).
Scalassa hakurakenteita kuvaa luokka nimeltä Map
.
Seis! map
hän on kokoelmien metodi eikä eräänlainen alkiokokoelma?
Juurihan aiemmissa luvuissa on käytetty metodia nimeltä map
?
Kyllä vain. Ihan sattumasta ei nimeämisessä ole kyse muttei
samastakaan asiasta.
Tuttu kokoelmien metodi map
synnyttää olemassa olevan kokoelman
perusteella toisen kokoelman. Metodin parametrifunktio kuvaa
riippuvuuden alkuperäisten alkioiden ja tulosalkioiden välillä.
Hakurakenne — Scalassa Map
-olio — kokoaa yhteen avaimia
ja arvoja; se pitää tallessa näiden välisiä riippuvuuksia.
map
-metodi on (myös) ihan muilla kokoelmilla kuin hakurakenteilla.
Ensin: avain–arvo-parit
Ennen kuin käytämme Map
-luokkaa, on hyvä tietää vähän avain–arvo-parien kuvaamisesta
Scalalla. Ne ovat ihan tavallisia pareja. Sellaisen voi muodostaa vaikkapa näin:
("koira", "dog")res17: (String, String) = (koira,dog)
Kuitenkin on tapana käyttää toista merkintätapaa, joka korostaa sitä, että avain "osoittaa" arvoon. Seuraava käsky tuottaa tismalleen saman lopputuloksen kuin edellinen:
"koira" -> "dog"res18: (String, String) = (koira,dog)
->
käytetään parin määrittelemiseen. (Huom. ->
eikä
=>
, jota käytetään mm. tyyppimäärittelyissä.)Minkä tahansa parin voi luoda kummalla tahansa tavalla. Hakurakenteiden avain–arvo -pareja luodessamme mekin tulemme käyttämään nuolimerkintää.
Sekä avaimena että arvona voi käyttää mitä vain Scala-oliota. Yllä sekä avain että arvo olivat merkkijonoja, kuten sanakirjallemme on sopivaa. Tarvittaessa voi myös vaikkapa laittaa avaimeksi luvun ja arvoksi merkkijonon tai toisinpäin:
233524 -> "Terhi Teekkari"res19: (Int, String) = (233524,Terhi Teekkari) "Sergio Leone" -> 5res20: (String, Int) = (Sergio Leone,5)
Lisää nuolesta
->
on ihan tavallinen metodi.
"seikkailija" -> "adventurer"res133: (String, String) = (seikkailija,adventurer)
Pistenotaatiotakin voisi käyttää. Seuraavat kaksi tekevät ihan saman:
"polvi".->("knee")res21: (String, String) = (polvi,knee) "polvi" -> "knee"res22: (String, String) = (polvi,knee)
Koska kyseessä on "nuoli", on operaattorinotaatio ymmärrettävästi tässä yhteydessä paljon yleisempi.
Hakurakenteet Scalalla: Map
-luokka
Map
-luokan avulla äskeinen pieni suomi–englanti-sanakirja toteutuu mutkattomasti.
Poimitaan ensinnäkin se Map
-luokka, jota haluamme nyt käyttää:
import scala.collection.mutable.Mapimport scala.collection.mutable.Map
Muista: oliot voivat olla muuttuvatilaisia (mutable) tai
muuttumattomia (immutable). Koska Scala-kieli tukee erilaisia ohjelmointityylejä,
niin sen peruskirjastoissa on kaksi eri Map
-luokkaa, joista toinen kuvaa muuttuvia
hakurakenteita ja toinen muuttumattomia. Tällä kurssilla käytämme varsinkin nyt aluksi
muuttuvia hakurakenteita (ts. hakurakenneoliota voi muuttaa sen luomisen jälkeen).
Niitä kuvaava luokka löytyy pakkauksesta scala.collection.mutable
, ja se on erikseen
otettava käyttöön import
-käskyllä, kuten yllä tehtiin ja kuten olemme tehneet saman
pakkauksen Buffer
-luokan kanssa.
Map
-olion voi luoda tehdasmetodilla (ilman new
-sanaa):
val suomestaEnglanniksi = Map("kissa" -> "cat", "laama" -> "llama", "tapiiri" -> "tapir", "koira" -> "puppy")suomestaEnglanniksi: Map[String,String] = Map(koira -> puppy, tapiiri -> tapir, kissa -> cat, laama -> llama)
Seuraavissa esimerkeissä näet valikoiman Map
-olioiden metodeita. Tälläkään kertaa
oleellista ei ole opetella niitä kaikkia ulkoa, vaan se, että saat yleiskuvan tästä
kokoelmatyypistä.
Seuraavaa lukiessasi huomaat myös, että hakurakenteilla on monta toisilleen vaihtoehtoista metodia, ja samaan tarpeeseen voi löytyä erilaisia ratkaisuja. Heti aluksi tuskin syntyy käsitystä siitä, mikä metodeista on milloinkin paras, mutta ei tarvitsekaan; tilanne paranee kokemuksen kasvaessa. On kuitenkin hyvä pystyä jo tunnistamaan erilaisia ratkaisuja.
Arvon hakeminen
Hakurakenteesta on helppo hakea arvo avaimen perusteella:
suomestaEnglanniksi.get("kissa")res23: Option[String] = Some(cat) suomestaEnglanniksi.get("Juhan af Grann")res24: Option[String] = None
get
-metodille annetaan parametriksi se avain, jota
vastaava arvo halutaan hakea.get
-metodilta saadaan Option[ArvojenTyyppi]
-tyyppinen
palautusarvo.Some
-kääreessä. Jos taas ei, saadaan None
.Arvon vaihtaminen
Kun kyseessä on muuttuvatilainen hakurakenne, voi olemassa olevaa sanakirjaa päivittää tähän tapaan:
suomestaEnglanniksi.get("koira")res25: Option[String] = Some(puppy) suomestaEnglanniksi("koira") = "dog"suomestaEnglanniksi.get("koira")res26: Option[String] = Some(dog)
Muista: Hakurakenteen avaimet ovat kaikki erilaisia. Hakurakenteeseen ei voi kuulua useita
avain–arvo-pareja, joissa on sama avain. Yllä siis korvataan pari "koira" -> "puppy"
parilla "koira" -> "dog"
.
Avain–arvo-parin lisäys
On olemassa useita tapoja lisätä uusi avain–arvo-pari hakurakenteeseen. Yksi tapa on käyttää samanlaista käskyä kuin yllä ja yksinkertaisesti sijoittaa arvo sellaiselle avaimelle, joka ei aiemmin ollut käytössä:
suomestaEnglanniksi("hiiri") = "mouse"
Toinen tapa lisätä tai vaihtaa uusi avain–arvo-pari on käyttää +=
-operaattoria samaan
tapaan kuin olet jo tottunut tekemään puskureille. Tällöin operaattorin oikealle puolelle
kirjoitetaan lisättävä pari:
suomestaEnglanniksi += "sika" -> "pig"res27: Map[String, String] = Map(koira -> dog, tapiiri -> tapir, kissa -> cat, sika -> pig, hiiri -> mouse, laama -> llama)
Tuo käsky korvaa vanhan avain–arvo-parin, jos avain on jo käytössä.
Kurssimateriaalissa käytetään jatkossa enimmäkseen tätä jälkimmäistä tapaa. Itse voit käyttää kumpaa vain.
Avain–arvo-parin poistaminen
Poistaminen onnistuu esimerkiksi näin:
suomestaEnglanniksi -= "laama"res28: Map[String, String] = Map(koira -> dog, tapiiri -> tapir, kissa -> cat, sika -> pig, hiiri -> mouse)
Saman olisi voinut tehdä myös remove
-metodilla, joka palauttaa poistamansa arvon
Option
-kääreessä:
suomestaEnglanniksi.remove("laama")res29: Option[String] = Some(llama)
Löytyykö avainta?
contains
-metodilla voi tutkia, onko tietty avain käytössä:
suomestaEnglanniksi.contains("tapiiri")res30: Boolean = true suomestaEnglanniksi.contains("laama")res31: Boolean = false
Esimerkkitapauksessamme "tapiiri"
löytyy sanakirjasta, kun taas "laama"
jo poistettiin
eikä löydy enää.
Kaikki avaimet tai kaikki arvot
Metodeilla keys
ja values
saa selville kaikki hakurakenteen avaimet tai arvot.
Metodien palautusarvot ovat alkiokokoelmia, joita voi käsitellä tutuilla metodeilla.
Esimerkiksi tässä tulostetaan ensin kukin avaimista ja sitten kukin arvoista:
suomestaEnglanniksi.keys.foreach(println)koira tapiiri kissa sika hiiri suomestaEnglanniksi.values.foreach(println)dog tapir cat pig mouse
mapValues
-metodi
mapValues
toimii kuten muista kokoelmatyypeistä tutuksi tullut map
-metodi, mutta
nimensä mukaisesti se kohdistuu nimenomaan hakurakenteen sisältämiin arvoihin, ei avaimiin.
Käytetään tätä tutunlaista hakurakennetta:
val englanniksi = Map("kissa" -> "cat", "tapiiri" -> "tapir", "koira" -> "dog")englanniksi: Map[String,String] = Map(koira -> dog, tapiiri -> tapir, kissa -> cat)
Sovelletaan mapValues
-metodia yllä määriteltyyn sanakirjaan. Tämän metodin parametriksi
annetaan funktio, joka suoritetaan jokaiselle hakurakenteen sisältämälle arvolle (muttei
avaimelle):
englanniksi.mapValues( _.toUpperCase )res32: Map[String,String] = Map(kissa -> CAT, tapiiri -> TAPIR, koira -> DOG)
mapValues
-metodin palautusarvona saatiin uusi hakurakenne, jossa alkuperäisten arvojen
tilalla on parametrifunktion palautusarvot. Tässä esimerkissä arvot on korvattu
isokirjaimisilla versioilla. Avaimet ovat entisenlaiset.
Alla olevassa toisessa esimerkissä taas korvataan arvoina käytetyt merkkijonot niiden pituuksilla:
englanniksi.mapValues( _.length )res33: Map[String,Int] = Map(kissa -> 3, tapiiri -> 5, koira -> 3)
Tuttuja metodeita hakurakenteillakin
Hakurakenteilla on suuri joukko muista kokoelmista tuttuja metodeita. Tässä muutama esimerkki lukujen 4.2 ja 6.3 esittelemistä kokoelmametodeista:
val englanniksi = Map("kissa" -> "cat", "tapiiri" -> "tapir", "koira" -> "dog")englanniksi: Map[String,String] = Map(koira -> dog, tapiiri -> tapir, kissa -> cat) englanniksi.isEmptyres34: Boolean = false englanniksi.sizeres35: Int = 3 englanniksi.foreach(println)(koira,dog) (tapiiri,tapir) (kissa,cat)
Hakurakenteen kääntämisestä
Usein kysyttyä: miten saadaan muodostettua hakurakenne, jossa on alkuperäisen rakenteet avaimet arvoina ja arvot avaimina?
Yksinkertaisessa tapauksessa tuo onnistuu kätevästi, kun
hyödynnämme parien swap
-metodia, joka toimii näin:
val pari = "avain" -> "arvo"pari: (String, String) = (avain,arvo) pari.swapres36: (String, String) = (arvo,avain)
Sama koko hakurakenteelle:
val suomestaEnglanniksi = Map("kissa" -> "cat", "koira" -> "dog", "laama" -> "llama")suomestaEnglanniksi: Map[String,String] = Map(kissa -> cat, koira -> dog, laama -> llama)
val englannistaSuomeksi = suomestaEnglanniksi.map( _.swap )englannistaSuomeksi: Map[String,String] = Map(cat -> kissa, dog -> koira, llama -> laama)
swap
-metodia kuhunkin
avain–arvo-pariin. Huomaa, että
koska hakurakenteen alkiot ovat
pareja, niin map
-metodille välitetään
parametriksi parin käsittelemiseen
sopiva funktio.Tässä pitää olla tarkkana. Jos sama englanninnos olisi esiintynyt useita kertoja, olisimme menettäneet suunnankäännöksessä informaatiota, koska avaimien on oltava ainutlaatuisia. (Kokeile.)
Jos halutaan, että samaa avainta vastaa useita arvoja, on käytettävä arvona kokoelmaa, jossa ovat tallessa kaikki tiettyä avainta vastaavat arvot. Tässä voi myös olla apua tarkoitukseen erikseen laadituista työkaluista (hakusanoja: scala multimap).
Entä haku molempiin suuntiin?
Jos halutaan, että yhdestä hakurakenteesta voisi hakea samalla tavalla sekä arvolla että avaimella, niin arvotkin täytyy jotenkin saada avaimiksi.
Sanakirjaesimerkissämme avaimet ja arvot olivat molemmat keskenään samantyyppisiä, merkkijonoja. Tällöinhän samat parit voisi periaatteessa lisätä rakenteeseen "molemmin päin". Mutta voidaanko tällöin varmistaa, että avaimet ovat edelleen ainutlaatuisia? Muillakin tavoin ratkaisu voi olla epäkäytännöllinen.
Parempi ratkaisu on voi olla käyttää kahta eri hakurakennetta, joista yksi on "käännetty" versio toisesta (ks. yltä). Voidaan myös toteuttaa tai ottaa jostakin kirjastosta käyttöön kaksisuuntaista hakurakennetta (bidirectional map) kuvaava luokka. Luokan voi toteuttaa esimerkiksi niin, että se pitää sisällään kaksi erillistä ja erisuuntaista hakurakennetta.
Mitä, jos haettu avain puuttuu?
Hakurakenteita käytettäessä on yleensä huomioitava erikseen sellaiset tilanteet, joissa rakenteesta haetaan avainta, mutta sitä ei löydykään. Scalan kirjastot tukevat tällaisten tilanteiden käsittelyä useilla toisilleen vaihtoehtoisilla tavoilla, jotka on hyvä tuntea.
Tuttua yltä on jo se, että get
-metodi on Option
-palautusarvoinen ja palauttaa None
,
kun avainta ei löydy:
englanniksi.get("Juhan af Grann")res37: String = None
Monesti onkin hyvä ratkaisu kutsua get
-metodia ja käyttää sen palauttamaa
Option
-oliota. Mutta on myös tilanteita, joissa tämä ei ole näppärin vaihtoehto.
getOrElse
-metodi
getOrElse
-metodi tarjoaa tavan käsitellä puuttuvia tietoja. Se muistuttaa luvusta 4.3
tuttua Option
-olioiden samannimistä metodia. Erona on, että siinä missä Option
-oliolle
sanottiin "anna sisältösi tai sen puuttuessa käytä tätä arvoa", niin hakurakenteelle
sanotaan "anna tätä avainta vastaava arvo tai sen puuttuessa käytä tätä arvoa".
englanniksi.getOrElse("kissa", "tuntematon hakusana")res38: String = cat englanniksi.getOrElse("Juhan af Grann", "tuntematon hakusana")res39: String = tuntematon hakusana
getOrElse
-metodille annetaan parametriksi haettava avain sekä
"vara-arvo", jonka on syytä olla samaa tyyppiä kuin hakurakenteen
arvot.getOrElse
-palauttaa sitä
vastaavan arvon.String
eikä Option[String]
.
Option
-käärettä ei tarvita, koska kummassakin tapauksessa
pystytään tuottamaan mielekäs String
-tyyppinen palautusarvo.
Kätevää!apply
-oletusmetodi (ja sen vaarat)
Olet jo tottunut katsomaan indeksoidun kokoelman alkion käskyllä kokoelma(indeksi)
, jonka
luku 5.3 kertoi olevan lyhennysmerkintä metodikutsulle kokoelma.apply(indeksi)
. apply
on siis eräänlainen "olion oletusmetodi", jonka nimeä ei ole pakko kutsuessa erikseen kirjoittaa.
Myös hakurakenteilla on apply
-metodi. Se toimii samansuuntaisesti kuin get
,
mutta oleellinen erokin näillä metodeilla on. Vertaillaan:
val englanniksi = Map("kissa" -> "cat", "tapiiri" -> "tapir", "koira" -> "dog")englanniksi: Map[String,String] = Map(koira -> dog, tapiiri -> tapir, kissa -> cat) englanniksi("tapiiri")res40: String = tapir englanniksi("Juhan af Grann")java.util.NoSuchElementException: key not found: Juhan af Grann ... at scala.collection.mutable.HashMap.apply(HashMap.scala:64) ... englanniksi.get("kissa")res41: Option[String] = Some(cat) englanniksi.get("Juhan af Grann")res42: Option[String] = None
apply
-metodin hyvänä puolena verrattuna get
iin on se, että arvon löytyessä saadaan
tuo arvo suoraan ilman Option
-käärettä, jolloin palautusarvon käyttö on pykälän verran
kätevämpää. Kuitenkin apply
-metodi siis tuottaa ajonaikaisen virhetilanteen, jos sille
annetaan parametriksi olematon avain, ja siksi sitä käyttäessä pitää olla hyvin valppaana.
Yleensä get
on turvallisempi ja parempi vaihtoehto.
Pikkutehtävä: get
, apply
ja getOrElse
Hakurakenne luokan ilmentymämuuttujassa
Kuten muitakin kokoelmia myös hakurakennetta voi tietysti käyttää oman luokan rakennusosana. Alla on yksi alkeellinen esimerkki tästä, ja muita esimerkkejä on tulossa myöhemmissä luvuissa.
Jäsenesimerkki
Otetaan taas käyttöön yhteisöjen jäseniä kuvaava luokka Member
(luvusta 4.4). Sen
koodi on oleellisin osin toistettu tässä.
class Member(val id: Int, val name: String, val yearOfBirth: Int, val yearOfDeath: Option[Int]) {
// etc.
}
Laaditaan kokeeksi yhteisöä kuvaava luokka Society
, jossa käytämme hakurakennetta
ilmentymämuuttujan kautta.
class Society(val name: String) {
private val members = Map[Int, Member]()
def add(newMember: Member) = {
this.members(newMember.id) = newMember
}
def getByID(memberID: Int) = this.members.get(memberID)
}
Map
-olion metodeita.Pikkutehtävä: hakurakenteet ja Option
Lisää Miscellaneous-projektista löytyvään Society
-luokkaan metodi yearOfDeath
, joka
toimii näin:
- Sille annetaan parametriksi jäsennumero.
- Se palauttaa
Option[Int]
-tyyppisen arvon, johon on kääritty sen jäsenen kuolinvuosi, jolla on annettu jäsennumero. - Kuitenkin jos kuolinvuotta ei ole, koska kyseistä jäsentä
ei ole tai hän on elossa, palautetaan
None
.
Älä tee muita muutoksia Society
-luokkaan äläkä muuta Member
-luokkaa.
Kannattaa kutsua erästä aiemmista luvuista tuttua kokoelmien metodia, niin tehtävän ratkaisu on pitkälti siinä.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Lisäluettavaa
Hakurakenteen "vara-arvo"
Vaikka getOrElse
-metodi on sinänsä kätevä, on melko tyypillistä,
että sen toiseksi parametriksi annetaan tietylle hakurakenteelle
aina sama "vara-arvo". Sanakirjaesimerkissämme voitaisiin vaikkapa
haluta tuottaa teksti "ähäkutti"
aina, jos rakenteesta haetaan
olemattomalla avaimella.
Voit välttyä välittämästä samaa parametriarvoa toistuvasti
getOrElse
-metodille käyttämällä withDefaultValue
-metodia.
Tätä metodia käytetään yleensä juuri luodulle hakurakenteelle,
kuten tässä:
val englanniksi = Map("kissa" -> "cat", "tapiiri" -> "tapir", "koira" -> "dog").withDefaultValue("ähäkutti")englanniksi: Map[String,String] = Map(koira -> dog, tapiiri -> tapir, kissa -> cat) englanniksi("kissa")res43: String = cat englanniksi("Juhan af Grann")res44: String = ähäkutti
withDefaultValue
-metodille ilmoitetaan,
mitä haluamme käyttää "vara-arvona"
silloin, kun haku on huti.apply
-metodilla olematonta
avainta, samme tämän vara-arvon.Tämä on oikein hyvä tapa käyttää hakurakennetta. Siispä seuraava suositus:
- Kun hakurakenteelle on määriteltävissä
mielekäs "vara-arvo", määrittele sellainen.
Tällöin on kätevää ja turvallista käyttää
apply
-metodia arvon hakemiseen. - Kun hakurakenteelle ei ole tarkoituksenmukaista
"vara-arvoa" (ohjelman luonteesta johtuen),
niin käytä hakemiseen
get
iä taigetOrElse
ä.
Uskontoni kieltää häshmäpin käytön?
Tuolla ylempänä esiintyi virheilmoituksessa luokan nimi HashMap
.
Mikä se on?
Sana hash ei tässä liity Twitteriin eikä muihinkaan ajanvietteisiin vaan erääseen hakurakenteiden toteutustekniikkaan.
Scala API:n Map
on abstrakteja metodeita sisältävä piirreluokka,
joka kuvaa hakurakenteiden toimintoja yleisesti määrittelemättä
sisäisesti käytettävää toteutustapaa. HashMap
on konkreettinen
luokka, joka toteuttaa Map
-piirreluokan abstraktit metodit
käyttäen hajautukseksi (hashing) kutsuttua toteutustapaa.
Luomamme hakurakenne on siis sekä yleisempää tyyppiä Map
että
sen alatyyppiä HashMap
.
Tämän luvun johdannossa mainittiin ohimennen, että kahteen puskuriin perustuva sanakirjatoteutus ei ollut kovin tehokas, koska indeksejä käytiin läpi järjestyksessä yksi kerrallaan ja mikä tahansa avain–arvo-pari voi löytyä mistä tahansa puskurien kohdasta. Hajautustoteutus on tyypillisissä käyttötilanteissa tehokkaampi, koska siinä avain–arvo-parit sijoitellaan hakurakenteeseen harkitusti avaimen perusteella.
Hajautusta ei käsitellä tällä kurssilla kuten ei tehokkuuden lisäämiseen tarkoitettuja algoritmeja yleensäkään, mutta voit lukea lisää vaikkapa Wikipediasta tai käydä Aallon kurssin Tietorakenteet ja algoritmit.
Yhteenvetoa
- Useita tietoja, keskenään erityyppisiäkin, voi koota yhteen monikoksi. Pari on kaksipaikkainen monikko.
- Hakurakenne on erittäin yleisesti käytetty kokoelmatyyppi.
Se sisältää avain–arvo-pareja ja poikkeaa indekseihin
perustuvista kokoelmista muun muassa siten, että:
- alkioilla ei ole vastaavaa numerojärjestystä
- eikä alkioita poimita järjestysnumeron (indeksin) perusteella, vaan
- useimmiten annetaan avain ja poimitaan sitä vastaava arvo.
- Scalassa hakurakenteita kuvaa luokka
Map
, jolla on runsaasti hyödyllisiä metodeita. - Lukuun liittyviä termejä sanastosivulla: monikko, pari; hakurakenne, avain–arvo-pari.
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.
(String, String)
tarkoittaa, että siinä on sellainen pari, jossa on kaksi merkkijonoa.