Tämä sivu kuvailee valikoituja Scala-kielen ominaisuuksia ja Scalan
oheiskirjastojen sisältämiä työkaluja. Sivulla on niistä pieniä, irrallisia
esimerkkejä. Voit oppimateriaalin varsinaisiin lukuihin jo
tutustuttuasi käydä kertaamassa yksityiskohtia täältä.
Tämä kooste ei kata koko Scala-kieltä, vaan siinä painottuvat rakenteet, joita
Ohjelmointi 1 -kurssilla muutenkin käsitellään. Mukana on muutamia Ohjelmointi 1:n
oman apukirjaston työkaluja.
Tämä on ainoastaan luettelo eräistä työkaluista. Alla ei opeteta periaatteita tai
käsitteitä eikä kerrota, mitä esitellyillä välineillä kannattaa tehdä; niistä
asioista opit oppimateriaalin varsinaisissa luvuissa. Sivu ei etene nikamalleen samassa
järjestyksessä kuin nuo luvut.
Etkö löydä etsimääsi?
Vastauksia voi löytyä myös näiden linkkien kautta:
Pidemmän päälle kannattaa opetella lukemaan Scalan omaa dokumentaatiota, vaikka se alkeiskurssilaiselle
osin vaikeaselkoista ovatkin.
Jos olisit kaivannut tälle sivulle jotakin, mitä täällä ei ole, voit
kertoa asiasta sivun lopun palautelomakkeella tai suoraan sähköpostitse
osoitteeseen juha.sorva@aalto.fi.
Myös tietotyypin voi erikseen kirjata, kuten alla, vaikka tyyppipäättelyn ansiosta se
on usein tarpeetonta:
val toinenMuuttuja: Int = 200toinenMuuttuja: Int = 200
Muuttujan nimeä voi käyttää lausekkeena. Tällainen lauseke voi olla suuremman
lausekkeen osana:
lukumuuttujares10: Int = 100
lukumuuttuja + toinenMuuttuja + 1res11: Int = 301
val-muuttujan arvoa ei voi vaihtaa, mutta var-muuttujaan voi sijoittaa uuden arvon,
joka korvaa aiemman:
var muutettavissa = 100muutettavissa: Int = 100
muutettavissa = 150muutettavissa: Int = 150
muutettavissa = muutettavissa + 1muutettavissa: Int = 151
Viimeisessä äskeisistä sijoituksista uusi arvo saadaan yksinkertaisella
laskutoimituksella saman muuttujan edellisestä arvosta. Tämänkaltaiset
sijoituskäskyt voi kirjoittaa lyhyemminkin yhdistämällä sijoituksen ja
aritmeettisen operaattorin (luku 4.1):
muutettavissares12: Int = 151
muutettavissa += 10muutettavissa -= 100muutettavissa *= 2muutettavissamuutettavissa: Int = 122
Kommentteja
Ohjelmakoodin kirjoitetut kommentit (luku 1.2) eivät vaikuta ohjelman suoritukseen.
// Tämä on yksirivinen kommentti. Se alkaa kahdella kauttaviivalla ja päättyy rivin loppuun.valmuuttuja=100// Kommentin voi kirjoittaa sen koodirivin perään, johon se liittyy./* Tällainen kommentti, joka alkaa kauttaviivalla ja tähdellä, voi olla monirivinenkin. Kommentti päättyy samoihin merkkeihin toisin päin. */
Aloitusmerkintää /** käytetään dokumentaatiokommenttien kirjoittamiseen (luku 3.2):
/** Tämä seuraavan muuttujan kuvaus tulee dokumenttiin. */valteksti="Minut on dokumentoitu."
Dokumentaatiokommenttien perusteella voi automaattisesti tuottaa Scaladoc-sivuja.
Pakkaukset ja kirjastot
Pakkausten käyttö
Scalan peruskirjastojen (luku 3.2) ja muiden pakkausten työkaluja voi
käyttää kirjaamalla pakkauksen nimen käytetyn funktion tai muun työkalun nimen eteen.
Tässä käytetään abs-itseisarvofunktiota pakkauksesta scala.math:
scala.math.abs(-50)res13: Int = 50
Itse asiassa yleispakkauksen scala sisältö on aina automaattisesti käytössä, joten
saman voi sanoa lyhyemminkin viittaamalla vain alipakkaukseen math:
math.abs(-50)res14: Int = 50
Yleispakkauksesta scala löytyvät mm. tietotyypit Int ja Double, kokoelmatyypit
Vector ja List sekä tulostusfunktio println. Näitä työkaluja voi käyttää
mainitsematta pakkauksen nimeä lainkaan. Ei siis tarvitse kirjoittaa esimerkiksi
scala.Int, vaikka se sallittua onkin.
Usein pakkausten nimien toistuvan kirjoittamisen voi välttää import-käskyllä:
import scala.math.absabs(-50)res15: Int = 50
abs(100)res16: Int = 100
Nyt ei tarvita pakkauksen nimeä.
Näin otetaan käyttöön pakkauksen koko sisältö kerralla:
import scala.math.*
import-käskyt kirjoitetaan usein kooditiedoston alkuun, jolloin mainitut työkalut
ovat käytössä koko tiedoston sisältämässä koodissa. Käskyn voi sijoittaa myös muualle:
esimerkiksi import funktion rungon alussa tuo työkalun käyttöön vain kyseiseen
funktioon.
Pakkauksen määritteleminen
Itse laadittujen työkalujen pakkaukset merkitään Scala-kooditiedostojen alkuun tällaisella
määrittelyllä (luku 2.6):
packagepakkauksen.kenties.moniosainen.nimi
Tiedostot tallennetaan pakkausten nimiä vastaaviin kansioihin.
Yleisiä funktioita scala.math-pakkauksesta
Muutama yleishyödyllinen funktio pakkauksesta scala.math:
import scala.math.*val itseisarvo = abs(-50)itseisarvo: Int = 50
val potenssi = pow(10, 3)potenssi: Double = 1000.0
val neliojuuri = sqrt(25)neliojuuri: Double = 5.0
val sini = sin(1)sini: Double = 0.8414709848078965
val kahdestaIsompi = max(2, 10)kahdestaIsompi: Int = 10
val kahdestaPienempi = min(2, 10)kahdestaPienempi: Int = 2
Samasta pakkauksesta löytyvät mm. muut trigonometriset funktiot (cos, atan jne.),
cbrt (kuutiojuuri), hypot (hypotenuusa; parametreiksi kaksi kateetinmittaa),
floor (alaspäin pyöristys), ceil (ylöspäin pyöristys), round (lähimpään pyöristys),
log ja log10 (logaritmeja). Koko luettelo löytyy pakkauksen dokumentaatiosta.
Osia muiden Scala APIn pakkausten sisällöstä on esitelty tämän sivun muissa kappaleissa
aiheittain.
Syötettä ja tulostetta: println, readLine
Tekstikonsoliin tai REPLiin tulostaminen onnistuu println-käskyllä:
println(100 + 1)101
println("laama")laama
Alla on esimerkkejä näppäimistösyötteen lukemisesta tekstikonsolissa (luku 2.7).
Esimerkeissä oletetaan, että käsky importscala.io.StdIn.* on annettu.
println("Kirjoita jotain tätä kehotetta seuraavalle riville: ")valkayttajanSyottamaTeksti=readLine()
Jos kehotteen ja syötteen väliin ei halua rivinvaihtoa, voi käyttää print-käskyä,
joka ei vaihda riviä lopuksi:
print("Kirjoita jotain tämän kehotteen perään samalle riville: ")valkayttajanSyottamaTeksti=readLine()
Sama lyhyemmin:
valkayttajanSyottamaTeksti=readLine("Kirjoita jotain tämän kehotteen perään samalle riville: ")
readLine tuottaa String-tyyppisen arvon. Käyttäjän syötteen voi myös tulkita saman
tien lukuarvoksi:
Parametrien tyypit on kirjattava
kaksoispisteiden perään.
Muista muutkin välimerkit.
Kun funktion rungon muodostaa vain yksi lauseke, funktion
paluuarvo saadaan evaluoimalla tuo lauseke.
Funktion kutsuminen
Funktiokutsu:
keskiarvo(10.0, 12.5)res17: Double = 11.25
Funktiokutsu on lauseke, jonka arvo on funktion palauttama arvo.
Monirivinen funktio
Kun funktion runko koostuu useasta peräkkäisestä käskystä, se jaetaan usealle riville ja
sisennetään def-riviä syvemmälle. Tässä esimerkki luvusta 1.7:
Kaikissa yllä olevissa esimerkeissä paluuarvon tyyppi on jätetty kirjaamatta koodiin,
mikä on sallittua tyyppipäättelyn vuoksi. Paluuarvon tyypin saa erikseen kirjatakin
(luku 1.8), kuten näissä esimerkeissä:
defkeskiarvo(eka:Double,toka:Double):Double=(eka+toka)/2defpalautaTeksti:String="Funktiokutsu palautaTeksti tuottaa aina tämän merkkijonon."
Tietyissä tilanteissa paluuarvon tyyppi on pakko kirjata. Näin on eritoten silloin,
jos funktio kutsuu itsensä kanssa samannimistä funktiota eli joko
toista samannimistä mutta eriparametrista
funktiota (kuormitettaessa; luku 4.1) tai
return-käskyn perään kirjoitetaan lauseke, jonka arvo palautetaan.
Funktiolle, jossa return-käskyä käytetään, on kirjattava paluuarvon tyyppi.
Yksittäisoliot
Olion määritteleminen: metodit, muuttujat, this
Yksittäisen olion määrittely luvussa 2.2 tarkemmin kuvaillusta esimerkistä:
objecttyontekija:varnimi="Matti Mikälienen"valsyntynyt=1965varkkpalkka=5000.0vartyoaika=1.0defikaVuonna(vuosi:Int)=vuosi-this.syntynytdefkuukausikulut(kulukerroin:Double)=this.kkpalkka*this.tyoaika*kulukerroindefkorotaPalkkaa(kerroin:Double)=this.kkpalkka=this.kkpalkka*kerroindefkuvaus=this.nimi+" (s. "+syntynyt+"), palkka "+this.tyoaika+" * "+this.kkpalkka+" euroa"endtyontekija
Avainsanan object perään kirjoitetaan oliolle valittu nimi ja
kaksoispiste. (Ei yhtäsuuruusmerkkiä kuten funktioiden määrittelyissä.)
Sisennykset kertovat, mitkä osat kuuluvat olion runkoon.
Olion rungon loppuun kirjoitetaan usein loppumerkki. Se ei ole
pakollinen mutta selkiyttää tällaista koodia, jossa on välissä tyhjiä
rivejä (ks. tyyliopas.
Olion tietoja tallennettuna muuttujiin. Osa on tässä muuttumattomia
(val), osa ei (var).
Olioon liitetyt funktiot eli metodit alkavat def-sanalla.
Olion metodia suoritettaessa this viittaa olioon itseensä. Esimerkiksi
tässä lausekkeen this.nimi arvo on olion oman nimi-muuttujan arvo.
this-sana ei kuitenkaan ole aina pakollinen; ks. luku 2.2.
tyontekija.korotaPalkkaa(1.1)tyontekija.ikaVuonna(2024)res20: Int = 59
"Pakkausoliot" ja import
Yksittäisoliota voi käyttää "pakkauksen kaltaisesti": varastona,
josta voi importata käyttöön työkaluja kuten funktioita, luokkia
ja toisia olioita (luku 5.3). Esimerkki:
Nämä funktiot on määritelty kokeilu-nimisen
olion sisään, jota on tarkoitus käyttää
"pakkausmaisesti".
kokeilu-olio on määritelty omat-nimisen
pakkauksen yhteyteen. Se on tavallaan omat-pakkauksen alipakkaus.
Tuo koodi tulee tallentaa omat-nimisessä kansiossa tiedostoon,
jonka nimi voi olla esimerkiksi kokeilu.scala. Oliosta voi nyt
ottaa työkaluja käyttöön tavallisella import-käskyllä:
import omat.kokeilu.*tuplaa(10)res21: Int = 20
triplaa(10)res22: Int = 30
Mitään teknisesti poikkeavaa kokeilu-yksittäisoliossa ei
verrattuna muihin yksittäisolioihin ole. Tässä vain päätimme
käyttää sitä pakkauksen kaltaisesti ja importata sen osia.
Sovelluksen käynnistäminen
O1-kurssilla esitellään kaksi tapaa määritellä, miten sovellusohjelma lähtee liikkeelle:
käynnistysfunktio ja käynnistysolio. Ensimmäinen tapa on tietyin tavoin joustavampi ja
monissa lähteissä suositellumpi, mutta tämän kurssin ohjelmissa kumpikin tapa toimii ihan
hyvin; käytämme joissakin ohjelmissa yhtä ja toisissa toista.
Vaihtoehto 1: käynnistysfunktio
Käynnistysfunktio (luku 2.7) on funktio, joka toimii sovelluksen
käynnistyskohtana:
@maindefkaynnistaTestiohjelma()=println("Nämä rivit suoritetaan, kun sovellus ajetaan.")println("Tässä yksinkertaisessa sovelluksessa ei muuta olekaan kuin nämä tulostuskäskyt.")println("Monimutkaisemmassa ohjelmassa täältä käynnistettäisiin muita ohjelman osia.")
@main merkitsee, että tämä muuten ihan tavallinen funktio
toimii käynnistysfunktiona.
@main-merkintää ei voi lisätä mille tahansa funktiolle, vaan kyseessä on oltava
joko suoraan pakkauksen sisään deffattu funktio tai yksittäisolion metodi.
(Käynnistysfunktioksi ei voi merkitä luokan metodia, koska luokasta pitää olla
ilmentymä ennen kuin sellaista metodia voi kutsua, eikä ilmentymää käynnistyshetkellä ole.)
Vaihtoehto 2: käynnistysolio
Käynnistysolio (luku 2.7) on yksittäisolio, joka toimii sovelluksen
käynnistyskohtana:
objectTestiohjelmaextendsApp:println("Nämä rivit suoritetaan, kun sovellus ajetaan.")println("Tässä yksinkertaisessa sovelluksessa ei muuta olekaan kuin nämä tulostuskäskyt.")println("Monimutkaisemmassa ohjelmassa täältä käynnistettäisiin muita ohjelman osia.")
extendsApp määrittelee, että kyseessä on käynnistysolio.
(Tarkemmin sanoen oliosta tulee App-piirreluokan
yksittäistapaus; ks. Piirreluokat alempaa.)
Luokat (ja lisää olioista)
Luokan määrittely
Tässä luvusta 2.4 esimerkkiluokka, jolla voi kuvata keskenään erilaisia työntekijöitä.
Kukin tämän luokan ilmentymä on oma Tyontekija-tyyppinen olionsa, jolla on omat tiedot:
classTyontekija(annettuNimi:String,annettuSyntymavuosi:Int,annettuPalkka:Double):varnimi=annettuNimivalsyntynyt=annettuSyntymavuosivarkkpalkka=annettuPalkkavartyoaika=1.0defikaVuonna(vuosi:Int)=vuosi-this.syntynyt// Jne. Muita metodeita.endTyontekija
Avainsana class luokan nimen edessä. Kuten yksittäisolioiden
myös luokkien määrittelyyn tulee kaksoispiste, sisennykset
ja (vapaaehtoinen) loppumerkki.
Luontiparametrit: kun tästä luokasta luodaan ilmentymä,
on annettava nimi, syntymävuosi ja palkka.
Luokan sisään kirjoitettu koodi metodien määrittelyt poislukien
toimii konstruktorina: se suoritetaan kunkin
uuden ilmentymän alustamiseksi. Tässä alustetaan useimmat olion
ilmentymämuuttujat luontiparametrien arvoilla sekä asetetaan
työajaksi luontiparametreista riippumatta aina alkuarvo 1.0.
Metodit määritellään aivan kuin yksittäisolioille. Sana this
viittaa luokankin koodissa siihen olioon, jolle metodia
kutsutaan. Esimerkiksi tässä lasketaan ikä juuri metodia
suorittavan ilmentymän syntynyt-muuttujan arvon perusteella.
Yllä olevan luokkamäärittelyn voi kirjoittaa lyhyemminkin (luku 2.4):
classTyontekija(varnimi:String,valsyntynyt:Int,varkkpalkka:Double):vartyoaika=1.0defikaVuonna(vuosi:Int)=vuosi-this.syntynyt// Jne. Muita metodeita.endTyontekija
Tässä hyödynnetään mahdollisuutta määritellä kerralla
sekä ilmentymämuuttujat että ne luontiparametrit,
jotka määräävät noiden ilmentymämuuttujien (alku)arvot.
Neljäs ilmentymämuuttujista ei saa arvoaan suoraan
luontiparametrista. Se määritellään erikseen.
Ilmentymien luominen ja käyttö
Yllä kuvattua Tyontekija-luokkaa voi käyttää näin (luku 2.3):
Luodaan ilmentymä kirjoittamalla luokan nimi ja sulkeissa
arvot luontiparametreille. (Jos luontiparametreja ei olisi,
kirjoitettaisiin nimen perään tyhjät kaarisulkeet.)
Lausekkeen arvo on viittaus uuteen olioon, joka on luokan
ilmentymä.
Muuttujaan voi tallentaa viittauksen luotuun olioon. Tällöin oliota voi käyttää helposti
muuttujan nimen kautta.
val juuriPalkattu = Tyontekija("Teija Tonkeli", 1985, 3000)juuriPalkattu: o1.Tyontekija = o1.Tyontekija@704234
juuriPalkattu.ikaVuonna(2024)res24: Int = 39
println(juuriPalkattu.kuvaus)Teija Tonkeli (s. 1985), palkka 1.0 * 3000.0 euroa
Luokan räätälöinti yksittäisolioksi
Voidaan määritellä yksittäisolio, joka on luokan ilmentymien kaltainen mutta
poikkeaa niistä tavalla tai toisella.
Tavallinen henkilö ei osaa lentää. Seuraava yksittäinen henkilöolio kuitenkin osaa.
Lisäksi yksi sen metodeista toimii toisin kuin muiden henkilöolioiden:
Määrittelemme yksittäisolion tavalliseen tapaan, mutta ilmoitamme
sen olevan eräänlainen henkilö. Räätälöimme tämän "erikoishenkilön"
seuraavilla riveillä omanlaisekseen.
Tällä oliolla henkilöllä on lisämetodi lenna.
Toinen oliokohtaisista metodeista korvaa luokan yleisemmän
määrittelyn, mikä tulee merkitä override-sanalla.
Yleiset perustyypit kuten Int, Double ja String ovat myös luokkia ja niiden toiminnot
metodeita (luku 5.2). Esimerkiksi yhteenlaskun voi tehdä pistenotaatiota ja
metodia nimeltä + käyttäen kuten tässä:
1.+(1)res25: Int = 2
Tutumpi lauseke 1+1 toimii myös, koska yksiparametrista metodia voi kutsua myös
operaattorinotaatiolla, jossa piste ja sulkeet jätetään pois. Sama käy myös itse laadituille
metodeille:
juuriPalkattu ikaVuonna 2024res26: Int = 39
Operaattorinotaatiota kannattaa kuitenkin yleensä välttää, jos metodin nimi on sanallinen
eikä operaattorimainen symboli kuten +.
Kuvankäsittelyä O1:n kirjastolla
Kurssin oheismoduuli O1Library on ohjelmakirjasto, jonka pakkaus o1 tarjoaa työkaluja
mm. graafiseen ohjelmointiin ja on runsaassa käytössä kurssilla.
O1Libraryn työkaluja esitellään kurssimateriaalin luvuissa ja tuon moduulin dokumentaatiossa. Alla on lyhyt yhteenveto tärkeimmistä.
Värit: o1.Color
Värejä kuvaa luokka Color (luku 1.3). Useita tämän luokan ilmentymiä on määritelty
vakioiksio1-pakkaukseen:
import o1.*Redres27: Color = Red
RoyalBlueres28: Color = RoyalBlue
Värisävyn voi määrittää myös RGB-komponenttien yhdistelmänä (luku 5.4). Kukin
komponentti on luku väliltä 0–255. Tässä luodaan marjapuuromainen sävy, jossa on
erityisen paljon punaista ja sinistä:
val omaSavy = Color(220, 150, 220)omaSavy: Color = Color(220, 150, 220)
Värin komponentteja voi tutkia:
omaSavy.redres29: Int = 220
RoyalBlue.blueres30: Int = 225
Värillä on R-, G- ja B-komponenttien lisäksi myös ns. alfakanava eli läpinäkymättömyys.
Red.opacityres31: Int = 255
val lapikuultavaPunainen = Color(255, 0, 0, 100)lapikuultavaPunainen: Color = Color(255, 0, 0, opacity: 100)
Väri, jonka opacity on vain 100 on varsin läpikuultava. Nolla
olisi tarkoittanut täysin läpinäkyvää ja 255 läpinäkymätöntä.
Ellei toisin määritellä, väri on täysin läpinäkymätön.
Sijainnit: o1.Pos
Sijainteja kaksiulotteisessa koordinaatistossa kuvaa luokka o1.Pos (luku 2.5).
val eroXSuunnassa = toka.xDiff(eka)eroXSuunnassa: Double = 15.5
val eroYSuunnassa = toka.yDiff(eka)eroYSuunnassa: Double = -10.0
val etaisyys = eka.distance(toka)etaisyys: Double = 18.445866745696716
val vahanOikealle = eka.addX(1.5)vahanOikealle: Pos = (17.0,10.0)
val molempiaSaadetty = vahanOikealle.add(10, -5)molempiaSaadetty: Pos = (27.0,5.0)
Mikään mainituista metodeista ei muuta olemassa olevaa Pos-oliota, kuten ei mikään
muukaan metodi. Esimerkiksi add-metodi ei muuta vanhaa sijaintioliota vaan tuottaa
uuden. Pos-oliot ovat tilaltaan muuttumattomia.
Kuvan voi ladata tiedostosta tai nettiosoitteesta (luku 1.3):
val moduulinTiedostostaLadattu = Pic("face.png")moduulinTiedostostaLadattu: Pic = face.png
val absoluuttisestaTiedostopolustaLadattu = Pic("d:/kurssi/GoodStuff/face.png")absoluuttisestaTiedostopolustaLadattu: Pic = d:/kurssi/GoodStuff/face.png
val netistaLadattu = Pic("https://en.wikipedia.org/static/images/project-logos/enwiki.png")netistaLadattu: Pic = https://en.wikipedia.org/static/images/project-logos/enwiki.png
Moduulin sisältä ladattu kuvatiedosto voi olla
samassa moduulissa lataavan koodin kanssa tai
O1Library-moduulin pics-kansiossa (tai jossain
muualla ohjelman luokkapolussa).
Kuvan saa näkyviin omaan ikkunaansa funktiolla o1.show tai kuvaolion samannimisellä
metodilla:
show(netistaLadattu)netistaLadattu.show()
Tarjolla on useita funktioita, joilla voi luoda geometrista kuviota esittäviä kuvia.
Tässä muutama esimerkki:
val ympyra = circle(250, Blue)ympyra: Pic = circle-shape
val kaide = rectangle(200, 300, Green)kaide: Pic = rectangle-shape
val tasakylkinen = triangle(150, 200, Orange)tasakylkinen: Pic = triangle-shape
val tahti = star(100, Black)tahti: Pic = star-shape
val soikio = ellipse(200, 300, Pink)soikio: Pic = ellipse-shape
Kurssilla useimmin käytetyt Pic-metodit asemoivat kuvia päällekkäin, vierekkäin tms.
(luku 2.3) Esimerkkejä:
val ympyraKaiteenVasemmallaPuolella = ympyra.leftOf(kaide)ympyraKaiteenVasemmallaPuolella: Pic = combined pic
val ympyraKaiteenAlla = ympyra.below(kaide)ympyraKaiteenAlla: Pic = combined pic
val ympyraKaiteenEdessa = ympyra.onto(kaide)ympyraKaiteenEdessa = combined pic
Tällaiset metodit eivät muokkaa olemassa olevia kuvia vaan tuottavat uusia Pic-olioita.
Kuvan voi asetella taustaa vasten tiettyyn sijaintiin (luku 2.5):
val pikkukuva = rectangle(10, 20, Black)pikkukuva: Pic = rectangle-shape
val pikkukuvaTaustaaVasten = kaide.place(pikkukuva, Pos(30, 80))pikkukuvaTaustaaVasten: Pic = combined pic
val ympyrakinSamaanKuvaan = pikkukuvaTaustaaVasten.place(ympyra, Pos(150, 150))ympyrakinSamaanKuvaan: Pic = combined pic
Tässä annetaan place-metodille sijoituskohta koordinaattiparina
(jossa x kasvaa oikealle, y alas.) Asemoitavan kuvan keskikohta
tulee noihin koordinaatteihin.
Suurehko ympyrä ei mahdu kokonaisuudessaan taustana toimivaa
suorakaidetta vasten. Tällöin place jättää ylijäämän pois
näkyvistä.
Tässä osittainen luettelo kuvaolioiden toiminnoista:
Sijoittelu vierekkäin ja allekkain: above, below,
leftOf, rightOf (luku 2.3).
Sijoittelu eteen ja taakse: onto, against, place
(luvut 2.3 ja 2.5).
Asemointi ankkureilla (esim. "tämän kuvan vasen yläkulma
tuon kuvan yläreunan keskelle"): ks. luvun 2.5 loppu.
Operaattorit && ja || evaluoidaan väljästi: jos vasemmanpuoleinen osalauseke
riittää määräämään koko loogisen lausekkeen totuusarvon, niin oikeanpuoleista ei
evaluoida lainkaan:
Tämän esimerkkifunktion paluuarvo on tyyppiä Option[Int] (luku 4.3). Funktio
palauttaa joko jakolaskun lopputuloksen käärittynä Some-olioon tai None, jos
osamäärää ei voi määrittää:
Tässä Option-oliota käytetään merkkijonotyypin String kanssa:
var testi: Option[String] = Nonetesti: Option[String] = None
testi = Some("melkein kaikki ovat somessa")testi: Option[String] = Some(melkein kaikki ovat somessa)
Option[String]-tyyppinen muuttuja voi viitata joko
None-yksittäisolioon — jolloin merkkijonoa ei ole —
tai Some-olioon, jonka sisään on kääritty String-arvo.
Hakasulkeisiin kirjoitetaan tyyppiparametri eli kääreen
sisällä mahdollisesti olevan arvon tyyppi.
Jos olisimme jättäneet tyyppimerkinnän
tästä pois, ei sijoituskäskystä voisi päätellä, millainen
Some-arvo testi-muuttujaan voitaisiin sijoittaa.
Option-tyypin sijaan voisi käyttää null-arvoa, mutta se ei ole useimmissa
Scala-ohjelmissa lainkaan kannatettavaa (luku 4.3).
Option-olioiden metodeita
Metodeilla isDefined ja isEmpty voi tutkia, onko kääre tyhjä:
Metodi getOrElse palauttaa arvon kääreen sisältä. Sille annetaan parametrilauseke, joka
määrää, mitä metodi palauttaa kääreen ollessa tyhjä:
kaarittyLuku.getOrElse(12345)res57: Int = 100
None.getOrElse(12345)res58: Int = 12345
Samantapainen metodi orElse palauttaa Option-olion itsensä, jos kyseessä on Some,
tai sille annetun parametrilausekkeen arvon, jos kyseessä on None. Se siis eroaa
getOrElsestä sikäli, ettei se pura käärettä:
Option-tyyppisten arvojen käsittelyyn sopivat myös
match-valintakäsky, josta kerrotaan lisää tuossa alla, ja
monet korkeamman asteen metodit, joista on kooste alempana
kohdassa Option kokoelmatyyppinä.
Valintakäskyt if ja match
if-perusteet
if-käsky (luku 3.4) valitsee kahdesta vaihtoehdosta evaluoimalla ehtolausekkeen:
val luku = 100luku: Int = 100
if luku > 0 then luku * 2 else 10res61: Int = 200
if luku < 0 then luku * 2 else 10res62: Int = 10
Ehtolauseke kirjoitetaan if ja then sanojen väliin.
Ehtona voi olla mikä tahansa Boolean-tyyppinen lauseke.
if-käskyllä muodostettua lauseketta voi käyttää muiden lausekkeiden tapaan esimerkiksi
muuttujaan sijoitettaessa tai funktion parametrina:
val valinnanTulos = if luku > 100 then 10 else 20valinnanTulos: Int = 20
println(if luku > 100 then 10 else 20)20
Jos valinnaisessa osassa on peräkkäisiä käskyjä, rivitetään ja sisennetään (mikä
on muutenkin tapana, jos käsky on vaikutuksellinen; ks.
tyyliopas):
if luku > 0 then
println("Luku on positiivinen.")
println("Tarkemmin sanoen se on: " + luku)
else
println("Kyseessä ei ole positiivinen luku.")Luku on positiivinen.
Tarkemmin sanoen se on: 100
Jos riittää, että ehdon ollessa totta suoritetaan tietty toimenpide ja muuten ei tehdä mitään,
niin else-osion voi jättää pois:
if luku != 0 then
println("Osamäärä on: " + 1000 / luku)
println("Loppu")Osamäärä on: 10
Loppu.
Viimeinen tulostuskäsky ei kuulu if-käskyyn vaan on sen perässä.
Tämä koodinpätkä siis tulostaa lopuksi "Loppu" riippumatta siitä,
onko luku-muuttujan arvo nolla vai ei. Jos olisi ollut, ei tämä
koodi muuta olisi muuta tulostanutkaan.
if-käskyjen yhdisteleminen
Yksi tapa valita useasta vaihtoehdosta on kirjoittaa if-käsky toisen if-käskyn
else-osioksi:
val luku = 100luku: Int = 100
if luku < 0 then "negatiivinen" else if luku > 0 then "positiivinen" else "nolla"res63: String = positiivinen
if luku < 0 then
println("Luku on negatiivinen.")
else if luku > 0 then
println("Luku on positiivinen.")
else
println("Luku on nolla.")Luku on positiivinen.
if-käskyt voi muutenkin kirjoittaa sisäkkäin:
if luku > 0 then
println("On positiivinen.")
if luku > 1000 then
println("On yli tuhat.")
else
println("On positiivinen muttei yli tuhat.")
On positiivinen.
On positiivinen muttei yli tuhat.
Tässä else-sana on sisennetty samalle tasolle kuin sisempi
if-käsky ja kytkeytyy näin siihen. Tuo else-osa suoritettiin
siksi, että ulompi ehto toteutui mutta sisempi ei.
Ulommassa käskyssä ei tässä ole else-osiota ollenkaan. Jos luku
ei olisi ollut positiivinen, ei olisi tulostunut mitään.
Seuraavassa esimerkissä, joka on sisennetty toisin, sisemmällä if-käskyllä ei ole
else-osiota, mutta ulommalla on:
if luku > 0 then
println("On positiivinen.")
if luku > 1000 then
println("On yli tuhat.")
else
println("On nolla tai negatiivinen.")On positiivinen.
Näitäkin esimerkkejä on selostettu tarkemmin luvussa 3.4. Luvun 3.5 lopussa taas
on esimerkkejä virhetilanteista, joita voi syntyä, kun käyttää if-käskyä funktion
paluuarvon määrittämiseen.
Loppumerkinnät valintakäskyissä
Halutessasi voit kirjoittaa loppumerkinendif päättääksesi valintakäskyn.
Joissain mutkikkaissa tilanteissa tämä saattaa selkiyttää koodia, mutta hyvin kirjoitetussa
koodissa nämä loppumerkit ovat harvoin tarpeen (ks. tyyliopas).
Esimerkki-if loppumerkeillä:
ifluku>0thenprintln("On positiivinen.")ifluku>1000thenprintln("On yli tuhat.")endifelseprintln("On nolla tai negatiivinen.")endif
match-valintakäsky
match-käsky (luvut 4.3 ja 4.4) määrittää lausekkeen arvon ja valitsee luetelluista
vaihtoehdoista ensimmäisen sellaisen, joka vastaa saatua arvoa. Käskyn yleinen muoto on:
lauseke L match
case hahmo A => koodia, joka suoritetaan, jos L:n arvo sopii hahmoon A
case hahmo B => koodia, joka suoritetaan, jos L:n arvo sopii hahmoon B (muttei A:han)
case hahmo C => koodia, joka suoritetaan, jos L:n arvo sopii hahmoon C (muttei A:han tai B:hen)Ja niin edelleen. (Tyypillisesti katetaan kaikki mahdolliset tapaukset.)end match
... ns. hahmoihin, joilla kuvataan erilaisia tapauksia.
Loppuun saa kirjoittaa loppumerkin, jos kokee sen
selkiyttävän koodia.
Konkreettinen koodiesimerkki:
valkuutionKuvaus=luku*luku*lukumatchcase0=>"luku on nolla ja niin sen kuutiokin"case1000=>"kympistä tulee tuhat"casemuuKuutio=>"luku "+luku+", jonka kuutio on "+muuKuutio
Tutkitaan kertolaskulausekkeen arvoa suoritettavan koodin
valitsemikseksi.
Lausekkeen arvoa yritetään sovittaa järjestyksessä hahmoihin,
joita on tässä kolme, kunnes sopiva löytyy.
Hahmona voi käyttää literaalia; tässä on käytetty Int-literaaleja. Näistä tapauksista ensimmäinen valitaan, jos
luvun kuutio oli nolla, toinen jos se oli tuhat.
Hahmoksi voi myös kirjoittaa uuden muuttujanimen; tässä on
valittu nimi muuKuutio. Tällainen tapaus sopii yhteen minkä
tahansa arvon kanssa ja tulee siis tässä valituksi mikäli
kuutio ei ollut nolla eikä tuhat.
Kun tällainen tapaus kohdataan, syntyy uusi paikallinen
muuttuja, jonka arvoksi tapaukseen "osunut" arvo tallentuu.
Muuttujan nimeä voi käyttää tapauksen koodissa.
Eräs käyttö match-käskylle on arvon poimiminen Option-kääreestä:
// Tätä käytetään alla match-esimerkissä.defosamaara(jaettava:Int,jakaja:Int)=ifjakaja==0thenNoneelseSome(jaettava/jakaja)
osamaara(ekaLuku,tokaLuku)matchcaseSome(tulos)=>"Tulos on: "+tuloscaseNone=>"Tulosta ei ole."
Hahmossa määritellään rakenne: jos kyseessä on Some, niin sen
sisällä on jokin arvo. Tuo arvo "puretaan esiin" ja poimitaan
muuttujaan tulos.
(Tosin Option-luokan yhteydessä korkeamman asteen metodit ovat usein vähintään yhtä
hyvä vaihtoehto kuin match; ks. luku 8.3 ja Option kokoelmatyyppinä alempana.)
Alla on vielä yksi esimerkki, joka esittelee eräitä match-käskyn ominaisuuksia.
Esimerkki on luvusta 4.4, josta löytyy enemmänkin vapaaehtoista materiaalia tästä
monipuolisesta käskystä.
def kokeilu(jonkinlainenArvo: Matchable) =
jonkinlainenArvo match
case jono: String => "kyseessä on merkkijono " + jono
case luku: Intif luku > 0 => "kyseessä on positiivinen kokonaisluku " + luku
case luku: Int => "kyseessä on ei-positiivinen kokonaisluku " + luku
case vektori: Vector[?] => "kyseessä on vektori, jossa on " + vektori.size + " alkiota"
case _ => "kyseessä on jokin sekalainen arvo"
Esimerkkifunktiomme parametrityyppi on Matchable, mikä tarkoittaa
että sille voi antaa minkä tahansa "match-kelpoisen" arvon
parametriksi.
Hahmoihin on kirjattu tietotyyppi. Kukin näistä hahmoista tärppää
vain, jos tutkittava arvo on kyseistä tyyppiä.
Lisäehto rajaa tapausta: kyseinen tapaus valitaan vain,
jos kyseessä on nollaa suurempi kokonaisluku. (match-käskyn
osana käytetään samaa if-sanaa kuin erillisessä if-valintakäskyssäkin.)
Alaviivahahmo sopii mihin tahansa arvoon ja tulee valituksi, jos
mikään edellisistä ei tullut. Tähän olisi voinut myös kirjoittaa
uuden muuttujan nimen (kuten ylempänä tehtiinkin), mutta jos
muuttujalle ei ole käyttöä, pelkkä alaviiva kelpaa.
Käyttöalue ja näkyvyysmääreet
Ohjelman osien — muuttujien, funktioiden, luokkien tai yksittäisolioiden — sallittu
käyttöalue eli skooppi määräytyy sen mukaan, missä tuo osa on määritelty (luku 5.6).
Lisäksi käyttöaluetta voi säädellä näkyvyysmääreillä kuten private (luku 3.2).
Julkisen ilmentymämuuttujan käyttöalueeseen sisältyy koko luokka.
Lisäksi siihen voi viitata luokan ulkopuolelta: olio.julkinenIlmentymamuuttuja.
Samoin julkista metodia voi käyttää mistä tahansa päin ohjelmaa.
Ilmentymämuuttuja tai metodi on julkinen ellei toisin määritellä.
Yksityisen ilmentymämuuttujan ja yksityisen metodin käyttöalue on
koko kyseinen luokka.
Tämä luokka itse on julkinen, joten sitä voi käyttää muualta
ohjelmasta vapaasti.
Funktioiden rungot ovat aina yksityisiä. Niiden sisässä oleviin
määrittelyihin ei pääse käsiksi mistään kyseisen funktion ulkopuolelta.
Paikallisten muuttujien käyttöalue
Kun siirrät hiiren kursorin laatikoiden päälle, korostuvat mainittujen
muuttujien käyttöalueet.
Parametrimuuttuja kuten parametri on määritelty koko kyseisen
funktion ohjelmakoodissa. Sitä voi käyttää sieltä mistä vain.
Funktion koodissa uloimmalla tasolla määritelty muuttuja, kuten
paikallinen, on käytettävissä määrittelykohdasta alkaen metodin
koodin loppuun.
Samoin toinenPaikallinen.
Kun ulompi käsky sisältää muuttujamäärittelyn, niin määritelty
muuttuja on käytettävissä vain kyseisen ulomman käskyn sisällä.
Esimerkiksi tässä muuttuja vainIffissa on määritelty vain
if-käskyn sisällä.
Kumppaniolio on yksittäisolio, jolle annetaan prikulleen sama
nimi kuin luokalle itselleen ja jonka määrittely kirjoitetaan
samaan tiedostoon.
Kumppaniolioon voi kirjata luokkaan yleisellä tasolla liittyviä
tietoja (kuten tämä ilmentymälaskuri) tai metodeita. Muuttujasta
montakoLuotu on muistissa vain yksi kopio, koska kumppanioliotakin
on vain yksi. Vrt. asiakasolioiden nimet ja numerot, joita on yksi
per asiakasolio.
Asiakas-luokka ja sen kumppaniolio ovat "kavereita", joilla ei
ole salaisuuksia. Ne pääsevät poikkeuksellisesti käsiksi myös
toistensa yksityisiin tietoihin.
Parit ja muut monikot
Monikko on tilaltaan muuttumaton rakenne, joka muodostuu kahdesta tai useammasta
keskenään mahdollisesti eri tyyppisestä arvosta (luku 9.2). Monikon voi määritellä
käyttäen sulkeita ja pilkkuja:
val nelikko = ("Tässä monikossa on neljä erilaista jäsentä.", 100, 3.14159, false)nelikko: (String, Int, Double, Boolean) = (Tässä monikossa on neljä erilaista jäsentä.,100,3.14159,false)
nelikko(0)res64: String = Tässä monikossa on neljä erilaista jäsentä.
nelikko(2)res65: Double = 3.14159
Tämän nelikon jäsenet ovat keskenään erityyppisiä.
Pari on yleinen erikoistapaus monikosta. Tässä parissa molemmat jäsenet ovat merkkijonoja:
val pari = ("laama", "llama")pari: (String, String) = (laama,llama)
Monikon osat voi sijoittaa useaan muuttujaan kerralla:
Parin voi määritellä suljemerkinnän sijaan myös näin:
val samanlainen = "laama" -> "llama"samanlainen: (String, String) = (laama,llama)
Viimeksi mainittua merkintätapaa käytetään varsinkin hakurakenteiden yhteydessä, kun
parit toimivat avain–arvo-pareina; ks. kohta Hakurakenteet (Map).
Toinen indeksointitapa
On vaihtoehtoinenkin merkintä, jolla monikosta voi — ja vanhoissa
Scala-versioissa oli tarpeenkin — poimia jäseniä. Huomaa alaviivat
ja ykkösestä alkava indeksointi.
val nelikko = ("Tässä monikossa on neljä erilaista jäsentä.", 100, 3.14159, false)nelikko: (String, Int, Double, Boolean) = (Tässä monikossa on neljä erilaista jäsentä.,100,3.14159,false)
nelikko._1res66: String = Tässä monikossa on neljä erilaista jäsentä.
nelikko._3res67: Double = 3.14159
Monikoiden erikoispiirre on, että Scala osaa muodostaa niitä automaattisesti, jos
parametriksi välitetään "irrallisia" arvoja mutta kaivataan monikkoa (luku 9.2):
def absDiff(pairOfNumbers: (Int, Int)) =
(pairOfNumbers(0) - pairOfNumbers(1)).absdef absDiff(pairOfNumbers: (Int, Int)): Int
absDiff((-300, 100))res68: Int = 400
absDiff(-300, 100)res69: Int = 400
Funktio vastaanottaa parin.
Sitä kutsuessa voi antaa joko parin tai kaksi irrallista arvoa,
joista Scala automaattisesti muodostaa parin (ns. auto-tupling).
Merkkijonon pituuden eli koon voi selvittää kummalla vain seuraavista tavoista:
val jono = "Olavi Eerikinpoika Stålarm"jono: String = Olavi Eerikinpoika Stålarm
jono.lengthres70: Int = 26
jono.sizeres71: Int = 26
Kirjainkokojen muokkausta:
val viesti = "five hours of Coding can save 15 minutes of Planning"viesti: String = five hours of Coding can save 15 minutes of Planning
viesti.toUpperCaseres72: String = FIVE HOURS OF CODING CAN SAVE 15 MINUTES OF PLANNING
viesti.toLowerCaseres73: String = five hours of coding can save 15 minutes of planning
viesti.capitalizeres74: String = Five hours of Coding can save 15 minutes of Planning
"abc" < "bcd"res87: Boolean = true
"abc" >= "bcd"res88: Boolean = false
"abc".compare("bcd")res89: Int = -1"bcd".compare("abc")res90: Int = 1"abc".compare("abc")res91: Int = 0"abc".compare("ABC")res92: Int = 32"abc".compareToIgnoreCase("ABC")res93: Int = 0
Paluuarvon etumerkki kertoo vertailun tuloksen.
Arvojen yhdistäminen osaksi merkkijonoa
Lausekkeiden arvoja voi upottaa merkkijonoon (luku 1.4):
val luku = 10luku: Int = 10
val upotuksin = s"Muuttujassa on $luku, ja sitä yhtä suurempi luku on ${luku + 1}."upotuksin: String = Muuttujassa on 10, ja sitä yhtä suurempi luku on 11.
Alkuun s-kirjain.
Dollarimerkin perään voi kirjoittaa muuttujan nimen.
Muuttujan arvo upotetaan merkkijonoon.
Lauseke rajataan tarvittaessa aaltosulkeilla.
Plus-operaattorillakin voi yhdistää merkkijonon perään erilaisia arvoja, kuten tässä
kokonaislukuja:
val samaPlussalla = "Muuttujassa on " + luku + ", ja sitä yhtä suurempi luku on " + (luku + 1) + "."samaPlussalla: String = Muuttujassa on 10, ja sitä yhtä suurempi luku on 11.
"luku on " + lukures94: String = luku on 10
"kuor" + 100res95: String = kuor100
Tuossa yhdistettiin lukuja nimenomaan merkkijonojen perään. Samaa ei kuitenkaan tule
tehdä toisin päin — eli muuntyyppinen arvo ennen plussaa:
luku + " on talletettu muuttujaan"luku + " on talletettu muuttujaan" ^warning: method + in class Double is deprecated (since 2.13.0):Adding a number and a String is deprecated. Use the string interpolation `s"$num$str"`
Erikoismerkit merkkijonoissa
Erikoismerkkejä voi kirjoittaa merkkijonoon kenoviivan avulla (luku 5.2):
val rivinvaihto = "\n"rivinvaihto: String =
"
"
println("eka rivi\ntoka rivi")eka rivi
toka rivi
val sarkainEliTabulaattori = "eka\ttoka\tkolmas"sarkainEliTabulaattori: String = eka toka kolmas
"tässä lainausmerkki \" ja toinenkin \""res96: String = tässä lainausmerkki " ja toinenkin "
"tässä kenoviiva \\ ja toinenkin \\"res97: String = tässä kenoviiva \ ja toinenkin \
Merkkijonoliteraaliin, joka on rajattu kummastakin päästä kolmella lainausmerkillä yhden
sijaan, voi kirjoittaa erikoismerkkejä sellaisenaan:
"""Tässä merkkijonossa on lainausmerkki " ja
kenoviiva \ kahdella eri rivillä."""res98: String =
Tässä merkkijonossa on lainausmerkki " ja
kenoviiva \ kahdella eri rivillä.
toString-metodi
Kaikilla Scala-olioilla on toString-niminen parametriton metodi, joka palauttaa
merkkijonokuvauksen oliosta:
toString-metodi on myös olioilla, jotka ovat sovellusohjelmoijan itse määrittelemää
tyyppiä (koska tuo metodi periytyy niille; ks. Periytyminen):
class Kokeilu(val muuttuja: Int)// defined class Kokeilu
val kokeiluolio = Kokeilu(10)kokeiluolio: Kokeilu = Kokeilu@56181kokeiluolio.toStringres101: String = Kokeilu@56181kokeiluoliores102: Kokeilu = Kokeilu@56181
Oletusarvoinen toString-metodi tuottaa tämän näköisen
merkkijonon (luku 2.5).
REPL käyttää juuri toString-metodia kuvatakseen
olioita. Yllä siis kutsuttiin toString-metodia yhteensä
kolmeen kertaan.
Oletusarvoisen toString-metodin voi korvata (luku 2.5 ja ks. Periytyminen):
class Testi(val muuttuja: Int):
override def toString = "OLIOLLA ON ARVO " + this.muuttuja// defined class Testi
val testiolio = Testi(11)testiolio: Testi = OLIOLLA ON ARVO 11
toString-metodi tulee kutsutuksi ilman erillistä käskyä, kun olio määrätään
tulostettavaksi tai yhdistetään merkkijonoon:
println(testiolio)OLIOLLA ON ARVO 11
testiolio + "!!!"res103: String = OLIOLLA ON ARVO 11!!!
s"Testiolion toString-paluuarvo upotetaan tähän väliin $testiolio ja täältä jatkuu."res104: String = Testiolion toString-paluuarvo upotetaan tähän väliin OLIOLLA ON ARVO 11 ja täältä jatkuu.
Kokoelmien alkeita
Puskurien peruskäyttöä
Puskurit ovat eräänlaisia alkiokokoelmia (luvut 1.5 ja 4.2). Puskureita kuvaava
tyyppi Buffer löytyy pakkauksesta scala.collection.mutable:
val tanneVoiLisataLukuja = Buffer[Double]()tanneVoiLisataLukuja: Buffer[Double] = ArrayBuffer()
Tyyppiparametrilla (luku 1.5) voi kirjata, millaisia alkioita
puskuriin varastoidaan. Tämä on erityisen tarpeellista silloin,
kun haluttua alkioiden tyyppiä ei voi päätellä, kuten tässä
tyhjää puskuria luodessa.
Puskurissa on nolla tai useampia alkioita järjestyksessä, kukin omalla indeksillään.
Indeksit alkavat nollasta, eivät ykkösestä.
Yksittäisen alkion voi katsoa indeksin perusteella näin:
lukuja(0)res106: Int = 12
lukuja(3)res107: Int = 7
Nämä ovat itse asiassa lyhennysmerkintöjä, jotka vastaavat näitä puskuriolion apply-metodin kutsuja (luku 5.3):
lukuja.apply(0)res108: Int = 12
lukuja.apply(3)res109: Int = 7
Metodi lift on samaa sukua. Se palauttaa tuloksen Option-tyyppisenä
eikä kaadu ajonaikaiseen virheeseen indeksin ollessa epäkelpo:
Kokoelmatyyppejä: puskurit, vektorit, laiskalistat ja muut
Kokoelmatyyppejä on monia. Ohjelmointi 1 -kurssilla käytämme aluksi enimmäkseen puskureita
(Buffer) ja sitten kasvavassa määrin vektoreita (Vector). Myös muita kokoelmatyyppejä
tulee vastaan.
Sekä puskurissa että vektorissa on alkioita tietyssä järjestyksessä, kukin omalla
indeksillään. Näiden kokoelmatyyppien päällimmäiset erot ovat:
Puskuri on muuttuvatilainen kokoelma. Siihen voi lisätä alkioita,
jolloin sen koko muuttuu. Alkioita voi myös poistaa tai vaihtaa
toisiksi.
Vektori on muuttumaton kokoelma. Siihen varastoidaan heti luodessa
tietyt alkiot. Alkiot eivät koskaan vaihdu toisiksi, eikä vektorin
koko koskaan muutu.
Vektoreita käytetään pitkälti samaan tapaan kuin puskureita käytettiin yllä olevissa
esimerkeissä. Kuitenkaan siis vektoreita ei voi muuttaa. Vektorit ovat Scalassa
käytettävissä ilman import-käskyä.
Merkkijonot ovat merkkien kokoelmia. Siitä lisää tuossa pian alla.
Range-oliot ovat kokoelmia, joilla voi kuvata lukuvälejä.
Niistäkin on muutama esimerkki heti alla.
Taulukko (Array) on indekseihin perustuva perustietorakenne.
Taulukolla on vakiokoko (kuten vektorilla) mutta sen alkioita
voi vaihtaa toisiksi (kuten puskurin). Scalassa taulukoita
käytetään pitkälti vastaavilla käskyillä kuten vektoreita ja
puskureitakin (luku 12.1).
Listat (List) ovat kokoelmia, jotka sopivat erityisen hyvin
alkioiden käymiseen läpi järjestyksessä. Niitä käsitellään
lyhyesti luvussa 10.3.
Laiskalistat (LazyList) muistuttavat "tavallisia" listoja ja
sopivat alkioiden käsittelyyn järjestyksessä. Niiden erikoisuus
on, että laiskalistan alkiot muodostetaan ja varastoidaan muistiin
vain tarpeen mukaan. Laiskalistoista kertoo luku 7.2 ja niistä on
myös oma kappaleensa jäljempänä tällä sivulla.
Joukossa (Set) voi olla vain yksi kappale samanlaista alkiota.
Joukon alkioilla ei ole järjestystä samassa mielessä kuin yllä
mainituissa kokoelmatyypeissä. Joukkoja käsitellään lyhyesti
luvussa 10.1.
Pinot (stack) ovat kokoelmia, joista poistetaan aina viimeksi
lisätty alkio (luku 10.3).
IArray-kokoelmat ovat muuttumattomia ja muistuttavat siltä
osin vektoreita mutta tehokkuusominaisuuksiltaan taulukoita.
Niistä on lyhyt maininta luvussa 12.1.
Valintaan kokoelmatyyppien välillä vaikuttavat mm. ohjelmointiparadigma ja laadulliset
seikat kuten luettavuus ja tehokkuus.
Kokoelmia voi panna sisäkkäin niin, että ulomman kokoelman alkioina on viittauksia
toisiin kokoelmiin. Tätä esittelee mm. luku 6.1.
Merkkijonot kokoelmina
Merkkijono on kokoelma (ks. luvut 5.2 ja 5.6), ja sitä voi käsitellä pitkälti samoin
kuin vektoria. Merkkijonon alkioina on Char-arvoja.
val jono = "laama"jono: String = laama
jono(3)res122: Char = m
jono.lift(3)res123: Option[Char] = Some(m)
Tavalliset String-tyyppiset merkkijonot ovat muuttumattomia, ja esimerkiksi niiden
yhdisteleminen tuottaa uusia merkkijonoja eikä muokkaa alkuperäisiä. (Muuttuvatilaisestikin
merkkijonoja voi kuvata; luku 11.2.)
Lukuvälit: Range
Range-oliot ovat muuttumattomia kokoelmia, jotka kuvaavat lukuja tietyltä väliltä
(luvut 5.2 ja 5.6).
val kouluarvosanat = Range(4, 11)kouluarvosanat: Range = Range 4 until 11
kouluarvosanat(0)res124: Int = 4
kouluarvosanat(2)res125: Int = 6
Annettu alkukohta sisältyy väliin mutta loppukohta ei.
Range-olion voi luoda myös käyttämällä Int-olioiden until- tai to-metodia (luku 5.2).
Jälkimmäinen laskee mainitun loppukohdankin osaksi väliä. Nämä kaksi tuottavat keskenään
samanlaiset seitsemän luvun mittaiset lukuvälit.
val samaKuinEdella = 4 until 11samaKuinEdella: Range = Range 4 until 11
val samaTamakin = 4 to 10samaTamakin: Range = Range 4 to 10
Osan välille sijoittuvista luvuista voi ohittaa:
val jokaToinen = 1 to 10 by 2jokaToinen: Range = Range 1 to 10 by 2
val jokaKolmas = 1 to 10 by 3jokaKolmas: Range = Range 1 to 10 by 3
Tämän osion esimerkeissä käytetään kokoelmina merkkijonoja ja vektoreita. Kuitenkin kaikki
esitellyt metodit on määritelty myös puskureille, taulukoille ja usealle muulle kokoelmatyypille,
osin myös indeksittömille kokoelmille kuten hakurakenteille.
Löytyykö alkio kokoelmasta ja miltä indeksiltä (luku 4.2)?
val onkoKokoelmassaAlkioM = "laamamaa".contains('m')onkoKokoelmassaAlkioM: Boolean = true
val onkoKokoelmassaAlkioZ = "laamamaa".contains('z')onkoKokoelmassaAlkioZ: Boolean = false
val ekanAlkionAIndeksi = "laamamaa".indexOf('a')ekanAlkionAIndeksi: Int = 1
val vastaavaVektorille = Vector(10, 100, 100, -20).indexOf(-20)vastaavaVektorille: Int = 3
val negatiivinenKunEiLoydy = "laamamaa".indexOf('z')negatiivinenKunEiLoydy: Int = -1
val etsitaanAlkaenIndeksista3 = "laamamaa".indexOf('a', 3)etsitaanAlkaenIndeksista3: Int = 4
val etsitaanLopustaAlkuun = "laamamaa".lastIndexOf('a')etsitaanLopustaAlkuun: Int = 7
Alkioita alusta, lopusta ja keskeltä: head, tail, take, drop, slice ym.
Alkioiden poimiminen kokoelman alkupäästä (luvut 4.2 ja 5.2):
val ekaAlkio = "kruuna".headekaAlkio: Char = k
val ekaaEiOleJotenEiOnnistu = "".headjava.util.NoSuchElementException: next on empty iterator...val ekaKaarittyna = "kruuna".headOptionekaKaarittyna: Option[Char] = Some(k)
val puuttuvaEka = "".headOptionpuuttuvaEka: Option[Char] = None
val ekatKolmeAlkiota = "kruuna".take(3)ekatKolmeAlkiota: String = kru
val liianIsoEiHaittaa = "kruuna".take(1000)liianIsoEiHaittaa: String = kruuna
val kaikkiPaitsiVika = "kruuna".initkaikkiPaitsiVika: String = kruun
val kaikkiPaitsiKaksiLopusta = "kruuna".dropRight(2)kaikkiPaitsiKaksiLopusta: String = kruu
val toimiiEriKokoelmille = Vector(10, 100, 100, -20).dropRight(2)toimiiEriKokoelmille: Vector[Int] = Vector(10, 100)
Mikään äskeisistä metodeista ei muuta alkuperäistä kokoelmaa, vaan ne muodostavat
uuden kokoelman, jossa on osa alkuperäisen alkioista. Sama pätee loppupäästä poimiviin
käskyihin:
val kaikkiPaitsiEka = "klaava".tailkaikkiPaitsiEka: String = laava
val kaikkiPaitsiEkatKolme = "klaava".drop(3)kaikkiPaitsiEkatKolme: String = ava
val vainVika = "klaava".lastvainVika: Char = a
val vikaKaarittyna = "klaava".lastOptionvikaKaarittyna: Option[Char] = Some(a)
val lopustaKaksi = "klaava".takeRight(2)lopustaKaksi: String = va
Jakaminen kahteen osaan splitAt-metodilla (luku 9.2):
val teksti = "kruuna/klaava"teksti: String = kruuna/klaava
val pariJossaAlkuJaLoppu = teksti.splitAt(6)pariJossaAlkuJaLoppu: (String, String) = (kruuna,/klaava)
val samaMonimutkaisemmin = (teksti.take(6), teksti.drop(6))samaMonimutkaisemmin: (String, String) = (kruuna,/klaava)
Alkiot uuteen kokoelmaan: to, toVector, toSet jne.
Kokoelmatyyppiä voi vaihtaa kopioimalla olemassa olevan kokoelman sisällön uuteen
(luku 4.2):
val vektori = "laama".toVectorvektori: Vector[Char] = Vector(l, a, a, m, a)
val puskuri = vektori.toBufferpuskuri: Buffer[Char] = ArrayBuffer(l, a, a, m, a)
val taulukko = puskuri.toArraytaulukko: Array[Char] = Array(l, a, a, m, a)
val joukko = "tyhmyys".toSetjoukko: Set[Char] = Set(s, y, t, m, h)val taasVektori = taulukko.to(Vector)taasVektori: Vector[Char] = Vector(l, a, a, m, a)
val laiskalista = taulukko.to(LazyList)laiskalista: LazyList[Char] = LazyList(<not computed>)
Monelle kokoelmatyypille (muttei kaikille) on valmis metodi:
toVector, toBuffer jne.
Syntyvä kokoelma noudattaa tyyppinsä sääntöjä. Esimerkiksi
joukoksi muuttaminen poistaa duplikaattialkiot. Joukko ei
myöskään säilytä alkioiden järjestystä.
Yleiskäyttöiselle metodille to voi kertoa parametrilla,
millaisen kokoelman haluaa.
newBuilder ja toinen tapa alustaa kokoelma
Joskus on kätevää kerätä alkioita yksi tai muutama kerrallaan ja—kun valmista—
lopuksi muodostaa kerätyistä alkioista vakiokokoinen kokoelma,
vaikkapa vektori. Keräysvaiheessa avuksi voi olla väliaikainen,
muuttuvatilainen apuolio, joka pitää kirjaa käsitellyistä alkioista.
Scala API tarjoaa tuollaisiksi apulaisiksi Builder-oliot.
Monelle kokoelmatyypille löytyy valmiina newBuilder-metodi,
jolla sopivan ja tehokkaan Builder-apuolion voi luoda.
Tässä esimerkki vektorin muodostamisesta:
Ensimmäiseksi parametriksi annetaan jokin sellainen funktio,
joka ottaa parametriksi yhden kokonaisluvun ja joka myös
palauttaa kokonaisluvun. Viittaus tähän funktioon tallentuu
toiminto-muuttujaan.
kahdesti-funktio kutsuu parametriksi saamaansa funktiota
ensin kerran ja sitten näin tuotetulle paluuarvolle uudestaan.
Tässä pari tavallista funktiota, jotka sopivat kahdesti-funktion parametriksi:
kahdesti(luku => luku + 1, 1000)res141: Int = 1002
kahdesti(n => 2 * n, 1000)res142: Int = 4000
Funktioliteraali määrittelee nimettömän funktion, joka palauttaa
parametriaan yhtä isomman luvun. kahdesti-metodille välitetään
parametriksi viittaus tähän nimettömään funktioon.
Funktioliteraalin merkkinä on oikealle osoittava nuoli. Sen
vasemmalla puolella mainitaan parametrit (joita on tässä vain
yksi) ja oikealla puolella on funktion runko.
Voidaan kirjoittaa (luku:Int)=>luku+1, mutta tuo pidempi
merkintä ei ole tässä tapauksessa tarpeen, koska käyttöyhteydestä
on automaattisesti pääteltävissä, että parametrin tyyppi on Int.
Tässä toinen korkeamman asteen funktio (luvuista 6.1 ja 6.2):
Lyhennettyjä funktioliteraaleja voi muodostaa käyttämällä alaviivaa nimettyjen
parametrien sijaan (luku 6.2). Tällöin nuolimerkintää ei tarvita. Nämä kaksi eri koodia
vastaavat toisiaan:
Lyhennetyt merkinnät toimivat vain riittävän yksinkertaisissa tapauksissa. Yksi rajoitus
on, että nimetöntä alaviivaparametria voi käyttää vain kerran. Pidempi merkintä voi
olla tarpeen myös silloin, jos funktioliteraali sisältää toisia funktiokutsuja.
Näitä tärkeimpiä rajoituksia on kuvailtu tarkemmin luvussa 6.2.
Kokoelmien käsittely korkeamman asteen metodeilla
Alkiokokoelmilla on paljon yleiskäyttöisiä korkeamman asteen metodeita (luvut 6.3, 7.1,
10.1 ja 10.2), joille annetaan parametriksi funktio, jota sovelletaan kokoelman alkioihin.
Alla on esimerkkejä eräistä. Esimerkeissä käytetään merkkijonoja ja vektoreita, mutta
samoja metodeita löytyy muiltakin kokoelmilta.
Toistaminen joka alkiolle: foreach
Metodilla foreach voi toistaa saman käskyn kullekin alkiolle (luku 6.3):
Jos mapille välitetty parametrifunktio palauttaa kokoelman, syntyy sisäkkäinen rakenne:
val lukuja = Vector(100, 200, 150)lukuja: Vector[Int] = Vector(100, 200, 150)
lukuja.map( luku => Vector(luku, luku + 1) )res147: Vector[Vector[Int]] = Vector(Vector(100, 101), Vector(200, 201), Vector(150, 151))
Metodi flatMap tekee saman kuin map ja flatten yhdessä ja tuottaa "litteän"
lopputuloksen (luku 6.3):
lukuja.flatMap( luku => Vector(luku, luku + 1) )res148: Vector[Int] = Vector(100, 101, 200, 201, 150, 151)
Arvioimista kriteerin perusteella: exists, forall, filter, takeWhile, ym.
exists-metodilla voi selvittää, toteutuuko annettu kriteeri millekään alkiolle
(luku 6.3); forall vastaavasti selvittää, toteutuuko annettu kriteeri kaikille
alkioille; count laskee, monelleko kriteeri toteutuu:
find etsii ensimmäisen alkion, joka täyttää annetun kriteerin (luku 6.3);
indexWhere tekee saman, mutta palauttaa indeksin eikä itse alkiota (luku 7.1):
filter poimii kaikki kriteerin täyttävät alkiot (luku 6.3); filterNot tekee
saman käänteisesti; partition jakaa alkiot kriteerin täyttäviin ja täyttämättömiin:
takeWhile poimii kokoelman alusta alkioita niin kauan kuin ehto täyttyy (luku 6.3);
dropWhile vastaavasti jättää alusta ehdon täyttäviä alkioita pois; span hoitaa nuo
molemmat kerralla:
Metodit maxBy ja minBy etsivät isoimman tai pienimmän alkion parametrifunktiota
vertailukriteerinä käyttäen (luku 10.1); sortBy muodostaa järjestetyn kokoelman:
import scala.math.absval luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20)
val isoinItseisarvo = luvut.maxBy(abs)isoinItseisarvo: Int = -20
val pieninItseisarvo = luvut.minBy(abs)pieninItseisarvo: Int = 4
val jarjestettyItseisarvonMukaan = luvut.sortBy(abs)jarjestettyItseisarvonMukaan: Vector[Int] = Vector(4, 5, 5, 10, -20)
val sanat = Vector("kaikkein pisin", "lyhin", "keskipitkä", "lyhyehkö")sanat: Vector[String] = Vector(kaikkein pisin, lyhin, keskipitkä, lyhyehkö)
val pisin = sanat.maxBy( _.length )pisin: String = kaikkein pisin
val jarjestettyPituudenMukaan = sanat.sortBy( _.length )jarjestettyPituudenMukaan: Vector[String] = Vector(lyhin, lyhyehkö, keskipitkä, kaikkein pisin)
Suurimman tai pienimmän arvon etsiminen epäonnistuu, jos kokoelma on tyhjä. Tämän
erikoistapauksen käsittely käy kätevästi Option-päätteisillä versioilla yllä
mainituista metodeista:
Äsken mainituille metodeille on myös parametrittomat vastineet max, min, sorted,
maxOption ja minOption, jotka käyttävät alkioiden luonnollista järjestystä, olettaen
että sellainen on määritelty (luku 10.1). Tässä esimerkkejä järjestämisestä:
val numerojarjestys = luvut.sortednumerojarjestys: Vector[Int] = Vector(-20, 4, 5, 5, 10)
val unicodenMukainenJarjestys = sanat.sortedunicodenMukainenJarjestys: Vector[String] = Vector(kaikkein pisin, keskipitkä, lyhin, lyhyehkö)
val samaKuinAsken = sanat.sortBy( sana => sana )samaKuinAsken: Vector[String] = Vector(kaikkein pisin, keskipitkä, lyhin, lyhyehkö)
val samaTamakin = sanat.sortBy(identity)samaTamakin: Vector[String] = Vector(kaikkein pisin, keskipitkä, lyhin, lyhyehkö)
val kirjaimetJarjestyksessa = "Let's offroad!".sortedkirjaimetJarjestyksessa: String = " !'Ladeffoorst"
Jos vertailtavina tai järjestettävinä on Double-arvoja, on tarkennettava, mitä
vertailutapaa niille käytetään. Tarjolla on kaksi valmista tapaa, TotalOrdering ja
IeeeOrdering, joista kumpi tahansa toimii useimpiin tarkoituksiin. (Tarkemmat tiedot
API-dokumentaatiossa.)
Yleiskäyttöistä alkioiden läpikäyntiä: foldLeft ja reduceLeft
Metodit foldLeft ja reduceLeft sukulaisineen ovat matalamman abstraktiotason
työkaluja, joilla voi tarkasti määritellä, miten paluuarvo muodostetaan kokoelman
alkioiden perusteella (luku 7.1). Tässä ensin foldLeft:
val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20)
val summa = luvut.foldLeft(0)( (osasumma, seuraava) => osasumma + seuraava )summa: Int = 4
val samaLyhyemmin = luvut.foldLeft(0)( _ + _ )samaLyhyemmin: Int = 4
Kaksi parametriluetteloa: ensimmäiseen kirjoitetaan alkuarvo,
joka on samalla lopputulos siinä tapauksessa, ettei kokoelmassa
olisi alkioita lainkaan, ja...
... toisessa on funktio, jolla yhdistetään välitulos ja seuraava
alkio. Tässä esimerkissä kyseessä on yksinkertainen summafunktio.
reduceLeft on samansuuntainen, mutta se käyttää ensimmäistä alkiota lähtöarvona eikä
siis tarvitse parametrikseen kuin yhdistämisfunktion:
import scala.math.minval luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20)
val summa = luvut.reduceLeft( _ + _ )summa: Int = 4
val pienin = luvut.reduceLeft(min)pienin: Int = -20
reduceLeftin paluuarvo on samaa tyyppiä kuin käsiteltävän kokoelman alkiot,
kun taas foldLeft voi tuottaa muunkintyyppisen tuloksen:
val onkoIsoaLukua = luvut.foldLeft(false)( (loytyiJo, seuraava) => loytyiJo || seuraava > 10000 )onkoIsoaLukua: Boolean = false
Koska reduceLeft olettaa, että kokoelmassa on ainakin yksi alkio, se tuottaa
ajonaikaisen virheen, mikäli näin ei olekaan:
val tyhja = Vector[Int]()tyhja: Vector[Int] = Vector()
val tyhjanSummaFoldilla = tyhja.foldLeft(0)( _ + _ )tyhjanSummaFoldilla: Int = 0
val tyhjanSummaReducella = tyhja.reduceLeft( _ + _ )java.lang.UnsupportedOperationException: empty.reduceLeft...
reduceLeftOption on kuin reduceLeft, muttei kaadu tyhjän listan tapauksessa,
vaan palauttaa Option-tyyppisen tuloksen:
Option on kokoelmatyyppi: kussakin Option-oliossa on joko yksi alkio (Some) tai
nolla (None). Asiaa puidaan tarkemmin luvussa 8.3. Alla on ainoastaan valikoima
esimerkkejä kokoelmien metodeista Option-arvoihin sovellettuina.
Käytetään kokeiluissa seuraavia muuttujia:
val jotain: Option[Int] = Some(100)jotain: Option[Int] = Some(100)
val eiMitaan: Option[Int] = NoneeiMitaan: Option[Int] = None
size:
jotain.sizeres164: Int = 1
eiMitaan.sizeres165: Int = 0
foreach:
jotain.foreach(println)100
eiMitaan.foreach(println) // ei tulosta mitään
def tuhatPer(luku: Int) = if luku != 0 then Some(1000 / luku) else NonetuhatPer(luku: Int): Option[Int]
jotain.flatMap(tuhatPer)res184: Option[Int] = Some(10)
Some(0).flatMap(tuhatPer)res185: Option[Int] = None
eiMitaan.flatMap(tuhatPer)res186: Option[Int] = None
Alkioiden alustaminen funktiolla: tabulate
Kokoelmatyyppien yhteyteen on määritelty tabulate-funktio, jolla voi luoda kokoelmia
kätevästi alkiot tietyn "kaavan" mukaan alustaen (luvut 6.1 ja 6.2).
Tälle metodille annetaan kaksi parametriluetteloa. Ensimmäisessä on haluttu alkioiden määrä
eli luotavan kokoelman koko ja toisessa funktio, jota kutsutaan kullekin indeksille kyseisen
alkion muodostamiseksi:
Laiskalista (LazyList; vanhalta nimeltään Stream eli virta) on kokoelma, jonka
kaikkia alkioita ei muodosteta ja tallanneta etukäteen vaan vain tarvittaessa eli
laiskasti (luku 7.2). Se sopii käytäväksi läpi järjestyksessä. Laiskalistan alkiot
voi käsitellä yksi kerrallaan varastoimatta niitä kaikkia yhtaikaisesti muistiin.
Monilta osin laiskalista muistuttaa edellä esiteltyjä kokoelmatyyppejä. Sellaisen voi
esimerkiksi luoda alkiot luettelemalla tai olemassa olevasta kokoelmasta kopioimalla:
Äskeiset kokoelmat olivat äärellisiä. Tutummista kokoelmatyypeistä poiketen laiskalista
voi olla päättymätön. continually-funktio tuottaa loputtoman listan:
val lista = LazyList.continually("Oliver")lista: LazyList[String] = LazyList(<not computed>)
lista.take(5).foreach(println)Oliver
Oliver
Oliver
Oliver
Oliver
Laiskalistassa on loputtomasti alkioita, jotka saadaan evaluoimalla
lauseke "Oliver" (toistamiseen aina tarvittaessa). Koska
kyseessä on literaalilauseke, tämän listan alkiot ovat keskenään
identtisiä.
Alkuperäinen laiskalista on ääretön, mutta take palauttaa
parametrinsa mittaisen laiskalistan, joka on pätkä alkuperäisestä.
Näin luodussa laiskalistassa voi olla myös keskenään erilaisia alkioita:
Tämä laiskalista muodostuu satunnaisluvuista. Uusia lukuja
kuitenkin arvotaan vain sitä mukaa kun tarvitaan.
Muodostetaan loputtomasta lukulistasta katkaistu versio,
joka päättyy, kun vastaan arvotuksi riittävän iso luku.
Tämäkään ei vielä arvo satunnaislukuja vaan vain määrittää
laiskalistan, joka osaa niitä tiettyyn pisteeseen saakka
tuottaa.
mkString muodostaa kokoelman perusteella merkkijonon,
jossa arvotut luvut on lueteltu. Tämä pakottaa LazyList-olion
evaluoimaan alkioita muodostavan arpomiskäskyn toistuvasti.
Laiskalistaa voi käyttää myös vuorovaikutteisen ohjelman toteuttamiseen. Seuraava
luvusta 7.2 toistettu ohjelma kysyy käyttäjältä syötteitä kunnes tämä sanoo "please"
ja raportoi syötteiden pituudet tähän tapaan:
Enter some text: hello
The input is 5 characters long.
Enter some text: stop
The input is 4 characters long.
Enter some text: please
@maindefsayPlease()=defreport(input:String)="The input is "+input.length+" characters long."definputs=LazyList.continually(readLine("Enter some text: "))inputs.takeWhile(_!="please").map(report).foreach(println)
Laiskalista "tuo" syötteitä ohjelman käsiteltäväksi. Se on
päättymätön merkkijonojen lista, jonka kukin alkio saadaan
kysymällä sitä käyttäjältä. Tässä kuitenkin vasta määritellään
lista, jonka alkiot tuotetaan kutsumalla readLinea aina, kun
tarvitaan uusi alkio.
takeWhile palauttaa lopetussanaan "please" rajatun laiskalistan.
map palauttaa raporttien listan, jonka kukin alkio muodostetaan
(tarvittaessa) evaluoimalla readLine-käsky ja soveltamalla sen
palauttamaan arvoon report-funktiota. Tämäkään käsky ei vielä
silti kysy käyttäjältä mitään eikä kutsu report-funktiota.
foreach määrää raporttilistan alkiot tulostettavaksi. Jotta
alkion voi käsitellä, se on ensin määritettävä kysymällä syötettä
käyttäjältä. Tuloksena syntyy ohjelma, joka toistuvasti kyselee
käyttäjältä syötteitä ja raportoi niiden mitat.
Loputtoman lukulistan voi luoda helposti LazyList.from-funktiolla:
val positiiviset = LazyList.from(1)positiiviset: LazyList[Int] = LazyList(<not computed>)
positiiviset.take(3).foreach(println)1
2
3
LazyList.from(0, 10).take(3).foreach(println)0
10
20
val ekaIsoNelio = LazyList.from(0).map( n => n * n ).dropWhile( _ <= 1234567 ).headekaIsoNelio: Int = 1236544
Lisää tapoja luoda LazyList-olio
iterate-metodi luo laiskalistan, jossa seuraava alkio saadaan
edellisestä tiettyä funktiota aina uudelleen soveltamalla:
def vaihteleva = LazyList.iterate(1)( x => -2 * x )vaihteleva: LazyList[Int]
vaihteleva.take(4).foreach(println)1
-2
4
-8
Rekursiivisella funktiolla voi määritellä minkälaisen vain
laiskalistan. Tämä yksinkertainen rekursioesimerkki tekee saman kuin
LazyList.from(1).
Operaattori #:: muodostaa LazyListin
yhdistelmänä: alkuun tulee vasemmalla
mainittu yksittäinen arvo, perään oikealla
mainittu laiskalista.
Määritelmä on rekursiivinen eli itseensä
viittaava: positiivisten lukujen sarja
muodostetaan laittamalla alkuarvon perään
kaikkien sitä suurempien positiivisten
lukujen sarja.
Evaluoimattomat eli by name -parametrit
Laiskalistat perustuvat ajatukseen, että metodin parametriksi välitetään evaluoimaton
lauseke eikä tuon lausekkeen arvoa. Tällainen evaluoimatonta parametria
eli by name -parametria evaluoidaan vasta kun (tai jos) metodin suorituksessa päästään
kohtaan, jossa kyseistä parametria käytetään.
By name -parametrin voi määritellä itsekin, mistä on alla pieni esimerkki.
def printtaaJaPalauta(luku: Int) =println("Palautan parametrini " + luku)lukuprinttaaJaPalauta(luku: Int): Int
def kokeilu(luku: Int, luvunTuottavaLauseke: =>Int) = if luku >= 0 then luvunTuottavaLauseke else -1kokeilu(luku: Int, luvunTuottavaLauseke: => Int): Int
Ensimmäinen funktiomme vain raportoi, milloin sitä on kutsuttu.
Jälkimmäisen funktion toinen parametri on by name -parametri,
mikä merkitään nuolella =>. Tämä parametri evaluoidaan vasta
kun tai jos sitä käytetään kokeilu-funktiota suorittaessa.
Asia näkyy tulosteesta:
kokeilu(printtaaJaPalauta(10), printtaaJaPalauta(100))Palautan parametrini 10Palautan parametrini 100
res192: Int = 100
kokeilu(printtaaJaPalauta(-10), printtaaJaPalauta(100))Palautan parametrini -10
res193: Int = -1
Ensimmäinen parametri on ihan tavallinen. Parametrilauseke
evaluoidaan joka tapauksessa ennen kuin luku (10 tai -10)
välitetään kokeilu-funktiolle.
Kun ensimmäinen parametri on positiivinen, päädytään
haaraan, jossa jälkimmäinen parametrilauseke evaluoidaan
ja printtaaJaPalauta tulee toisen kerran kutsutuksi.
Kun ensimmäinen parametri on negatiivinen, päädytään
haaraan, jossa palautetaan -1. Jälkimmäistä parametria
ei tarvita eikä evaluoida lainkaan.
Laiskat muuttujat
Laiska muuttuja on yksittäinen muuttuja, joka toimii kuin laiskalistan
alkiot: se saa arvonsa evaluoimalla siihen sijoitetun lausekkeen,
kun sen arvoa ensi kerran tarvitaan. Siitä eteenpäin muuttuja
säilöö tuon arvon, eikä sijoitettua lauseketta evaluoida uudelleen.
Scalassa tällainen muuttuja määritellään sanoilla lazyval:
lazy val eka = printtaaJaPalauta(1)eka: Int = <lazy>
lazy val toka = printtaaJaPalauta(2)toka: Int = <lazy>
Funktion sisältämää tulostuskäskyä ei vielä suoritettu. Jatketaan:
if eka > 0 then eka * 10 else toka * 10Palautan parametrini 1
res194: Int = 10
if eka > 0 then eka * 10 else toka * 10res195: Int = 10
if-käskyn ehdon evaluoiminen vaatii eka-muuttujalle
arvon, joten tämän laiska muuttujan arvo määritetään
printtaaJaPalauta-funktiota kutsumalla. Tuloste ilmestyy
näkyviin.
Valituksi tulee ensimmäinen haara jossa if-lausekkeen
arvoksi saadaan eka*10. eka-muuttujalle on jo laskettu
arvo joten sitä ei lasketa uudestaan (eikä funktiomme tulosta
toista riviä, kuten olisi käynyt, jos eka olisi def eikä
lazyval.
Käskyn uusiminenkaan ei tuota printtaaJaPalauta-funktion
lisätulosteita, koska eka-muuttujalla on jo arvo.
Koska jälkimmäistä haaraa ei valittu, ei toka-muuttujan
arvoa tarvittu eikä sen arvoa ole vielä edes määritetty, vaikka
tuo muuttuja if-käskyssä esiintyykin.
Toistaminen silmukoilla
for–do-silmukka
for–do-silmukalla voi toistaa toimenpiteen kullekin kokoelman alkiolle (luku 5.5):
val puskuri = Buffer(100, 20, 5, 50)puskuri: Buffer[Int] = Buffer(100, 20, 5, 50)
foralkio<- puskurido println("Nyt käsiteltävä alkio: " + alkio) println("Sitä yhtä suurempi: " + (alkio + 1))Nyt käsiteltävä alkio: 100
Sitä yhtä suurempi: 101
Nyt käsiteltävä alkio: 20
Sitä yhtä suurempi: 21
Nyt käsiteltävä alkio: 5
Sitä yhtä suurempi: 6
Nyt käsiteltävä alkio: 50
Sitä yhtä suurempi: 51
Silmukan alussa määritellään, millaisille alkioille silmukan runkoa
toistetaan. Huomaa avainsanat for ja do.
Nuolen <- perässä on lauseke, joka kertoo, mistä arvoja noukitaan
vuoron perään käsiteltäviksi.
Nuolen vasemmalla puolella on muuttujan nimi, joka ohjelmoijan sopii
valita. Tämän niminen muuttuja on käytettävissä silmukan rungossa ja
sisältää aina parhaillaan käsiteltävän arvon (tässä: vuorossa olevan
alkion puskurista).
Silmukan runko suoritetaan kullekin alkiolle vuoron perään. Huomaa
sisennykset.
Silmukan rungossa voi yhdistellä erilaisia käskyjä. Esimerkiksi if-valintakäskyä voi
käyttää:
for alkio <- puskuri do
if alkio > 10 then
println("Tämä alkio on kymppiä isompi: " + alkio)
else
println("Tässä kohdassa on pieni alkio.")
end forTämä alkio on kymppiä isompi: 100
Tämä alkio on kymppiä isompi: 20
Tässä kohdassa on pieni alkio.
Tämä alkio on kymppiä isompi: 50
Loppumerkki on vapaaehtoinen mutta selkiyttää joskus.
Läpikäytävä kokoelma voi olla muukin, vaikkapa Range-tyyppinen lukuväli tai merkkijono
(luku 5.6):
for luku <- 10 to 15 do
println(luku)10
11
12
13
14
15
for indeksi <- puskuri.indices do
println("Indeksillä " + indeksi + " on luku " + puskuri(indeksi))Indeksillä 0 on luku 100
Indeksillä 1 on luku 20
Indeksillä 2 on luku 5
Indeksillä 3 on luku 50
for merkki <- "testi" do
println(merkki)t
e
s
t
i
for (alkio, indeksi) <- puskuri.zipWithIndex do
println("Indeksillä " + indeksi + " on luku " + alkio)Indeksillä 0 on luku 100
Indeksillä 1 on luku 20
Indeksillä 2 on luku 5
Indeksillä 3 on luku 50
Mm. luvut 5.5 ja 5.6 sisältävät runsaasti lisäesimerkkejä for–do-silmukoista.
for–yield ja monipuolisempia for-silmukoita
Scalan for-silmukalla on puolia, joita ei Ohjelmointi 1 -kurssilla
varsinaisesti esitellä tai tarvita. Silmukalla voi esimerkiksi tilan
muuttamisen sijaan tuottaa uuden kokoelman. Tällöin käytetään do-sanan
sijaan sanaa yield:
val vektori = Vector(100, 0, 20, 5, 0, 50)vektori: Vector[Int] = Vector(100, 0, 20, 5, 0, 50)
for luku <- vektori yield luku + 100res196: Vector[Int] = Vector(200, 100, 120, 105, 100, 150)
for sana <- Vector("laama", "alpakka", "vikunja") yield sana.lengthres197: Vector[Int] = Vector(5, 7, 7)
Samassa yhteydessä voi myös suodattaa arvoja:
for luku <- vektori if luku != 0 yield 100 / lukures198: Vector[Int] = Vector(1, 5, 20, 2)
Rivitys voi selkiyttää. Äskeisen voi rivittää esimerkiksi näin tai toisin:
for
luku <- vektori
if luku != 0
yield 100 / lukures199: Vector[Int] = Vector(1, 5, 20, 2)
Silmukan rungossa voi olla toinen silmukka. Tällöin sisempi silmukka suoritetaan kokonaan,
kaikkine toistoineen, kullakin ulomman silmukan suorituskerralla (luku 5.6).
Tässä yksi esimerkki:
val lukuja = Vector(5, 3)lukuja: Vector[Int] = Vector(5, 3)
val merkkeja = "abcd"merkkeja: String = abcd
for luku <- lukuja do
println("Ulomman kierros alkaa.")
for merkki <- merkkeja do
println(s"luku nyt $luku ja merkki nyt $merkki")
end for
println("Ulomman kierros päättyy.")
end forUlomman kierros alkaa.
luku nyt 5 ja merkki nyt a
luku nyt 5 ja merkki nyt b
luku nyt 5 ja merkki nyt c
luku nyt 5 ja merkki nyt d
Ulomman kierros päättyy.
Ulomman kierros alkaa.
luku nyt 3 ja merkki nyt a
luku nyt 3 ja merkki nyt b
luku nyt 3 ja merkki nyt c
luku nyt 3 ja merkki nyt d
Ulomman kierros päättyy.
Sisäkkäisyys ja for
Yhteen for-silmukkaan voi yhdistää useita "sisäkkäisiä" läpikäyntejä.
Seuraavat kolme koodia tekevät keskenään saman:
while-silmukan alkuun kirjoitetaan ehtolauseke, joka määrää, kauanko silmukan runkoa
toistetaan. Esimerkki:
var luku = 1luku: Int = 1
whileluku < 10doprintln(luku)luku += 4println(luku)1
5
5
9
9
13
Esimerkin ensimmäinen käsky alustaa muuttujan, jota jäljempänä
käytetään. Tämä alustus ei ole varsinaisesti osa silmukkaa.
Määrittelyn alussa on sanat while ja do ja niiden välissä
ehtolauseke.
Perässä on silmukan runko sisennettynä.
Ehtolausekkeen on oltava Boolean-tyyppinen. Se evaluoidaan aina
juuri ennen kutakin silmukan rungon suorituskertaa. Jos saadaan
false, niin silmukan suoritus päättyy, muuten suoritetaan runko
ja palataan jälkeen evaluoimaan tämä sama ehtolauseke.
Tässä esimerkissä silmukan runko toistetaan kolmesti. Ensimmäisen
suorituskerran lopussa luku-muuttujan arvo on 5, toisella
kerralla 9 ja kolmannella 13. Kun jatkamisehtoa tämän jälkeen
tarkastetaan, se ei enää ole voimassa.
On mahdollista, että toistokertoja on nolla: jatkamisehto tarkistetaan ensimmäisen kerran
jo ennen kuin runkoa on suoritettu kertaakaan. Edellisessä esimerkissä luku oli aluksi 1
ja jatkamisehto luku<10 siksi aluksi true. Alla näin ei ole:
var luku = 20luku: Int = 20
while luku < 10 do
println(luku)
luku += 4
println(luku)
end while
Ehto ei ole aluksi voimassa, eikä runkoa suoriteta
kertaakaan. Tämä koodi ei tulosta mitään.
Sivuhuomio: Myös while-silmukan voi päättää
loppumerkkiin, kuten tässä on esimerkin
vuoksi näytetty. Merkintä on vapaaehtoinen kuten
loppumerkit Scalassa muutenkin.
Hakurakenne on kokoelma, jonka alkioina on avain–arvo-pareja (luku 9.2). Se ei perustu
numeerisiin indekseihin vaan arvojen hakemiseen avainten perusteella. Avain–arvo-pareina
käytetään tavallisia kaksijäsenisiä monikkoja (ks. Parit ja muut monikot). Hakurakenteessa
voi esiintyä sama arvo useasti, mutta avainten on oltava keskenään erilaisia.
Scalan peruskirjastoissa on kaksi eri Map-luokkaa, joista toinen kuvaa muuttuvia
hakurakenteita ja toinen muuttumattomia. Muuttumattomat hakurakenteet ovat
automaattisesti käytettävissä, ja niitä on käytetty myös tämän sivun esimerkeissä
ellei toisin ole mainittu. Tässä kuitenkin kokeilemme muuttuvatilaista hakurakennetta,
joka otetaan erikseen käyttöön:
Metodin paluuarvo on String eikä Option[String] kuten
get-metodin tapauksessa.
Jos kyseessä on muuttuvatilainen hakurakenne, voi käyttää myös metodia getOrElseUpdate.
Haun epäonnistuessa se lisää hakurakenteeseen jälkimmäisen parametrinsa määräämän arvon,
joten haku lopulta onnistuu aina:
withDefaultValue-metodille ilmoitetaan, mitä halutaan
käyttää "vara-arvona" silloin, kun haku on huti.
Nyt kun hakurakenteesta haetaan olematonta avainta, ei
synny virhettä vaan saadaan tämä vara-arvo.
withDefault
Äskeisessä esimerkissä vara-arvo oli aina sama. Metodilla withDefault voit
asettaa hakurakenteelle "varafunktion", joka määrittää paluuarvoja hutihaun
tuottaneen avaimen perusteella:
def raportti(haettu: String) = "hait sanaa " + haettu + " muttei löytynyt"raportti(haettu: String): String
val englanniksi = Map("kissa" -> "cat", "tapiiri" -> "tapir", "koira" -> "dog").withDefault(raportti)englanniksi: Map[String,String] = Map(koira -> dog, tapiiri -> tapir, kissa -> cat)
englanniksi("kissa")res214: String = cat
englanniksi("insulintialainen kummitussirkka")res215: String = hait sanaa insulintialainen kummitussirkka muttei löytynyt
Esimerkissä ensin luodaan pari erillistä kokoelmaa ja yhdistetään
ne zip-metodilla. Syntyy pareja sisältävä vektori.
Tällaisen vektorin perusteella toMap voi luoda hakurakenteen.
Metodilla groupBy muodostetaan hakurakenne, johon alkuperäisen kokoelman alkiot on
ryhmitelty sen mukaan, mitä parametriksi annettu funktio alkion kohdalla palauttaa:
Kaikilla kuvioilla on isBiggerThan-metodi, jolla voi verrata
kuvioiden pinta-aloja keskenään.
Kaikilla kuvioilla on myös area-metodi pinta-alan laskemiseen.
Tämä metodi on abstrakti: sillä ei ole runkoa eikä sitä voi
sellaisenaan kutsua. Pinta-alan laskentatapa määritellään
erikseen alakäsitteille eli niille luokille, jotka perivät
Shape-piirteen (ks. alta).
Vertailumetodille voi antaa parametriksi viittauksen mihin
tahansa Shape-tyyppiseen olioon. Kaikilla tällaisilla olioilla
on jonkinlainenarea-metodi, joten voimme kutsu tuota metodia
vertailumetodin parametrille.
Alakäsite piirreluokalle
Seuraavat kaksi luokkaa perivät Shape-piirteen (luku 7.3). Ne edustavat kuviokäsitteen
alakäsitteitä:
Periytyminen merkitään extends-sanalla. Tästä seuraa, että
kaikki Circle-tyyppiset oliot ovat paitsi ympyröitä myös
kuvioita. Niillä on mm. piirreluokassa Shape määritelty
isBiggerThan-metodi.
Luokissa voidaan tarjota toteutukset piirreluokan abstrakteille
metodeille. Esimerkiksi tässä määritellään, että ympyrä on
sellainen kuvio, jonka pinta-ala lasketaan pii * r2, ja
suorakaide on sellainen kuvio, jonka pinta-ala lasketaan sivujen
kertolaskulla.
Luokka voi periytyä useasta piirreluokasta:
classXextendsA,B,C,D,Etc
Piirreluokka voi periytyä toisesta (tai useammastakin):
Muuttujan kuvio staattinen tyyppi on Shape. Sillä voi
viitata mihin tahansa Shape-tyyppiseen, esimerkiksi ympyrään
tai suorakaiteeseen. Staattinen tyyppi käy ilmi pelkästä
ohjelmakoodista.
Muuttujaan kuvio on tässä esimerkissä ensin sijoitettu arvo,
jonka dynaaminen tyyppi on Circle. Se korvataan arvolla,
jonka dynaaminen tyyppi on Rectangle. Dynaamisen tyypin on
oltava yhteensopiva muuttujan staattisen tyypin kanssa.
Kaikille Scala-olioille yhteisen isInstanceOf-metodin avulla voi tutkia arvon dynaamista
tyyppiä. Tässä todetaan, että kuvio-muuttujassa on parhaillaan viittaus olioon, joka on
sekä Rectangle että Shape-tyyppinen:
Muuttujan staattiseksi tyypiksi tulee alkuarvon perusteella
päätellyksi Circle, jolloin siihen voi sijoittaa vain
Circle-tyyppisiä arvoja eikä muita kuvioita.
Staattinen tyyppi rajoittaa arvojen käyttöä (luku 7.3):
var testi: Shape = Circle(10)testi: o1.shapes.Shape = o1.shapes.Circle@9c8b50
testi.radius-- Error: |testi.radius |^^^^^^^^^^^^ |value radius is not a member of o1.shapes.Shape
Lausekkeen test staattinen tyyppi on Shape. Mielivaltaiselle
Shape-oliolle ei ole määritelty radius-muuttujaa, vaikka
ympyröille onkin.
match-käskyllä voi valita dynaamisen tyypin mukaan:
testi match
case ympyra: Circle =>
println("Se on ympyrä, ja sen säde on " + ympyra.radius)
case _ =>
println("Se ei ole ympyrä.")Se on ympyrä, ja sen säde on 10.0
Luontiparametrit piirreluokilla
Piirreluokalla voi olla luontiparametreja. Esimerkiksi tässä määritellään, että
PersonAtAalto-olioilla on luontiparametrina nimi ja toimenkuva:
Kun tavallinen luokka tai yksittäisolio perii tämän piirreluokan,
välitetään piirreluokalle parametrit. Tässä esimerkkejä:
objectPresidentextendsPersonAtAalto("Ilkka","preside over the university")classEmployee(name:String,job:String)extendsPersonAtAalto(name,job)classLocalStudent(name:String,valid:String,valadmissionYear:Int)extendsPersonAtAalto(name,"study for a degree")classExchangeStudent(name:String,valaaltoID:String,valhomeUniversity:String,valhomeID:String)extendsPersonAtAalto(name,"study temporarily")
Yksittäisolio välittää piirreluokalle luontiparametrit.
Tässä President-olio välittää kaksi merkkijonoa piirreluokan
muuttujien name ja occupation arvoiksi.
Kun luokka perii piirteen, välitetään piirreluokalle
parametreja vastaavasti.
Usein (muttei aina) kyseessä ovat luokan omien luontiparametrien
arvot, jotka välitetään "ylöspäin" piirreluokalle.
Tässä ovat siis kyseessä tavalliset luokat/yksittäisoliot,
jotka perivät piirreluokan. (Vrt. jatkoesimerkki alla.)
Sanotaan, että haluamme lisäksi piirreluokan Student edustamaan erilaisia opiskelijoita
yleisesti — niin paikallisia kuin vaihto-opiskelijoitakin. Tämä versio ei toimi:
traitStudent(name:String,valid:String)extendsPersonAtAalto(name,"study for a degree")
Tässä yritetään välittää piirreluokan määrittelystä
luontiparametreja yläkäsitteelle PersonAtAalto.
Yritys tuottaa käännösaikaisen virheilmoituksen:
piirreluokasta ei voi näin välittää parametria "ylöspäin".
Sen sijaan seuraava toimii:
traitStudent(valid:String)extendsPersonAtAalto
PersonAtAalto ei saa tästä luontiparametreja. Toisaalta
ei tarvitsekaan; ne voi kirjata toisaalle. Tässä vain todetaan,
että opiskelijat ovat aaltolaisia, joilla on (aaltolaisten muiden
ominaisuuksien lisäksi) opiskelijanumero.
classLocalStudent(name:String,id:String,valadmissionYear:Int)extendsPersonAtAalto(name,"study for a degree"),Student(id)classExchangeStudent(name:String,aaltoID:String,valhomeUniversity:String,valhomeID:String)extendsPersonAtAalto(name,"study temporarily"),Student(aaltoID)
Välitämme parametrit sekä PersonAtAalto- että
Student-piirreluokalle näistä tavallisista
luokista.
Metodin korvaaminen: override
Alakäsitteessä voi korvata yläkäsitteelle määritellyn metodin käyttämällä override-sanaa
(luvut 2.4 ja 7.3). Eräs yleinen korvattava on toString-metodi. Tässä toisenlainen
esimerkki:
Alakasite-tyyppisen olion eka-metodi toimii yläkäsitteen
toteutuksesta riippumattomasti. Korvaava toteutus ratkaisee.
Osana alakäsitteen toka-metoditoteutusta kutsutaan
yläkäsitteen versiota metodista, joten...
... Alakasite-tyyppisen olion metodi tuottaa ensin
alakäsitteelle määritellyn tulosteen ja tekee sitten sen,
mitä korvattu Ylakasite-tyypin metodikin tekee.
super-sanaa voi käyttää yläkäsitteen määrittelyyn viittaamiseen muutenkin kuin
korvatussa metodissa, mutta tuo on suhteellisen yleinen käyttötapaus.
Periytyminen yliluokasta
Luokka voi periytyä paitsi piirreluokasta myös toisesta "tavallisesta"
luokasta. Tässä käytetään luvun 7.5 tapaan Rectangle-luokkaa, jonka perii luokka
Square:
open-sana kertoo luokan olevan avoin. Avoimesta luokasta
voi periyttää muita luokkia vapaasti. Ilman tätä määrettä
luokka olisi suljettu.
Käytetään extends-sanaa, jonka perään kirjoitetaan perityn
luokan nimi. Aliluokka Square periytyy nyt yliluokasta Rectangle,
ja Square-oliot ovat nyt myös Rectangle-tyyppisiä (ja Shape-tyyppisiä, koska Rectangle-luokka perii Shape-piirteen).
Luokalla Square on yksi luontiparametri, joka kertoo
kunkin sivun mitan.
Kun aliluokasta luodaan ilmentymä, on aluksi suoritettava myös
yliluokassa määritellyt alustustoimenpiteet. Periytyvä luokka
voi samalla välittää luontiparametreja yliluokalleen. Esimerkiksi
tässä määritellään, että kun Square-oliota luodaan, tehdään
aluksi samat alustustoimenpiteet kuin Rectangle-oliolle.
Tämä tehdään niin, että kummaksikin suorakaiteen luontiparametriksi
(eli kummaksikin sivunpituudeksi) tulee neliöolion saaman
luontiparametrin arvo.
Konkreettisessa luokassa kaikilla metodeilla on toteutus. Voidaan myös määritellä
abstrakti luokka, jollaisessa sopii olla abstrakteja, toteutuksettomia metodeita kuten
piirreluokassakin. Tässä esimerkki:
Sana abstract tekee luokasta abstraktin. Tästä luokasta
ei voi luoda suoraan ilmentymiä.
kokonaishinta-metodi on abstrakti. Konkreettisten aliluokkien
on tarjottava toteutus tälle metodille, jotta kaikki Tuote-tyyppiset oliot kykenevät tämän metodin suorittamaan.
Voiko se välittää luontiparametreja yläkäsitteilleen?
Ei voi.
Voi.
Voi.
Voiko sellaisia periä useita (extends-sanan perässä)?
Voi.
Ei voi.
Ei voi.
Näitä tekniikoita voi myös yhdistellä keskenään. Luokka voi esimerkiksi periytyä yhdestä
yliluokasta ja yhdestä tai useammasta piirreluokasta. Tai piirreluokka voi periytyä
tavallisesta luokasta.
Scalan luokkahierarkia
Kaikki Scala-oliot ovat kattotyyppiä Any. Sillä on välittömät aliluokatAnyVal ja AnyRef:
AnyVal-luokasta periytyvät tutut tietotyypit Int, Double,
Boolean, Char, Unit, ja muutama muu. Sille harvemmin laaditaan
itse uusia aliluokkia, ja moinen pitää erikseen ilmoittaa. (JVM ei
käsittele AnyVal-olioita viittausten kautta. AnyValien
on oltava muuttumattomia ja täyttää muitakin tiukkoja
ehtoja. Oikeissa paikoissa käytettyinä näillä tyypeillä voi parantaa
suoritustehokkuutta.)
AnyRef, toiselta nimeltään Object, on yliluokka muille luokille
ja yksittäisolioille. Esimerkiksi luokat String ja Vector
periytyvät tästä luokasta. Myös itse laatimasi (ei-piirre-)luokat
periytyvät automaattisesti AnyRefistä ellet erikseen toisin
määrittele. (JVM käsittelee AnyRef-olioita viittausten
kautta.)
Lisäksi on piirreluokka Matchable, joka kattaa kaikki sellaiset yläkäsitteenä kaikki
sellaiset Scala-tyypit, joita on luvallista käyttää match-käskyssä. Sekä AnyRefillä
että AnyValilla on tämä piirre, ja Matchable kattaakin lähes kaikki Scala-tyypit,
poislukien eräät erikoistapaukset.
Sana sealed piirre- tai muun luokan alussa määrittelee suljetun tyypin. Se tarkoittaa,
että tuolle luokalle ei voi määritellä muita välittömiä alatyyppejä kuin
ne, jotka on samaan tiedostoon kirjattu (luku 7.4). Esimerkiksi Option-luokan määrittely
alkaa näin:
sealedabstractclassOption/* Etc. */
Option-luokasta periytyvät vain samassa kooditiedostossa määritellyt yksittäisolio None
ja aliluokka Some. Näin taataan, että mikä tahansa Option on aina joko None tai jokin
Some-olio.
Tavalliset konkreettiset luokat ovat aina "melkein suljettuja" ellei erikseen open-
määreellä mainita. "Melkein suljettuja" siinä mielessä, että kääntäjä varoittaa, jos
luokalle määrittelee välittömän aliluokan muussa tiedostossa — mutta kuitenkin sallii
tuon. open-määreellisestä luokasta voi johtaa aliluokkia vapaasti (luku 7.5).
Sana final (luku 7.5) on vielä jyrkempi kuin sealed: se estää alakäsitteiden
määrittelyn kokonaan. Sen voi kirjoittaa myös yksittäisen metodin määrittelyn alkuun
(ennen def-sanaa), jolloin kyseistä metodia ei voi korvata alatyypeissä.
Luetelmatyypit: enum
Jos tyypin kukin olio on etukäteen tiedossa, voi nuo oliot määritellä luetelmatyypiksi.
Luetelmatyyppi on kuin tavallinen luokka, mutta siitä ei voi luoda mitään muita ilmentymiä
kuin koodiin erikseen listatut. Kaksi esimerkkiä luvusta 7.4:
Ilmentymät eli kaikki "tapaukset", jotka tästä tietotyypistä
on olemassa, luetellaan. Kun nuo oliot eroavat toisistaan
vain nimiensä osalta, kuten tässä, riittää yksi case-sana
ja pilkuin eroteltu luettelo.
Kun luetelmatyypit on noin määritelty, niitä voi käyttää näin:
val today = Weekday.Mondaytoday: Weekday = Monday
val cruelest = Month.Aprilcruelest: Month = April
import Weekday.*val deadlineDay = WednesdaydeadlineDay: Weekday = Wednesday
Kaikkien luetelmatyyppien yhteyteen tulee automaattisesti määritellyksi eräitä metodeita,
kuten values ja fromOrdinal:
Month.fromOrdinal(0)res226: Month = January
Month.fromOrdinal(11)res227: Month = December
Weekday.valuesres228: Array[Weekday] = Array(Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday)
Kuten tavalliselle luokalle myös luetelmatyypille voi määritellä ilmentymämuuttujia,
luontiparametreja ja metodeita, kuten tässä lukuun 7.4 perustuvassa esimerkissä:
Yksittäisolion Random-metodit tuottavat (näennäis)satunnaislukuja:
import scala.util.RandomRandom.nextInt(10)res229: Int = 8
Random.nextInt(10)res230: Int = 6
Random.nextInt(10)res231: Int = 2
Tässä arvotut luvut ovat väliltä 0–9 eli parametria
10 pienempiä.
Yllä käytetty yksittäisolio käyttää satunnaislukujen siemenenä
tietokoneen kellonaikaa. Siemenen voi myös määrätä itse ja välittää Random-luokasta
erikseen luodulle ilmentymälle:
val generaattori1 = Random(74534161)generaattori1: Random = scala.util.Random@75fbc2df
val generaattori2 = Random(74534161)generaattori2: Random = scala.util.Random@3f92984e
Tässä luotiin kaksi lukugeneraattoria ja määrättiin molemmille
sama mielivaltaisesti valittu siemen.
generaattori1.nextInt(100)res232: Int = 53generaattori1.nextInt(100)res233: Int = 38generaattori1.nextInt(100)res234: Int = 97generaattori2.nextInt(100)res235: Int = 53generaattori2.nextInt(100)res236: Int = 38generaattori2.nextInt(100)res237: Int = 97
Molemmat generaattorit käyttävät samaa näennäissatunnaislukuja
tuottavaa algoritmia. Kun siemenluku on sama, identtinen
nextInt-metodikutsujen sarja tuottaa samat luvut.
Random-olioilla on myös muita "arpomiseen" perustuvia metodeita kuin nextInt.
Mainitsemisen arvoinen on ainakin kokoelman järjestyksen uusiva shuffle (luku 8.1):
Metodi fromFile ottaa parametriksi tiedostopolun ja palauttaa
Source-tyyppisen olion, jonka kauttaa voi pyytää tiedoston
sisältöä. Polku voi olla suhteellinen (kuten tässä) tai
absoluuttinen.
Silmukalla käydään tässä läpi kukin niistä riveistä, jotka
getLinesia kutsumalla saadaan. (On myös muita tapoja käydä
läpi tiedoston sisältöä kuin rivi kerrallaan; ks. luku 12.2.)
try–finally-rakenne huolehtii siitä, että finally-lohkoon
sijoitettu tiedostoyhteyden sulkeva käsky tulee suoritetuksi,
vaikka datan lukeminen jostain syystä epäonnistuisikin.
Ja tässä esimerkki tekstitiedoston kirjoittamisesta:
importjava.io.PrintWriterimportscala.util.Random@maindefwritingExample()=valfileName="examplefolder/random.txt"valfile=PrintWriter(fileName)tryforn<-1to10000dofile.println(Random.nextInt(100))println("Created a file "+fileName+" that contains pseudorandom numbers.")println("In case the file already existed, its old contents were replaced with new numbers.")finallyfile.close()endwritingExample
PrintWriter-olion voi luoda näin. Parametriksi annetaan
kirjoitettavan tiedoston nimi.
println-metodilla voi kirjoittaa tiedostoon yhden rivin
tekstiä.
Yhteyden sulkeminen on erityisen tärkeää, kun tiedostoon
kirjoitetaan, koska vasta yhteyttä suljettaessa vahvistuu
viimeisten merkkien tallennus levylle.
Graafiset käyttöliittymät
Graafisia käyttöliittymiä laaditaan apukirjastoa käyttäen. O1:n materiaalissa esiintyy
kaksi eri kirjastoa: O1Library-moduulin GUI-työkalut sekä yleisempi Swing-kirjasto.
o1-kirjaston työkalut
Kurssin oman GUI-työkalupakin keskeisin osa on luokka o1.View. Alla on pääasiat kokoava
esimerkki.
View-luokan ajatuksena on, että View-olio tarjoaa ikkunanäkymän johonkin olioon, joka
toimii sovelluksen aihealueen mallina (luku 2.7). Seuraavassa esimerkissämme mallin
muodostaa yksi tämän pikkuluokan ilmentymä:
// Kappale on muuttuvatilainen olio. Sillä on sijainti ja väri.classKappale(varvari:Color):varsijainti=Pos(10,10)defliiku()=this.sijainti=this.sijainti.add(1,1)defpalaa()=this.sijainti=Pos(10,10)endKappale
Laaditaan tämän näköinen käyttöliittymä, jossa kappale on piirretty kaksiväristä taustaa
vasten ympyränä:
Esimerkkiohjelman "kappale" liikkuu vähitellen oikealle ja alas.
Se palaa takaisin alkuun tuplaklikkauksella ja vaihtaa väriä sen
mukaan, kummalla taustalla hiiren kursori on.
Käyttöliittymämme on yksittäisolio, joka on erikoistapaus
View-tyypistä.
Annamme View-oliolle parametriksi ikkunan otsikon. Valinnaisena
lisäparametrina voi antaa mm. ajan tikitysnopeuden (tässä: 10).
View-oliolle on määriteltävä makePic-metodi, joka määrittää,
millainen kuva kullakin ajanhetkellä piirretään näkyviin. Tässä
muodostamme kuvan asettamalla ympyrän kuvan taustaneliötä vasten.
Tapahtumankäsittelijämetodit (luku 3.1) reagoivat ajan kulumiseen
ja käyttäjän toimiin. Tässä muutama esimerkki: kappale etenee
tikittäessä, vaihtaa väriä hiiren liikkuessa ja palaa alkuun
tuplaklikkauksella.
Poimimme talteen hiiren klikkausta kuvaavan MouseClicked-olion
ja kysymme siltä klikkausten lukumäärää (luku 3.6).
isDone-metodi määrää, milloin käyttöliittymä lakkaa reagoimasta
tapahtumiin. Tässä esimerkissä se tapahtuu, kun kappale on liikkunut
tietyn matkaa oikealle.
View-olion luominen ei vielä tuo mitään näkyviin eikä aloita
ajan "tikitystä". Nämä hoituvat start-metodia kutsumalla.
GUI-ohjelmointiin tarkoitettua Swing-kirjastoa on esitelty luvussa 12.4. Tässä kokoava
esimerkki sieltä:
importscala.swing.*importscala.swing.event.*objectTapahtumakokeiluextendsSimpleSwingApplication:valekaNappi=Button("Paina minua")(())valtokaNappi=Button("Ei kun MINUA!")(())valkehote=Label("Paina jompaakumpaa napeista.")valkaikkiJutut=BoxPanel(Orientation.Vertical)kaikkiJutut.contents++=Vector(kehote,ekaNappi,tokaNappi)valnappulaikkuna=MainFrame()nappulaikkuna.contents=kaikkiJututnappulaikkuna.title="Kokeiluikkuna"this.listenTo(ekaNappi,tokaNappi)this.reactions+={casepainallus:ButtonClicked=>vallahdenappula=painallus.sourcevalviesti="Painoit nappia, jossa lukee: "+lahdenappula.textDialog.showMessage(kaikkiJutut,viesti,"Viesti")lahdenappula.text=lahdenappula.text+"!"}deftop=this.nappulaikkunaendTapahtumakokeilu
Sovellusta kuvaa yksittäisolio, joka periytyy Swing-käyttöliittymien
laatimiseen sopivasta luokasta.
Määritellään, miten havaittuihin tapahtumiin reagoidaan.
Kun tapahtuma havaitaan, suoritetaan apuikkunan näyttävä koodi.
Koodissa voi käyttää muuttujaa painallus, johon on tallentunut
tapahtumaa kuvaava ButtonClicked-tyyppinen olio.
SimpleSwingApplicationit tarvitsevat pääikkunan (top), joka
tulee näkyviin käynnistäessä.
Yllä esitetty tapa käsitellä tapahtumia on yleispätevämpi, mutta yksinkertaisiin
tilanteisiin riittää, kun annat Button-oliolle koodia, joka ajetaan, kun nappulaa
painetaan:
Scala-kielen varatut sanat eli sanat, joita ei voi käyttää tunnuksina, ovat:
abstract case catch class def do else enum export extends
false final finally for given if implicit import lazy match
new null object override package private protected return sealed super
then throw this trait true try type val var while
with yield
: = <- => <: >: # @ =>> ?=>
Lisäksi seuraavat sanat ovat "pehmeästi varattuja", mikä tarkoittaa, ettei niiden käyttö
tunnuksina ole kielletty, mutta niillä on tietyissä yhteyksissä erityismerkityksiä:
as derives end extension infix inline opaque open transparent using
| * + -
Palaute
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.
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.
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.
Palautusta lähetetään...
Identtinen palautus
Tämä palautus on identtinen aikaisemman palautuksen kanssa. Oletko varma, että haluat lähettää palautuksen?
Palautuksen lähettämisessä arvosteluun tapahtui virhe. Tarkistathan internet-yhteytesi. Jos palautuskerta käytettiin, palautus arvostellaan automaattisesti, kun palvelu on jälleen käytettävissä.
Nyt ei tarvita pakkauksen nimeä.