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.
Oheismoduulit: 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ä sulkeita 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 sulkeita 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:
toMinsAndSecs(187)res4: (Int, Int) = (3,7) val (min, s) = toMinsAndSecs(200)min: Int = 3 s: Int = 20
Kirjoita ratkaisu Miscellaneous-moduulin 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
Moduulin 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 suljemerkintää
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
moduulissa 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.
Tutuillakin työkaluilla tuo on ratkaistavissa. 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
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.isEmptyres32: Boolean = false englanniksi.sizeres33: Int = 3 englanniksi.foreach(println)(koira,dog) (tapiiri,tapir) (kissa,cat)
Erityismaininnan ansaitsee map
-metodi, jolla voi käsitellä hakurakenteen sisältämiä
avain–arvo-pareja ja näin muodostaa uuden hakurakenteen:
val kokeiluMap = englanniksi.map( pari => pari._1.toUpperCase -> (pari._2 + "!!!") )kokeiluMap: Map[String,String] = Map(KISSA -> cat!!!, TAPIIRI -> tapir!!!, KOIRA -> dog!!!)
map
(pienellä) hakurakenteeseen, joka on
tyyppiä Map
(isolla).map
-metodille
välitetään parametriksi parin käsittelemiseen sopiva funktio.Usein halutaan muodostaa uusi hakurakenne, jossa avaimet ovat entiset mutta arvot erilaiset kuin alkuperäisessä. Muodostetaan kokeeksi hakurakenne, josta voi hakea avaimen perusteella käännöksen pituuden:
val kaannospituudet = englanniksi.map( pari => pari._1 -> pari._2.length )kaannospituudet: Map[String,String] = Map(kissa -> 3, tapiiri -> 5, koira -> 3)
map
-metodille funktion, joka jättää
avain–arvoparin ensimmäisen jäsenen ennalleen.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.swapres34: (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.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 haluat, että samaa avainta vastaa useita arvoja, käytä arvona kokoelmaa, jossa ovat tallessa kaikki tiettyä avainta vastaavat arvot. Tässä voi myös olla apua erityistyökaluista (hae netistä: scala multidict).
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")res35: 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")res36: String = cat englanniksi.getOrElse("Juhan af Grann", "tuntematon hakusana")res37: 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")res38: 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")res39: Option[String] = Some(cat) englanniksi.get("Juhan af Grann")res40: 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-moduulista 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")res41: String = cat englanniksi("Juhan af Grann")res42: 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!
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.
Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.
(String, String)
tarkoittaa, että siinä on sellainen pari, jossa on kaksi merkkijonoa.