Luku 9.2: Pareja ja hakuja
Johdanto: kaksi paluuarvoa?
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.
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ä on siedettävä ratkaisu, joskaan Vector[Int]
-tyyppi ei osuvasti kuvaa sitä, että
palautettavia lukuja on aina tasan kaksi. 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 tuttuun tyyliin:
var pari = ("reppu", "reissumies")pari: (String, String) = (reppu,reissumies) pari(0)res2: String = reppu pari(1)res3: String = reissumies
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)
Keskeinen ero on sekin, että parin koko on etukäteen tiedossa. Tämä yritys tuottaa käännösaikaisen virheilmoituksen eikä näin ollen voi kaataa ohjelmaa:
nimiJaKengannumero(2)-- Error: nimiJaKengannumero(2) ^
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.
Kokoelman jaottelu pariksi
Pareja on käytetty myös useassa Scala APIn 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)res5: (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.
Palautetun parin ensimmäisessä jäsenessä ovat kaikki alkuperäisen kokoelman alkiot osoitettua järjestysnumeroa edeltävät alkiot ja toisessa kaikki loput.
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.zipWithIndexres6: Vector[(String, Int)] = Vector((laama,0), (alpakka,1), (vikunja,2))
Paluuarvona saadaan vektori, jossa on sisällä pareja, joissa on jäseninä merkkijono ja kokonaisluku kussakin.
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 do println(pari(0) + " on numero " + pari(1))laama on numero 0 alpakka on numero 1 vikunja on numero 2 for (alkio, indeksi) <- lajit.zipWithIndex do 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(120, 90, 80)korkeudet: Vector[Int] = Vector(120, 90, 80) val korkeudetJaLajit = korkeudet.zip(lajit)korkeudetJaLajit: Vector[(Int, String)] = Vector((120,laama), (90,alpakka), (80,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((120,laama), (90,alpakka), (80,vikunja))
Vetoketjun voi myös avata:
korkeudetJaLajit.unzipres7: (Vector[Int], Vector[String]) = (Vector(120, 90, 80),Vector(laama, alpakka, vikunja))
Tässä siis syntyi pari, jonka jäsenet ovat vektoreita.
Pari funktion parametrina
Luonnollisesti voimme myös määritellä funktion, joka ottaa parametriksi parin. Tämä funktio palauttaa lukuparin erotuksen itseisarvon:
def absDiff(pairOfNumbers: (Int, Int)) =
(pairOfNumbers(0) - pairOfNumbers(1)).abs
Käyttöesimerkki:
val kokeilu = (10, 30)kokeilu: (Int, Int) = (10,30) absDiff(kokeilu)res8: Int = 20 absDiff((-300, 100))res9: Int = 400
Tämäkin toimii:
absDiff(-300, 100)res10: Int = 400
Tästä siis puuttuvat parin muodostavat lisäsulut ja kutsumme
yksiparametrista absDiff
-funktiota aivan kuin se ottaisi kaksi
erillistä Int
-parametria. Scala muodostaa kahdesta parametriarvostamme
automaattisesti parin, ja tämä tekee saman kuin absDiff((-300, 100))
.
Samansuuntaisella ajatuksella toimii seuraavakin:
Parit ja korkeamman asteen funktiot
Käytetään näitä edeltä tuttuja kokoelmia:
val lajit = Vector("laama", "alpakka", "vikunja")lajit: Vector[String] = Vector(laama, alpakka, vikunja) val korkeudet = Vector(120, 90, 80)korkeudet: Vector[Int] = Vector(120, 90, 80) val korkeudetJaLajit = korkeudet.zip(lajit)korkeudetJaLajit: Vector[(Int, String)] = Vector((120,laama), (90,alpakka), (80,vikunja))
Yksi tapa tulostaa raportti kustakin lajista on tämä:
def korkeusraportti(korkeusLajiPari: (Int, String)) = korkeusLajiPari(1) + " on " + korkeusLajiPari(0) + " cm"def korkeusraportti(korkeusLajiPari: (Int, String)): String korkeudetJaLajit.map(korkeusraportti).foreach(println)laama on 120 cm alpakka on 90 cm vikunja on 80 cm
Koska kokoelma sisältää pareja, annamme map
-metodille funktion,
joka ottaa parametriksi yhden parin ja kertoo, mitä sillä tehdään.
Kuitenkin Scala antaa käyttää parin vastaanottavan funktion sijaan kaksiparametrista funktiota. Niinpä voimme kirjoittaa näinkin:
def korkeusraportti(cm: Int, laji: String) = s"$laji on $cm cm"def korkeusraportti(cm: Int, laji: String): String korkeudetJaLajit.map(korkeusraportti).foreach(println)laama on 120 cm alpakka on 90 cm vikunja on 80 cm
Määrittelemme aivan tavallisen kaksiparametrisen funktion, joka ottaa luvun ja merkkijonon.
Sen voi antaa parametriksi myös pareja sisältävän kokoelman
map
-metodille. Scala huolehtii siitä, että kaksiparametrista
funktiotamme kutsutaan kokoelman kunkin parin jäsenille.
Tehtä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))res11: Boolean = true isAscending(Vector(-10, 1, 5, 5, 10))res12: Boolean = true isAscending(Vector(-10, 1, 5, 9, 5, 10))res13: Boolean = false isAscending(Vector(10, 5, 1, -10))res14: Boolean = false
Metodin ei sovi tehdä lisätyötä järjestämällä alkiot. Sen pitää ainoastaan tarkistaa, ovatko alkiot järjestyksessä.
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. (Vaihtoehtoisesti
voit kokeilla toteuttaa funktion sliding
-metodin avulla. Se esiteltiin
luvun 7.1 drySpell
-tehtävän yhteydessä.)
forall
-metodilla voi katsoa, päteekö kaikille peräkkäisten alkioiden
pareille, että ne ovat keskenään oikeassa järjestyksessä. Apufunktion,
joka välität forall
ille voit itse määritellä joko nimetyksi funktioksi
tai nimettömäksi funktioliteraaliksi.
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)res15: (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ä:
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ä).
Monikot ja tapausluokat
Luvun 4.4 vapaaehtoisessa materiaalissa esiteltiin lyhyesti Scalan tapausluokat (case class). Tapausluokat voi nähdä tiettyyn tarkoitukseen erikoistuneina monikkoina. Lisää tapausluokista kerrotaan keväisellä Ohjelmointistudio 2 -kurssilla.
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)res16: String = tapir englanniksi(hakusanoja.indexOf("koira"))res17: String = puppy
Voimme myös päivittää sanakirjaa vaihtamalla puskurin alkion toiseksi:
englanniksi(hakusanoja.indexOf("koira")) = "dog"englanniksi(hakusanoja.indexOf("koira"))res18: 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.
Ensinnäkin käytimme erillisiä puskureita avaimille ja arvoille, vaikka miellämme sanakirjan yhdeksi kokonaisuudeksi.
Lisäksi 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.
Kolmanneksi: mitä jos avainta ei löydy? Esim.
englanniksi(hakusanoja.indexOf("insulintialainen kummitussirkka"))
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ä.
(Ja neljänneksi: Tehokkuudessakin 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
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, 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. map
-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
Avain–arvo-parit ovat Scalassa ihan tavallisia pareja. Sellaisen voi muodostaa vaikkapa näin:
("koira", "dog")res19: (String, String) = (koira,dog)
On kuitenkin tapana käyttää toista merkintätapaa, joka korostaa sitä, että avain "osoittaa" arvoon. Seuraava käsky tuottaa tismalleen saman lopputuloksen kuin edellinen:
"koira" -> "dog"res20: (String, String) = (koira,dog)
Nuolta ->
käytetään parin määrittelemiseen.
Sen vasemmalle puolelle kirjoitetaan avain ja oikealle puolelle se arvo, johon kyseinen avain osoittaa.
Tulos on pari toisiinsa liittyvää tietoa.
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 panna avaimeksi luvun ja arvoksi merkkijonon tai toisinpäin:
233524 -> "Terhi Teekkari"res21: (Int, String) = (233524,Terhi Teekkari) "Sergio Leone" -> 5res22: (String, Int) = (Sergio Leone,5)
Huomaathan, että näissä pareissa käytetty nuoli on ->
eikä =>
, jota olemme käyttäneet
parametrien tyypeissä ja case
-sanan perässä.
Nyt alkaa jo mennä nuolet vähän sekaisin.
Lisää nuolesta
->
on ihan tavallinen metodi.
"seikkailija" -> "adventurer"res23: (String, String) = (seikkailija,adventurer)
Kohdeolio, jolle metodia kutsutaan.
Viivalla ja väkäsellä nimetty metodi on tarjolla kaikille Scala-olioille. Tässä on käytetty operaattorinotaatiota (luku 5.2), jossa ei ole pistettä kohdeolion ja metodin nimen välissä.
Jälkimmäinen merkkijono on metodin parametri. Operaattorinotaatiossa sen ympäriltä voi jättää sulkeet pois.
Metodi palauttaa parin, joka muodostuu kohdeoliosta ja parametrioliosta.
Pistenotaatiotakin voisi käyttää. Seuraavat kaksi tekevät ihan saman:
"polvi".->("knee")res24: (String, String) = (polvi,knee) "polvi" -> "knee"res25: (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.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.
Luodaan Map
-olio:
val suomestaEnglanniksi = Map("kissa" -> "cat", "laama" -> "llama", "tapiiri" -> "tapir", "koira" -> "puppy")suomestaEnglanniksi: Map[String,String] = Map(koira -> puppy, tapiiri -> tapir, kissa -> cat, laama -> llama)
Erona tutunlaisiin kokoelmatyyppeihin on se, että hakurakenteen alkiot ovat aina avain–arvo-pareja.
Hakurakenteella on kaksi tyyppiparametria: avainten tyyppi ja arvojen tyyppi. Tässä olemme luoneet hakurakenteen, jossa sekä avaimet että arvot ovat merkkijonoja.
REPLin tulosteestakin huomaa, että kyseessä ei ole indeksoitu rakenne. Alkiot ovat hakurakenteessa mielivaltaisen näköisessä järjestyksessä. Niillä ei ole järjestysnumeroita. Se, missä järjestyksessä hakurakenneolio pitää sisältönsä, riippuu sen sisäisestä toteutuksesta eikä ole tässä yhteydessä oleellista.
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 silti hyvä pystyä jo tunnistamaan erilaisia ratkaisuja.
Arvon hakeminen
Hakurakenteesta on helppo hakea arvo avaimen perusteella:
suomestaEnglanniksi.get("kissa")res26: Option[String] = Some(cat) suomestaEnglanniksi.get("insulintialainen kummitussirkka")res27: Option[String] = None
get
-metodille annetaan parametriksi se avain, jota
vastaava arvo halutaan hakea.
get
-metodilta saadaan Option[ArvojenTyyppi]
-tyyppinen
paluuarvo.
Jos haettu avain oli kokoelmassa, saadaan sitä vastaava
arvo Some
-kääreessä. Jos taas ei, saadaan None
.
Arvon vaihtaminen
Kun kyseessä on muuttuvatilainen hakurakenne, voi sanakirjaa päivittää tähän tapaan:
suomestaEnglanniksi.get("koira")res28: Option[String] = Some(puppy) suomestaEnglanniksi("koira") = "dog"suomestaEnglanniksi.get("koira")res29: 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"res30: 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"res31: 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")res32: Option[String] = Some(llama)
Löytyykö avainta?
contains
-metodilla voi tutkia, onko tietty avain käytössä:
suomestaEnglanniksi.contains("tapiiri")res33: Boolean = true suomestaEnglanniksi.contains("laama")res34: 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 paluuarvot 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.isEmptyres35: Boolean = false englanniksi.sizeres36: 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( finEngPari => finEngPari(0).toUpperCase -> (finEngPari(1) + "!!!") )kokeiluMap: Map[String,String] = Map(KISSA -> cat!!!, TAPIIRI -> tapir!!!, KOIRA -> dog!!!)
Käytämme metodia map
(pienellä) hakurakenteeseen, joka on
tyyppiä Map
(isolla).
Koska hakurakenteen alkiot ovat pareja, niin map
-metodille
välitetään parametriksi parin käsittelemiseen sopiva funktio.
Esimerkkifunktiomme muodostaa uuden parin, jossa on alkuperäisen parin ensimmäisen jäsenen (eli suomenkielisen avaimen) isokirjaiminen versio sekä toisen jäsenen (eli englanninkielisen arvon) huutomerkeillä täydennetty versio.
Näistä uusista pareista muodostuu uusi hakurakenne, jossa avaimien perusteella voi hakea huutomerkillisiä merkkijonoja.
Äskeisen voi kirjoittaa kauniimmin ja selkeämmin. Hyödynnetään aiemmin tässä luvussa mainittua tietoa siitä, että Scala sallii meidän käyttää kaksiparametrista funktiota sellaisessa kohdassa, johon kelpaisi yhden parin parametrikseen ottava funktio. Siis näin:
val kokeiluMap = englanniksi.map( (fin, eng) => fin.toUpperCase -> (eng + "!!!") )kokeiluMap: Map[String,String] = Map(KISSA -> cat!!!, TAPIIRI -> tapir!!!, KOIRA -> dog!!!)
Käytämmekin kaksiparametrista funktioliteraalia, jonka parametrit on helppo nimetä kuvaavasti. Indeksejä emme tarvitse.
Usein halutaan muodostaa uusi hakurakenne, jossa avaimet ovat entiset mutta arvot erilaiset kuin alkuperäisessä. Muodostetaan kokeeksi hakurakenne, josta voi hakea avaimen perusteella englanninnoksen pituuden:
val kaannospituudet = englanniksi.map( (fin, eng) => fin -> eng.length )kaannospituudet: Map[String,Int] = Map(kissa -> 3, tapiiri -> 5, koira -> 3)
Välitämme map
-metodille funktion, joka jättää
avain–arvoparin ensimmäisen jäsenen ennalleen.
Hakurakenteen kääntämisestä
Joskus 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.swapres37: (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)
Sovelletaan 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).
Mitä jos haettu avain puuttuu?
Hakurakenteita käytettäessä on usein 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
-paluuarvoinen ja palauttaa None
,
kun avainta ei löydy:
englanniksi.get("kummitussirkka")res38: 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. 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")res39: String = cat englanniksi.getOrElse("kummitussirkka", "tuntematon hakusana")res40: String = tuntematon hakusana
getOrElse
-metodille annetaan parametriksi haettava avain sekä
"vara-arvo", jonka on syytä olla samaa tyyppiä kuin hakurakenteen
arvot.
Jos avain löytyy hakurakenteesta, getOrElse
-palauttaa sitä
vastaavan arvon.
Jos avainta ei löydy, metodi palauttaa toisen parametrinsa.
Metodin paluuarvo on String
eikä Option[String]
. Option
-käärettä ei tarvita, koska kummassakin tapauksessa pystytään
tuottamaan mielekäs String
-tyyppinen paluuarvo. Kätevää!
apply
-oletusmetodi (ja sen vaarat)
Olet jo tottunut katsomaan indeksoidun kokoelman alkion käskyllä kokoelma(indeksi)
,
jonka luku 5.3 kertoi lyhennysmerkinnäksi 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")res41: String = tapir englanniksi("kummitussirkka")java.util.NoSuchElementException: key not found: kummitussirkka ... at scala.collection.mutable.HashMap.apply(HashMap.scala:64) ... englanniksi.get("kissa")res42: Option[String] = Some(cat) englanniksi.get("kummitussirkka")res43: 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 paluuarvon 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 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]):
// Jne.
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)
end Society
Yhteisöoliolla on ominaisuuksina paitsi nimi myös hakurakenne, johon tallennetaan jäsenolioita, jäsennumeroita avaimina käyttäen. Kun yhteisöolio luodaan, tämä hakurakenne on vielä tyhjä.
Yhteisöoliolla on metodeita, joilla siihen voi lisätä jäseniä
ja joilla sen jäsenkuntaa voi tutkia. Näiden toteutuksessa
käytetään apuna 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.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.
Kutsu 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")res44: String = cat englanniksi("kummitussirkka")res45: String = ähäkutti
withDefaultValue
-metodille ilmoitetaan,
mitä haluamme käyttää "vara-arvona"
silloin, kun haku on huti.
Nyt kun haemme apply
-metodilla olematonta
avainta, samme tämän vara-arvon.
Mikä on HashMap
?
Tuolla ylempänä esiintyi virheilmoituksessa luokan nimi HashMap
.
Sana hash ei tässä liity sosiaaliseen mediaan tai muihinkaan
ajanvietteisiin vaan erääseen hakurakenteiden toteutustekniikkaan.
Scala APIn 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, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Joel Toppinen, Anna Valldeoriola Cardó 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, Juha Sorva ja Jaakko Nakaza. 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; sitä 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 suunnitteluun ja toteutukseen on osallistunut useita opiskelijoita yhteistyössä O1-kurssin opettajien kanssa.
Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.
Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.
Sulkeet löytyvät myös paluuarvon kuvauksesta. Merkintä
(String, String)
tarkoittaa, että siinä on sellainen pari, jossa on kaksi merkkijonoa.