Kurssin viimeisimmän version löydät täältä: O1: 2024
Scalaa kootusti
Tämä sivu kuvailee valikoituja Scala-kielen ominaisuuksia ja Scala-kielen oheiskirjastojen sisältämiä työkaluja. Sivulla on niistä pieniä, irrallisia esimerkkejä. Voit oppimateriaalin varsinaisiin lukuihin jo tutustuttuasi käydä täältä kertaamassa yksityiskohtia "Miten se nyt kirjoitettiinkaan?" -hengessä.
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 Scala-kielen määrittelyä ja Scala API -dokumentaatiota, vaikka ne alkeiskurssilaiselle osin vaikeaselkoisia 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.
Tämän sivun osiot
- Alkeita
- Pakkaukset ja kirjastot
- Funktioiden alkeita
- Yksittäisoliot
- Luokat (ja lisää olioista)
- Kuvankäsittelyä O1:n kirjastolla
- Totuusarvot
- Olemattomia arvoja
- Valintakäskyt
if
jamatch
- Käyttöalue ja näkyvyysmääreet
- Parit ja muut monikot
- Lisää merkkijonoista
- Kokoelmien alkeita
- Yleisiä kokoelmien metodeita
- Kokoelman koko:
size
,isEmpty
janonEmpty
- Alkion etsiminen:
contains
jaindexOf
- Alkioita alusta, lopusta ja keskeltä:
head
,tail
,take
,drop
,slice
ym. - Alkioiden lisääminen ja kokoelmien yhdistäminen
- Alkiot uuteen kokoelmaan:
to
,toVector
,toSet
jne. - Muita metodeita:
mkString
,indices
,zip
,reverse
,flatten
ym.
- Kokoelman koko:
- Lisää funktioista
- Kokoelmien käsittely korkeamman asteen metodeilla
- Toistaminen joka alkiolle:
foreach
- Alkioiden kuvaaminen toisiksi:
map
,flatMap
- Arvioimista kriteerin perusteella:
exists
,forall
,filter
,takeWhile
, ym. - Suuruus ja pienuus:
maxBy
,minBy
,sortBy
- Yleiskäyttöistä alkioiden läpikäyntiä:
foldLeft
jareduceLeft
Option
kokoelmatyyppinä- Alkioiden alustaminen funktiolla:
tabulate
- Toistaminen joka alkiolle:
- Laiskalistat ja väljä evaluointi
- Toistaminen silmukoilla
- Hakurakenteet (
Map
) - Ylä- ja alakäsitteitä
- Satunnaislukuja
- Tiedostonkäsittelyä
- Graafiset käyttöliittymät
- Varatut sanat
- Palaute
- Tekijät
Alkeita
Lukuja
Laskutoimituksia (luku 1.3):
100 + 1res0: Int = 101 1 + 100 * 2res1: Int = 201 (1 + 100) * 2res2: Int = 202
Int
-tyyppisten kokonaislukujen jakolasku pyöristää kohti nollaa:
76 / 7res3: Int = 10
Modulo-operaattori %
tuottaa jakojäännöksen (luku 1.7):
76 % 7res4: Int = 6
Double
-arvoilla laskeminen tuottaa desimaaleja (laskentatarkkuuden puitteissa):
76.0 / 7.0res5: Double = 10.857142857142858
Katso myös lukutyyppien rajoitukset luvusta 5.4 ja lukutyyppien metodeita luvusta 5.2.
Merkkejä
String
on merkkijonotyyppi (luku 1.3). Merkkijonoilla on operaattorit +
ja *
:
"maa" + "laama"res6: String = maalaama "laama" * 3res7: String = laamalaamalaama
Yksittäisiä merkkejä voi kuvata tyypillä Char
(luku 5.2). Char
-literaali
muodostetaan heittomerkeillä:
'a'res8: Char = a '!'res9: Char = !
Lisää merkkijonojen käsittelyä alempana kohdissa Merkkijonojen metodeita, Kokoelmien alkeita ja Kokoelmien käsittely korkeamman asteen metodeilla.
Muuttujia
Muuttujan määritteleminen (luku 1.4):
val lukumuuttuja = 100lukumuuttuja: Int = 100
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):
muutettavissa += 10muutettavissa: Int = 161 muutettavissa -= 100muutettavissa: Int = 61 muutettavissa *= 2muutettavissa: 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.
val muuttuja = 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. */
val teksti = "Minut on dokumentoitu."
Dokumentaatiokommenttien perusteella voidaan 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)res12: 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)res13: 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ä:
Käyttöönotto import
-käskyllä
Pakkauksen sisältämän työkalun käyttöönotto (luku 1.6):
import scala.math.absimport scala.math.abs abs(-50)res14: Int = 50 abs(100)res15: Int = 100
Näin otetaan käyttöön pakkauksen koko sisältö kerralla:
import scala.math._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):
package pakkauksen.kenties.moniosainen.nimi
Tiedostot tallennetaan pakkausten nimiä vastaavaan hakemistorakenteeseen.
Tässä mainitun tavan lisäksi Scalassa voi määritellä pakkauksina toimivia olioita, joista lisää alempana kohdassa Pakkausoliot.
Yleisiä funktioita scala.math
-pakkauksesta
Muutama yleishyödyllinen funktio pakkauksesta scala.math
:
import 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 API:n 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 import scala.io.StdIn._
on annettu.
println("Kirjoita jotain tätä kehotetta seuraavalle riville: ")
val kayttajanSyottamaTeksti = 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: ")
val kayttajanSyottamaTeksti = readLine()
Sama lyhyemmin:
val kayttajanSyottamaTeksti = 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:
val syotettyInt = readInt()
val syotettyDouble = readDouble()
Viimeksi mainitut käskyt käskeytyvät ajonaikaisen virhetilanteeseen, elleivät syötetyt merkit vastaa mitään lukua.
Funktioiden alkeita
Yksinkertainen funktio
Esimerkkifunktio luvusta 1.7:
def keskiarvo(eka: Double, toka: Double) = (eka + toka) / 2
Funktion kutsuminen
Funktiokutsu:
keskiarvo(10.0, 12.5)res16: Double = 11.25
Monirivinen funktio
Kun funktion runko koostuu useasta peräkkäisestä käskystä, ovat aaltosulkeet tarpeen. Tässä esimerkki luvusta 1.7:
def verot(tulot: Double, tuloraja: Double, peruspros: Double, lisapros: Double) = {
val perusosa = min(tuloraja, tulot)
val lisaosa = max(tulot - tuloraja, 0)
perusosa * perusprosentti + lisaosa * lisaprosentti
}
Vaikutuksellisten funktioiden tapauksessa aaltosulkeita on tapana käyttää silloinkin, kun se ei ole pakollista; ks. tyyliopas.
Parametreista
Yllä olevilla funktioilla on yksi parametriluettelo (kaarisulkeissa funktion nimen perässä). Parametriluettelo voi olla tyhjäkin (luku 2.6):
def tulostaVakioteksti() = {
println("Tämä tulostuu aina, kun kutsu tulostaVakioteksti() suoritetaan.")
}
Funktiolla ei välttämättä ole parametriluetteloa lainkaan. (Tämä on yleistä olioiden yhteydessä; luku 2.2.)
def palautaTeksti = "Funktiokutsu palautaTeksti tuottaa aina tämän merkkijonon."
Parametriluetteloja voi olla useita (luku 6.1):
def kokeilu(eka: Int, toka: String)(lisaparametri: Int) = eka * lisaparametri + toka
kokeilu(10, "laama")(100)res17: String = 1000laama
Palautusarvoista
Kaikissa yllä olevissa esimerkeissä palautusarvon tyyppi on jätetty kirjaamatta koodiin, mikä on sallittua tyyppipäättelyn vuoksi. Palautusarvon tyypin saa erikseen kirjatakin (luku 1.8), kuten näissä esimerkeissä:
def keskiarvo(eka: Double, toka: Double): Double = (eka + toka) / 2
def palautaTeksti: String = "Funktiokutsu palautaTeksti tuottaa aina tämän merkkijonon."
Tietyissä tilanteissa palautusarvon 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
- itseään (rekursiossa; luku 12.1).
Arvon palauttaminen return
-käskyllä
Arvon voi (muttei ole Scalassa tapana) määrätä palautettavaksi myös
return
-käskyllä (luku 8.3), joka katkaisee funktion suorituksen:
def verot(tulot: Double, tuloraja: Double, peruspros: Double, lisapros: Double): Double = {
val perusosa = min(tuloraja, tulot)
val lisaosa = max(tulot - tuloraja, 0)
return perusosa * peruspros + lisaosa * lisapros
}
return
-käskyn perään kirjoitetaan lauseke, jonka arvo palautetaan.return
-käskyä käytetään, on kirjattava palautusarvon tyyppi.Yksittäisoliot
Olion määritteleminen: metodit, muuttujat, this
Yksittäisen olion määrittely luvussa 2.2 tarkemmin kuvaillusta esimerkistä:
object tyontekija {
var nimi = "Matti Mikälienen"
val syntynyt = 1965
var kkpalkka = 5000.0
var tyoaika = 1.0
def ikaVuonna(vuosi: Int) = vuosi - this.syntynyt
def kuukausikulut(kulukerroin: Double) = this.kkpalkka * this.tyoaika * kulukerroin
def korotaPalkkaa(kerroin: Double) = {
this.kkpalkka = this.kkpalkka * kerroin
}
def kuvaus =
this.nimi + " (s. " + syntynyt + "), palkka " + this.tyoaika + " * " + this.kkpalkka + " euroa"
}
object
perään kirjoitetaan oliolle valittu nimi.val
), osa ei (var
).def
-sanalla.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.Yksittäisolion käyttö: pistenotaatio
Olion muuttujien käyttö:
tyontekija.kkpalkkares18: Double = 5000.0 tyontekija.tyoaika = 0.6tyontekija.tyoaika: Double = 0.6
Metodikutsuja:
tyontekija.korotaPalkkaa(1.1)tyontekija.ikaVuonna(2021)res19: Int = 56
Sovelluksen käynnistäminen: käynnistysoliot
Käynnistysolio (luku 2.7) on yksittäisolio, joka toimii sovelluksen käynnistyskohtana:
object Testiohjelma extends App {
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ä kutsuttaisiin muiden ohjelmaan kuuluvien olioiden metodeita.")
}
extends App
määrittelee, että kyseessä on käynnistysolio.
(Tarkemmin sanoen tässä liitetään olioon App
-piirre; ks. kohta
Piirreluokat alempaa.)Pakkausoliot
Yksittäisolio voi toimia pakkauksena, johon kootaan toisiinsa liittyviä työkaluja kuten
funktioita, olioita ja luokkia (luku 5.2). Tällaisen pakkausolion voi
määritellä ihan tavalliseksi yksittäisolioksi. Alla on määritelty pakkaus omat.kokeilu
:
package omat
object kokeilu {
def tuplaa(luku: Int) = luku * 2
def triplaa(luku: Int) = luku * 3
}
kokeilu
-nimisen olion sisään,
jota on tarkoitus käyttää pakkausoliona.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._import omat.kokeilu._ tuplaa(10)res20: Int = 20 triplaa(10)res21: Int = 30
Toinen tapa määritellä pakkausolio
Pakkausolion voi määritellä myös näin package
-avainsanaa kahdesti
käyttäen:
package omat
package object kokeilu {
def tuplaa(luku: Int) = luku * 2
def triplaa(luku: Int) = luku * 3
}
Näin määriteltynä pakkausolio tulee sijoittaa haluttua pakkauksen nimeä
vastaavaan kansioon, esimerkiksi tiedostoon omat/kokeilu/package.scala
.
Monesti tällaiselle tiedostolle annetaan nimeksi package.scala
,
mutta muukin nimi toimii.
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:
class Tyontekija(annettuNimi: String, annettuSyntymavuosi: Int, annettuPalkka: Double) {
var nimi = annettuNimi
val syntynyt = annettuSyntymavuosi
var kkpalkka = annettuPalkka
var tyoaika = 1.0
def ikaVuonna(vuosi: Int) = vuosi - this.syntynyt
// Jne. Muita metodeita.
}
class
luokan nimen edessä. Pakolliset aaltosulkeet.new
-käskyllä, on annettava nimi, syntymävuosi ja palkka.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):
class Tyontekija(var nimi: String, val syntynyt: Int, var kkpalkka: Double) {
var tyoaika = 1.0
def ikaVuonna(vuosi: Int) = vuosi - this.syntynyt
// Jne. Muita metodeita.
}
Ilmentymien luominen ja käyttö
Yllä kuvattua Tyontekija
-luokkaa voi käyttää näin (luku 2.3):
new Tyontekija("Eugenia Enkeli", 1963, 5500)res22: o1.Tyontekija = o1.Tyontekija@1145e21
new
-sanalla, jonka perään kirjoitetaan
luokan nimi ja sulkeissa arvot konstruktoriparametreille.Muuttujaan voi tallentaa viittauksen luotuun olioon. Tällöin oliota voi käyttää helposti muuttujan nimen kautta.
val juuriPalkattu = new Tyontekija("Teija Tonkeli", 1985, 3000)juuriPalkattu: o1.Tyontekija = o1.Tyontekija@704234 juuriPalkattu.ikaVuonna(2021)res23: Int = 36 println(juuriPalkattu.kuvaus)Teija Tonkeli (s. 1985), palkka 1.0 * 3000.0 euroa
Ilmentymän räätälöinti
Ilmentymälle voi määritellä yksilöllisesti toimivia metodeita.
Tässä esimerkkiluokka luvusta 2.4:
class Henkilo(val nimi: String) {
def lausu(lause: String) = this.nimi + ": " + lause
def reagoiKryptoniittiin = this.lausu("Onpa kumma mineraali.")
}
Tavallinen henkilö ei osaa lentää. Tämä tietty Henkilo
-olio kuitenkin
osaa. Lisäksi yksi sen metodeista toimii toisin kuin muiden henkilöolioiden:
val realistinenTerasmies = new Henkilo("Clark") {
def lenna = "WOOSH!"
override def reagoiKryptoniittiin = "GARRRRGH!"
}
lenna
.override
-sanalla.(Tämä on itse asiassa esimerkki käsitteiden välisestä periytymisestä; ks. luku 7.3._)
Perustyypit olioina ja operaattorinotaatio
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)res24: 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 2021res25: Int = 36
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
vakioiksi o1
-pakkaukseen:
import o1._import o1._ Redres26: Color = Red CornflowerBlueres27: Color = CornflowerBlue
Tarjolla olevat värivakiot kattavat mm. kaikki W3C-organisaation CSS Color Module -standardin nimeämät sävyt.
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.redres28: Int = 220 CornflowerBlue.blueres29: Int = 237
Värillä on R-, G- ja B-komponenttien lisäksi myös ns. alfakanava eli läpinäkymättömyys.
Red.opacityres30: Int = 255
val lapikuultavaPunainen = Color(255, 0, 0, 100)lapikuultavaPunainen: Color = Color(255, 0, 0, opacity: 100)
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 eka = new Pos(15.5, 10)eka: Pos = (15.5,10.0) val toka = Pos(0, 20)toka: Pos = (0.0,20.0)
Pos
-olio on oleellisesti pari Double
-muotoisia koordinaatteja.Koordinaatteja voi tutkia erikseen:
eka.xres31: Double = 15.5 toka.yres32: Double = 10.0
Pos
-olioilla voi laskea:
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.
Lisää metodeita mm. luvssa 3.1 ja dokumentaatiossa.
Kuvat: o1.Pic
Kuvia edustaa luokka o1.Pic
.
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
pics
-kansiossa (tai jossain
muualla sohjelman luokkapolussa).Kuvilla on leveys ja korkeus pikseleinä:
netistaLadattu.widthres33: Double = 135.0 netistaLadattu.heightres34: Double = 155.0
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
place
-metodille sijoituskohta koordinaattiparina
(jossa x kasvaa oikealle, y alas.) Asemoitavan kuvan keskikohta
tulee noihin koordinaatteihin.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.
- Kääntely:
clockwise
,counterclockwise
(luku 2.3). - Peilaus:
flipHorizontal
,flipVertical
(luku 2.3). - Skaalaus:
scaleBy
(luku 2.3),scaleTo
. - Rajaus:
crop
(luku 2.5). - Siirto:
shiftLeft
,shiftRight
(luku 3.1). - Yksittäisen pikselin tutkiminen:
pixelColor
(luku 5.4). - Värimuutos pikseleittäin:
transformColors
,combine
(luku 6.1). - Kuvan tuottaminen pikseleittäin:
Pic.generate
(luku 6.1).
Kattava luettelo on tietysti dokumentaatiossa.
Muita o1
-luokkia
Color
in, Pos
in ja Pic
in lisäksi o1
-pakkauksessa on muutakin graafisten ohjelmien
laatimisessa hyödyllistä välineistöä. Tässä tärkeimpiä:
- Luokkaa
View
voi käyttää graafisten käyttöliittymien laatimiseen. Siitä on yhteenveto alempana tällä sivulla kohdassa Graafiset käyttöliittymät. - Luokka
Direction
kuvaa (mielivaltaisia) suuntia kaksiulotteisessaPos
-koordinaatistossa (luvut 3.6 ja 4.4; dokumentaatio). - Luokka
Grid
kuvaa kaksiulotteisia ruudukkoja (luku 7.4; dokumentaatio). Sen kanssa yhteen sopivat- luokka
GridPos
, joka kuvaa sijainteja ruudukossa (luvut 6.3 ja 7.4; dokumentaatio), ja - luokka
CompassDir
, joka kuvaa pääilmansuuntia ruudukkotyyppisessä koordinaatistossa (luku 6.3; dokumentaatio).
- luokka
- Luokka
Anchor
kuvaa kuvien kiinnityskohtia ja helpottaa asemointia (luku 2.5; dokumentaatio).
Totuusarvot
Boolean
-tyyppi
Totuusarvoja voi kuvata Boolean
-tietotyypillä (luku 3.3). Tämän tyyppisiä arvoja on
tasan kaksi, true
ja false
, joita vastaavat Scala-literaalit.
falseres35: Boolean = false val tamanMuuttujanArvoOnTosi = truetamanMuuttujanArvoOnTosi: Boolean = true
Vertailuoperaattorit
Vertailuoperaattorit tuottavat totuusarvoja (luku 3.3):
10 <= 10res36: Boolean = true 20 < (10 + 10)res37: Boolean = false val ikavuodet = 20ikavuodet: Int = 20 val onAikuinen = ikavuodet >= 18onAikuinen: Boolean = true ikavuodet == 30res38: Boolean = false 20 != ikavuodetres39: Boolean = false
!=
vertaillaan erisuuruutta.Logiikkaoperaattorit
Logiikkaoperaattoreita (luku 5.1):
Operaattori | Nimi | Esimerkki | Vastaa tarpeeseen |
---|---|---|---|
&& |
ja (and) | jokuVaite && toinenVaite |
"Ovatko molemmat totuusarvot true ?" |
|| |
tai (or) | jokuVaite || toinenVaite |
"Onko ainakin toinen totuusarvoista true ?" |
^ |
joko–tai eli poissulkeva tai
(exclusive or eli xor)
|
jokuVaite ^ toinenVaite |
"Onko tasan yksi totuusarvoista true ?" |
! |
ei tai negaatio
(not tai negation)
|
!jokuVaite |
"Onko totuusarvo false ?" |
Esimerkkejä:
val jaettava = 50000jaettava: Int = 50000 var jakaja = 100jakaja: Int = 100 !(jakaja == 0)res40: Boolean = true jakaja != 0 && jaettava / jakaja < 10res41: Boolean = false jakaja == 0 || jaettava / jakaja >= 10res42: Boolean = true jaettava / jakaja >= 10 || jakaja == 0res43: Boolean = true
Operaattorit &&
ja ||
evaluoidaan väljästi: jos vasemmanpuoleinen osalauseke
riittää määräämään koko loogisen lausekkeen totuusarvon, niin oikeanpuoleista ei
evaluoida lainkaan:
jakaja = 0jakaja: Int = 0 jaettava / jakaja >= 10 || jakaja == 0java.lang.ArithmeticException: / by zero ... jakaja == 0 || jaettava / jakaja >= 10res44: Boolean = true jakaja != 0 && jaettava / jakaja < 10res45: Boolean = false
Olemattomia arvoja
Option
, Some
ja None
Tämän esimerkkifunktion palautusarvo 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ää:
def osamaara(jaettava: Int, jakaja: Int) =
if (jakaja == 0) None else Some(jaettava / jakaja)
osamaara(100, 5)res46: Option[Int] = Some(20) osamaara(100, 0)res47: Option[Int] = None
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.Some
-arvo
testi
-muuttujaan voitaisiin sijoittaa.Option
-tyypin sijaan voisi käyttää null
-arvoa, mutta se ei ole useimmissa
Scala-ohjelmissa kannatettavaa (luku 4.3).
Option
-olioiden metodeita
Metodeilla isDefined
ja isEmpty
voi tutkia, onko kääre tyhjä:
val kaarittyLuku = Some(100)kaarittyLuku: Option[Int] = Some(100) kaarittyLuku.isDefinedres48: Boolean = true kaarittyLuku.isEmptyres49: Boolean = false None.isDefinedres50: Boolean = false None.isEmptyres51: Boolean = true
Metodi getOrElse
palauttaa arvon kääreen sisältä. Sille annetaan parametrilauseke, joka
määrää, mitä metodi palauttaa kääreen ollessa tyhjä:
kaarittyLuku.getOrElse(12345)res52: Int = 100 None.getOrElse(12345)res53: 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
getOrElse
stä sikäli, ettei se pura käärettä:
kaarittyLuku.orElse(Some(54321))res54: Option[Int] = Some(100) None.getOrElse(Some(54321))res55: Option[Int] = Some(54321)
Lisää Option
ista
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) luku * 2 else 10res56: Int = 200
if (luku < 0) luku * 2 else 10res57: Int = 10
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) 10 else 20valinnanTulos: Int = 20 println(if (luku > 100) 10 else 20)20
Jos valinnaisessa osassa on peräkkäisiä käskyjä, rivitetään ja käytetään aaltosulkeita (mikä on muutenkin tapana, jos käsky on vaikutuksellinen; ks. tyyliopas):
if (luku > 0) { 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) {
println("Osamäärä on: " + 1000 / luku)
}
println("Loppu.")Osamäärä on: 10
Loppu.
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) "negatiivinen" else if (luku > 0) "positiivinen" else "nolla"res58: String = positiivinen if (luku < 0) { println("Luku on negatiivinen.") } else if (luku > 0) { println("Luku on positiivinen.") } else { println("Luku on nolla.") }Luku on positiivinen.
if
-käskyt voi muutenkin laittaa sisäkkäin:
if (luku > 0) {
println("On positiivinen.")
if (luku > 1000) {
println("On yli tuhat.")
} else {
println("On positiivinen muttei yli tuhat.")
}
} On positiivinen.
On positiivinen muttei yli tuhat.
else
-osiota
ollenkaan. Jos luku ei olisi ollut positiivinen, ei olisi
tulostunut mitään.Äskeisessä esimerkissä else
-sana liittyi "lähimpään" if
-käskyyn. Tuo else
-osa
suoritettiin nimenomaan siksi, että ulompi ehto toteutui mutta sisempi ei. Seuraavassa
esimerkissä, joka on aaltosulutettu toisin, sisemmällä if
-käskyllä ei ole else
-osiota,
mutta ulommalla on:
if (luku > 0) { println("On positiivinen.") if (luku > 1000) { 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 on
esimerkkejä virhetilanteista, joita voi syntyä, kun käyttää if
-käskyä funktion
palautusarvon määrittämiseen.
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.) }
match
-sanaa edeltävän lausekkeen arvoa verrataan...Konkreettinen koodiesimerkki:
val kuutionKuvaus = luku * luku * luku match {
case 0 => "luku on nolla ja niin sen kuutiokin"
case 1000 => "kympistä tulee tuhat"
case muuKuutio => "luku " + luku + ", jonka kuutio on " + muuKuutio
}
Int
-literaaleja. Näistä tapauksista ensimmäinen valitaan,
jos luvun kuutio oli nolla, toinen jos se oli tuhat.muuKuutio
. Tällainen tapaus sopii yhteen minkä
tahansa arvon kanssa ja tulee siis tässä valituksi mikäli
kuutio ei ollut nolla eikä tuhat.Eräs käyttö match
-käskylle on arvon poimiminen Option
-kääreestä:
// Tätä käytetään alla match-esimerkissä.
def osamaara(jaettava: Int, jakaja: Int) =
if (jakaja == 0) None else Some(jaettava / jakaja)
osamaara(ekaLuku, tokaLuku) match {
case Some(tulos) => "Tulos on: " + tulos
case None => "Tulosta ei ole."
}
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.2 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: Any) =
jonkinlainenArvo match {
case jono: String => "kyseessä on merkkijono " + jono
case luku: Int if 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"
}
Any
, mikä tarkoittaa
että sille voi antaa minkä tahansa arvon parametriksi.match
-käskyn
osana käytetään samaa if
-sanaa kuin erillisessä
if
-valintakäskyssäkin.)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).
Luokan ja sen osien käyttöalue
class Esimerkki(konstruktoriparametri: Int) {
val julkinenIlmentymamuuttuja = konstruktoriparametri * 2
private val yksityinenIlmentymamuuttuja = konstruktoriparametri * 3
def julkinenMetodi(parametri: Int) = parametri * this.yksityinenMetodi(parametri)
private def yksityinenMetodi(parametri: Int) = parametri + 1 + this.yksityinenIlmentymamuuttuja
}
olio.julkinenIlmentymamuuttuja
.
Samoin julkista metodia voi käyttää mistä tahansa päin ohjelmaa.
Ilmentymämuuttuja tai metodi on julkinen ellei toisin määritellä.Paikallisten muuttujien käyttöalue
Kun siirrät hiiren kursorin laatikoiden päälle, korostuvat mainittujen muuttujien käyttöalueet.
def funktio(parametri: Int) = {
var paikallinen = parametri + 1
var toinenPaikallinen = paikallinen * 2
if (paikallinen > toinenPaikallinen) {
val vainIffissa = toinenPaikallinen
toinenPaikallinen = paikallinen
paikallinen = vainIffissa
}
toinenPaikallinen - paikallinen
}
parametri
on määritelty koko kyseisen
funktion ohjelmakoodissa. Sitä voi käyttää sieltä mistä vain.paikallinen
, on käytettävissä määrittelykohdasta alkaen metodin
koodin loppuun.toinenPaikallinen
.vainIffissa
on määritelty vain
if
-käskyn sisällä.Mutkikkaampia esimerkkejä löytyy luvusta 5.6.
Paikalliset funktiot
Vastaavasti myös funktioita voi määritellä toisten funktioiden sisään paikallisiksi, mistä kerrotaan luvussa 6.4. Tässä alkeellinen esimerkki:
def ulompiFunktio(luku: Int) = {
def sisempi(tuplattava: Int) = tuplattava * 2
sisempi(luku) + sisempi(luku + 1)
}
sisempi
on määritelty toisen funktion sisään ja on tarkoitettu
vain sen avuksi.Kumppanioliot
Poikkeuksena yleisiin sääntöihin luokka ja sen kumppaniolio pääsevät käsiksi toistensa yksityisiin osiin. Tässä tiivistelmä luvun 5.3 esimerkistä:
object Asiakas {
private var montakoLuotu = 0
}
class Asiakas(val nimi: String) {
Asiakas.montakoLuotu += 1
val numero = Asiakas.montakoLuotu
override def toString = "#" + this.numero + " " + nimi
}
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 8.4). 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)
Monikon jäsenet on numeroitu ykkösestä(!) alkaen, ja niihin pääsee käsiksi alaviivaa hyödyntäen:
nelikko._1res59: String = Tässä monikossa on neljä erilaista jäsentä. nelikko._3res60: Double = 3.14159
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:
val (suomeksi, englanniksi) = parisuomeksi: String = laama englanniksi: String = llama
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
).
Lisää merkkijonoista
Merkkijonojen metodeita
Tässä kappaleessa on esimerkkejä eräistä merkkijonojen metodeista (luvut 3.3 ja 5.2). Katso myös yltä johdantokohta Merkkejä sekä alta kokoelmien ominaisuuksia yleisemmin esittelevät kohdat Kokoelmien alkeita ja Kokoelmien käsittely korkeamman asteen metodeilla. (Merkkijonothan ovat kokoelmia, joiden alkioina on merkkejä.)
Merkkijonon pituuden eli koon voi selvittää kummalla vain seuraavista tavoista:
val jono = "Olavi Eerikinpoika Stålarm"jono: String = Olavi Eerikinpoika Stålarm jono.lengthres61: Int = 26 jono.sizeres62: Int = 26
Kirjainkokojen muokkausta:
"five hours of Coding can save 15 minutes of Planning".toUpperCaseres63: String = FIVE HOURS OF CODING CAN SAVE 15 MINUTES OF PLANNING "five hours of Coding can save 15 minutes of Planning".toLowerCaseres64: String = five hours of coding can save 15 minutes of planning "five hours of Coding can save 15 minutes of Planning".capitalizeres65: String = Five hours of Coding can save 15 minutes of Planning
Osamerkkijono:
"Olavi Eerikinpoika Stålarm".substring(6, 11)res66: String = Eerik "Olavi Eerikinpoika Stålarm".substring(3)res67: String = vi Eerikinpoika Stålarm
Merkkijonon jakaminen osiin:
"Olavi Eerikinpoika Stålarm".split(" ")res68: Array[String] = Array(Olavi, Eerikinpoika, Stålarm) "Olavi Eerikinpoika Stålarm".split("la")res69: Array[String] = Array(O, vi Eerikinpoika Stå, rm)
Tyhjän poisto merkkijonon reunoilta:
val teksti = " tyhjät merkit poistuvat ympäriltä mutteivät keskeltä "teksti: String = " tyhjät merkit poistuvat ympäriltä mutteivät keskeltä " teksti.trimres70: String = tyhjät merkit poistuvat ympäriltä mutteivät keskeltä
Merkkijonon sisältämien numeromerkkien tulkitseminen luvuksi:
"100".toIntres71: Int = 100 "100".toDoubleres72: Double = 100.0 "100.99".toDoubleres73: Double = 100.99 "sata".toIntjava.lang.NumberFormatException: For input string: "sata" ... " 100".toIntjava.lang.NumberFormatException: For input string: " 100" ... " 100".trim.toIntres74: Int = 100
Äskeiset toimet voi tehdä turvallisemmin Option
-päätteisillä metodeilla:
"100".toIntOptionres75: Option[Int] = Some(100) "sata".toIntOptionres76: Option[Int] = None "100.99".toDoubleOptionres77: Option[Double] = Some(100.99)
Vertailua Unicode-aakkoston mukaan:
"abc" < "bcd"res78: Boolean = true "abc" >= "bcd"res79: Boolean = false "abc".compare("bcd")res80: Int = -1 "bcd".compare("abc")res81: Int = 1 "abc".compare("abc")res82: Int = 0 "abc".compare("ABC")res83: Int = 32 "abc".compareToIgnoreCase("ABC")res84: Int = 0
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.
s
-kirjain.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 " + lukures85: String = luku on 10 "kuor" + 100res86: 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 \""res87: String = tässä lainausmerkki " ja toinenkin " "tässä kenoviiva \\ ja toinenkin \\"res88: 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ä."""res89: 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:
100.toStringres90: String = 100 false.toStringres91: String = false
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 = new Kokeilu(10)kokeiluolio: Kokeilu = Kokeilu@56181 kokeiluolio.toStringres92: String = Kokeilu@56181 kokeiluoliores93: Kokeilu = Kokeilu@56181
toString
-metodi tuottaa tämän näköisen
merkkijonon (luku 2.5).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 = new 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 + "!!!"res94: String = OLIOLLA ON ARVO 11!!! s"Testiolion toString-palautusarvo upotetaan tähän väliin $testiolio ja täältä jatkuu."res95: String = Testiolion toString-palautusarvo 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
:
import scala.collection.mutable.Bufferimport scala.collection.mutable.Buffer
Puskurien luominen:
Buffer("eka", "toka", "kolmas", "vielä neljäskin")res96: Buffer[String] = ArrayBuffer(eka, toka, kolmas, vielä neljäskin)
val lukuja = Buffer(12, 2, 4, 7, 4, 4, 10, 3)lukuja: Buffer[Int] = ArrayBuffer(12, 2, 4, 7, 4, 4, 10, 3)
new
-sanaa ei käytetä
puskurioliota luodessa Buffer
-sanan edessä (koska
luominen tapahtuu tehdasmetodilla).Puskuri voi olla tyhjä:
val tanneVoiLisataLukuja = Buffer[Double]()tanneVoiLisataLukuja: Buffer[Double] = ArrayBuffer()
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)res97: Int = 12 lukuja(3)res98: Int = 7
Nämä ovat itse asiassa lyhennysmerkintöjä, jotka vastaavat näitä puskuriolion
apply
-metodin kutsuja (luku 5.3):
lukuja.apply(0)res99: Int = 12 lukuja.apply(3)res100: Int = 7
Metodi lift
on samaa sukua. Se palauttaa tuloksen Option
-tyyppisenä
eikä kaadu ajonaikaiseen virheeseen indeksin ollessa epäkelpo:
lukuja(10000)java.lang.IndexOutOfBoundsException: 10000 ... lukuja.lift(10000)res101: Option[Int] = None lukuja.lift(-1)res102: Option[Int] = None lukuja.lift(3)res103: Option[Int] = Some(7)
Puskurin alkion voi vaihtaa toiseen:
lukuja(3) = 1val neljasAlkioOnNyt = lukuja(3)neljasAlkioOnNyt: Int = 1
Operaattorilla +=
voi lisätä uuden alkion puskurin loppuun, mikä kasvattaa puskurin kokoa:
lukuja += 11res104: Buffer[Int] = ArrayBuffer(12, 2, 4, 1, 4, 4, 10, 3, 11) lukuja += -50res105: Buffer[Int] = ArrayBuffer(12, 2, 4, 1, 4, 4, 10, 3, 11, -50)
Operaattorilla -=
voi poistaa yhden alkion:
lukuja -= 4res106: Buffer[Int] = ArrayBuffer(12, 2, 1, 4, 4, 10, 3, 11, -50) lukuja -= 4res107: Buffer[Int] = ArrayBuffer(12, 2, 1, 4, 10, 3, 11, -50)
Alkioita voi lisätä ja poistaa myös esimerkiksi näillä metodikutsuilla:
lukuja.append(100)lukuja.prepend(1000)lukujares108: Buffer[Int] = ArrayBuffer(1000, 12, 2, 1, 4, 10, 3, 11, -50, 100) lukuja.insert(5, 50000)lukujares109: Buffer[Int] = ArrayBuffer(1000, 12, 2, 1, 4, 50000, 10, 3, 11, -50, 100) val poistettuNeljasAlkio = lukuja.remove(3)poistettuNeljasAlkio: Int = 1 lukujares110: Buffer[Int] = ArrayBuffer(1000, 12, 2, 4, 50000, 10, 3, 11, -50, 100)
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 yllä olevissa esimerkeissä.
Kuitenkaan siis vektoreita ei voi muuttaa ja taulukoita vain vakiokoon puitteissa.
Vektorit ovat Scalassa käytettävissä ilman import
-käskyä.
Tässä pari esimerkkiä:
val vektori = Vector(12, 2, 4, 7, 4, 4, 10, 3)vektori: Vector[Int] = Vector(12, 2, 4, 7, 4, 4, 10, 3) vektori(6)res111: Int = 10 vektori.lift(10000)res112: Option[Int] = None
Muita kokoelmatyyppejä:
- 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 kuten vektoreita ja puskureitakin (luku 11.1). - Listat (
List
) ovat kokoelmia, jotka sopivat erityisen hyvin alkioiden käymiseen läpi järjestyksessä. Niitä käsitellään lyhyesti luvussa 11.2. - 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.1 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 9.2. - Hakurakenteista (
Map
) ei poimita alkioita indeksien vaan ns. avainten perusteella. Niistä on oma osionsa jäljempänä tällä sivulla. - Pinot (stack) ovat kokoelmia, joista poistetaan aina viimeksi lisätty alkio (luku 11.2).
Option
on niukka-alkioinen kokoelmatyyppi.
Valintaan kokoelmatyyppien välillä vaikuttavat mm. ohjelmointiparadigma ja laadulliset seikat kuten luettavuus ja tehokkuus.
Scalan virallisessa dokumentaatiossa on tiivis yleiskatsaus tarjolla oleviin kokoelmaluokkiin.
Kokoelmia voi laittaa 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)res113: Char = m jono.lift(3)res114: 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 10.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)res115: Int = 4 kouluarvosanat(2)res116: Int = 6
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
Yleisiä kokoelmien metodeita
Tämä osio täydentää yllä olevaa johdantoa kokoelmiin. Alla on lyhyitä esimerkkejä eräistä yleiskäyttöisistä kokoelmien metodeista. Kaikki tässä kappaleessa esitellyt ovat ensimmäisen asteen metodeita; lisää tehokkaita työkaluja löytyy kohdasta Kokoelmien käsittely korkeamman asteen metodeilla.
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.
Kokoelman koko: size
, isEmpty
ja nonEmpty
Kokoelman koon tutkiminen (luku 4.2):
Vector(10, 100, 100, -20).sizeres117: Int = 4 Vector().sizeres118: Int = 0 Vector(10, 100, 100, -20).isEmptyres119: Boolean = false Vector(10, 100, 100, -20).nonEmptyres120: Boolean = true Vector().isEmptyres121: Boolean = true Vector().nonEmptyres122: Boolean = false "laama".isEmptyres123: Boolean = false "".isEmptyres124: Boolean = true
Alkion etsiminen: contains
ja indexOf
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 8.4):
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)
Pätkä keskeltä slice
-metodilla:
Vector("eka/0", "toka/1", "kolmas/2", "neljäs/3", "viides/4").slice(1, 4)res125: Vector[String] = Vector(toka/1, kolmas/2, neljäs/3)
Alkuindeksin alkio tulee mukaan. Loppuindeksin ei.
Alkioiden lisääminen ja kokoelmien yhdistäminen
Uuden kokoelman muodostaminen lisäämällä alkioita:
val lukuja = Vector(10, 20, 100, 10, 50, 20)lukuja: Vector[Int] = Vector(10, 20, 100, 10, 50, 20) val yksiAlkioLoppuun = lukuja :+ 999999yksiAlkioLoppuun: Vector[Int] = Vector(10, 20, 100, 10, 50, 20, 999999) val yksiAlkioAlkuun = 999999 +: lukujayksiAlkioAlkuun: Vector[Int] = Vector(999999, 10, 20, 100, 10, 50, 20) val kokoelmienYhdistelma = lukuja ++ Vector(999, 998, 997)kokoelmienYhdistelma: Vector[Int] = Vector(10, 20, 100, 10, 50, 20, 999, 998, 997)
Tällaiset lisäystoiminnot, jotka muodostavat uusia kokoelmia, ovat sallittuja myös tilaltaan muuttumattomille kokoelmille (kuten yllä). Olemassa olevan muuttuvatilaisen kokoelman muokkaamisesta on esimerkkejä ylempänä kohdassa Puskurien peruskäyttöä.
Muistisääntö kokoelmien operaattoreille (kuten +:
)
+:
-operaattoria väärin päin 🤦🏼♀️.Muistisääntö noille Scala-operaattoreille:
The COLon goes on the COLlection side.
Nämä ovat siis OK:
vektori :+ uusiElementti // lisää loppuun
uusiElementti +: vektori // lisää alkuun
Mutta nämä eivät:
vektori +: uusiElementti // virhe
uusiElementti :+ vektori // virhe
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>)
toVector
, toBuffer
jne.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:
val vektorinrakentaja = Vector.newBuilder[String]vektorinrakentaja: ReusableBuilder[String,Vector[String]] = VectorBuilder@5dcf0772 vektorinrakentaja += "eka"vektorinrakentaja += "toka"vektorinrakentaja += "kolmas"val rakennettuKokoelma = vektorinrakentaja.result()rakennettuKokoelma: Vector[String] = Vector(eka, toka, kolmas)
Muita metodeita: mkString
, indices
, zip
, reverse
, flatten
ym.
Kokoelman sisällön muotoileminen merkkijonoksi järjestyy usein kätevimmin
mkString
-metodilla (luku 4.2):
val vektori = Vector(100, 20, 30)vektori: Vector[Int] = Vector(100, 20, 30) println(vektori.toString)Vector(100, 20, 30) println(vektori)Vector(100, 20, 30) println(vektori.mkString)1002030 println(vektori.mkString("---"))100---20---30
Kokoelman kaikki indeksit Range
-tyyppisenä kokoelmana (luku 5.6):
"laama".indicesres126: Range = Range 0 until 5 Vector(100, 20, 30).indicesres127: Range = Range 0 until 3
Kahden kokoelman yhdistäminen pareja sisältäväksi kokoelmaksi (luku 8.4):
val lajit = Vector("laama", "alpakka", "vikunja")lajit: Vector[String] = Vector(laama, alpakka, vikunja) val korkeudet = Vector(180, 80, 60)korkeudet: Vector[Int] = Vector(180, 80, 60) val korkeudetJaLajit = korkeudet.zip(lajit)korkeudetJaLajit: Vector[(Int, String)] = Vector((180,laama), (80,alpakka), (60,vikunja)) val kolmePariaKoskaKorkeuksiaVainKolme = korkeudet.zip(Vector("laama", "alpakka", "vikunja", "guanako"))kolmePariaKoskaKorkeuksiaVainKolme: Vector[(Int, String)] = Vector((180,laama), (80,alpakka), (60,vikunja)) val parivektoriKokoelmapariksi = korkeudetJaLajit.unzipparivektoriKokoelmapariksi: (Vector[Int], Vector[String]) = (Vector(180, 80, 60), Vector(laama, alpakka, vikunja)) val lajitJaIndeksit = lajit.zip(lajit.indices)lajitJaIndeksit: Vector[(String, Int)] = Vector((laama,0), (alpakka,1), (vikunja,2)) val sama = lajit.zipWithIndexsama: Vector[(String, Int)] = Vector((laama,0), (alpakka,1), (vikunja,2))
Käänteisen kokoelman muodostaminen reverse
-metodilla (luku 4.2):
"laama".reverseres128: String = amaal Vector(10, 20, 15).reverseres129: Vector[Int] = Vector(15, 20, 10)
Sisäkkäisen kokoelman "litistäminen" flatten
-metodilla (luku 6.1):
val kaksiulotteinenVektori = Vector(Vector(1, 2), Vector(100, 200), Vector(2000, 1000))kaksiulotteinenVektori: Vector[Vector[Int]] = Vector(Vector(1, 2), Vector(100, 200), Vector(2000, 1000)) val yksiulotteinen = kaksiulotteinenVektori.flattenyksiulotteinen: Vector[Int] = Vector(1, 2, 100, 200, 2000, 1000)
Scala API -dokumentaatiossa
on kuvattu paljon muitakin sekalaisia kokoelmien metodeita, kuten sum
, product
,
grouped
, sliding
, transpose
jne. Lisäksi kokoelmilla on paljon korkeamman
asteen metodeita: ks. Kokoelmien käsittely korkeamman asteen metodeilla alempana.
Lisää funktioista
Korkeamman asteen funktiot
Funktion voi välittää parametriksi toiselle. Alla on tiivistelmä yhdestä luvun 6.1 esimerkistä.
Korkeamman asteen funktio kahdesti
:
def kahdesti(toiminto: Int => Int, kohde: Int) = toiminto(toiminto(kohde))
toiminto
-muuttujaan.kahdesti
-funktio kutsuu parametriksi saamaansa funktiota
ensin kerran ja sitten näin tuotetulle palautusarvolle uudestaan.Tässä pari tavallista funktiota, jotka sopivat kahdesti
-funktion parametriksi:
def seuraava(luku: Int) = luku + 1
def tuplaa(tuplattava: Int) = 2 * tuplattava
Käyttöesimerkkejä:
kahdesti(seuraava, 1000)res130: Int = 1002 kahdesti(tuplaa, 1000)res131: Int = 4000
Nimettömät funktiot: funktioliteraaleja
Funktion voi kirjoittaa koodiin def
-merkinnän sijaan literaalina, jolloin
syntyy nimetön funktio (luku 6.2).
Käytetään esimerkiksi tätä korkeamman asteen funktiota:
def kahdesti(toiminto: Int => Int, kohde: Int) = toiminto(toiminto(kohde))
kahdesti(luku => luku + 1, 1000)res132: Int = 1002
kahdesti(n => 2 * n, 1000)res133: Int = 4000
kahdesti
-metodille välitetään
parametriksi viittaus tähän nimettömään funktioon.(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):
def onkoJarjestyksessa(eka: String, toka: String, kolmas: String, vertaa: (String, String) => Int) =
vertaa(eka, toka) <= 0 && vertaa(toka, kolmas) <= 0
onkoJarjestyksessa
vaatii viimeiseksi parametrikseen funktion,
joka tuottaa kahden merkkijonon perusteella kokonaisluvun.Funktiota voi käyttää esimerkiksi näin:
val pituusjarjestyksessa = onkoJarjestyksessa("Java", "Scala", "Haskell", (j1, j2) => j1.length - j2.length)pituusjarjestyksessa: Boolean = true val unicodejarjestyksessa = onkoJarjestyksessa("Java", "Scala", "Haskell", (j1, j2) => j1.compare(j2))unicodejarjestyksessa: Boolean = false
Lyhyempiä funktioliteraaleja: nimettömät parametrit
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:
kahdesti(luku => luku + 1, 1000)
kahdesti(n => 2 * n, 1000)
kahdesti( _ + 1 , 1000)
kahdesti( 2 * _ , 1000)
Myös nämä koodit tekevät keskenään saman:
onkoJarjestyksessa("Java", "Scala", "Haskell", (j1, j2) => j1.length - j2.length )
onkoJarjestyksessa("Java", "Scala", "Haskell", (j1, j2) => j1.compare(j2) )
onkoJarjestyksessa("Java", "Scala", "Haskell", _.length - _.length )
onkoJarjestyksessa("Java", "Scala", "Haskell", _.compare(_) )
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, 6.4, 9.2 ja 9.3), 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):
Vector(10, 50, 20).foreach(println)10
50
20
"laama".foreach( kirjain => println(kirjain.toUpper + "!") )L!
A!
A!
M!
A!
Alkioiden kuvaaminen toisiksi: map
, flatMap
Metodi map
tuottaa kokoelman, jonka alkiot on muodostettu parametrifunktion
osoittamalla tavalla alkuperäisen kokoelman alkioista (luku 6.3):
val sanoja = Vector("laama", "Tyra", "Norma", "ritarit sanoo: ")sanoja: Vector[String] = Vector(laama, Tyra, Norma, "ritarit sanoo: ") sanoja.map( sana => sana + "nni" )res134: Vector[String] = Vector(laamanni, Tyranni, Normanni, ritarit sanoo: nni) sanoja.map( sana => sana.length )res135: Vector[Int] = Vector(5, 4, 5, 15)
Sama lyhennetyillä funktioliteraaleilla:
sanoja.map( _ + "nni" )res136: Vector[String] = Vector(laamanni, Tyranni, Normanni, ritarit sanoo: nni) sanoja.map( _.length )res137: Vector[Int] = Vector(5, 4, 5, 15)
Jos map
ille 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) )res138: 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) )res139: 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:
val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20) luvut.exists( _ < 0 )res140: Boolean = true luvut.exists( _ < -100 )res141: Boolean = false luvut.forall( _ > 0 )res142: Boolean = false luvut.forall( _ > -100 )res143: Boolean = true luvut.count( _ > 0 )res144: Int = 4
find
etsii ensimmäisen alkion, joka täyttää annetun kriteerin (luku 6.3);
indexWhere
tekee saman, mutta palauttaa indeksin eikä itse alkiota (luku 6.4):
val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20) luvut.find( _ < 5 )res145: Option[Int] = Some(4) luvut.find( _ == 100 )res146: Option[Int] = None luvut.indexWhere( _ < 5 )res147: Int = 2 luvut.indexWhere( _ == 100 )res148: Int = -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:
val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20) val vahintaanViitoset = luvut.filter( _ >= 5 )vahintaanViitoset: Vector[Int] = Vector(10, 5, 5) val alleViitoset = luvut.filterNot( _ >= 5 )alleViitoset: Vector[Int] = Vector(4, -20) val askeisetParina = luvut.partition( _ >= 5 )askeisetParina: (Vector[Int], Vector[Int]) = (Vector(10, 5, 5),Vector(4, -20))
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:
val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20) val kunnesPieni = luvut.takeWhile( _ >= 5 )kunnesPieni: Vector[Int] = Vector(10, 5) val alkaenEkastaPienesta = luvut.dropWhile( _ >= 5 )alkaenEkastaPienesta: Vector[Int] = Vector(4, 5, -20) val molemmatParina = luvut.span( _ >= 5 )molemmatParina: (Vector[Int], Vector[Int]) = (Vector(10, 5),Vector(4, 5, -20))
Suuruus ja pienuus: maxBy
, minBy
, sortBy
Metodit maxBy
ja minBy
etsivät isoimman tai pienimmän alkion parametrifunktiota
vertailukriteerinä käyttäen (luku 9.2); sortBy
muodostaa järjestetyn kokoelman:
import scala.math.absimport scala.math.abs val 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:
sanat.maxByOption( _.length )res149: Option[String] = Some(kaikkein pisin) sanat.minByOption( _.length )res150: Option[String] = Some(lyhin) sanat.drop(100).minByOption( _.length )res151: Option[String] = None
Ä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 9.2). 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.)
import scala.Ordering.Double.TotalOrderingimport scala.Ordering.Double.TotalOrdering Vector(1.1, 3.0, 0.0, 2.2).sortedres152: Vector[Double] = Vector(0.0, 1.1, 2.2, 3.0) Vector(1.1, 3.0, 0.0, 2.2).maxres153: Double = 3.0 Vector(-10.0, 1.5, 9.5).maxBy( _.abs )res154: Double = -10.0
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 palautusarvo muodostetaan kokoelman
alkioiden perusteella (luku 6.4). 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
reduceLeft
on samansuuntainen, mutta se käyttää ensimmäistä alkiota lähtöarvona eikä
siis tarvitse parametrikseen kuin yhdistämisfunktion:
import scala.math.minimport scala.math.min val 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
reduceLeft
in palautusarvo 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:
val tyhjanSumma = tyhja.reduceLeftOption( _ + _ )tyhjanSumma: Option[Int] = None
Lisää kokoelmien metodeita Scala API -dokumentaatiossa.
Option
kokoelmatyyppinä
Option
on kokoelmatyyppi: kussakin Option
-oliossa on joko yksi alkio (Some
) tai
nolla (None
). Asiaa on puitu tarkemmin luvussa 8.2. 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.sizeres155: Int = 1 eiMitaan.sizeres156: Int = 0
foreach
:
jotain.foreach(println)100 eiMitaan.foreach(println) // ei tulosta mitään
contains
:
jotain.contains(100)res157: Boolean = true jotain.contains(50)res158: Boolean = false eiMitaan.contains(100)res159: Boolean = false
exists
:
jotain.exists( _ > 0 )res160: Boolean = true jotain.exists( _ < 0 )res161: Boolean = false eiMitaan.exists( _ > 0 )res162: Boolean = false
forall
:
jotain.forall( _ > 0 )res163: Boolean = true jotain.forall( _ < 0 )res164: Boolean = false eiMitaan.forall( _ > 0 )res165: Boolean = true
filter
:
jotain.filter( _ > 0 )res166: Option[Int] = Some(100) jotain.filter( _ < 0 )res167: Option[Int] = None eiMitaan.filter( _ > 0 )res168: Option[Int] = None
map
:
jotain.map( 2 * scala.math.Pi * _ )res169: Option[Double] = Some(628.3185307179587) kokeilu2.map( 2 * scala.math.Pi * _ )res170: Option[Double] = None
flatten
:
Some(jotain)res171: Some[Option[Int]] = Some(Some(100)) Some(eiMitaan)res172: Some[Option[Int]] = Some(None) Some(jotain).flattenres173: Option[Int] = Some(100) Some(eiMitaan).flattenres174: Option[Int] = None
flatMap
:
def tuhatPer(luku: Int) = if (luku != 0) Some(1000 / luku) else NonetuhatPer: (luku: Int)Option[Int] jotain.flatMap(tuhatPer)res175: Option[Int] = Some(10) Some(0).flatMap(tuhatPer)res176: Option[Int] = None eiMitaan.flatMap(tuhatPer)res177: Option[Int] = None
Alkioiden alustaminen funktiolla: tabulate
Kokoelmatyyppien yhteyteen on määritelty tabulate
-tehdasmetodi, 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:
Vector.tabulate(10)( indeksi => indeksi * 2 )res178: Vector[Int] = Vector(0, 2, 4, 6, 8, 10, 12, 14, 16, 18)
tabulate
muodostaa alkiot kutsumalla parametrina saamaansa
funktiota kullekin indeksille. Tässä tuplausfunktiota on
kutsuttu luvuille 0–9.Sama toimii myös useassa ulottuvuudessa:
Vector.tabulate(3, 4)( (eka, toka) => eka * 100 + toka )res179: Vector[Vector[Int]] = Vector(Vector(0, 1, 2, 3), Vector(100, 101, 102, 103), Vector(200, 201, 202, 203))
Laiskalistat ja väljä evaluointi
LazyList
-kokoelmatyyppi
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.1). 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:
val laiskaaDataa = LazyList(10.2, 32.1, 3.14159)laiskaaDataa: LazyList[Double] = LazyList(<not computed>) laiskaaDataa.mkString(" ")res180: String = 10.2 32.1 3.14159 val sanavektori = Vector("eka", "toka", "kolmas", "neljäs")sanavektori: Vector[String] = Vector(eka, toka, kolmas, neljäs) val laiskaSanalista = sanavektori.to(LazyList)laiskaSanalista: LazyList[String] = LazyList(<not computed>)
Laiskalistoilla on myös muista kokoelmista tuttuja metodeita. Alla muutama esimerkki:
laiskaSanalista.drop(2).headres181: String = kolmas laiskaSanalista.filter( _.length > 4 ).map( _ + "!" ).foreach(println)kolmas! neljäs!
Äskeiset kokoelmat olivat äärellisiä. Tutummista kokoelmatyypeistä poiketen laiskalista
voi olla päättymätön. continually
-tehdasmetodi tuottaa loputtoman listan:
val lista = LazyList.continually("Oliver")lista: LazyList[String] = LazyList(<not computed>) lista.take(5).foreach(println)Oliver Oliver Oliver Oliver Oliver
"Oliver"
(toistamiseen aina tarvittaessa). Koska
kyseessä on literaalilauseke, tämän listan alkiot ovat keskenään
identtisiä.take
palauttaa
parametrinsa mittaisen laiskalistan, joka on pätkä alkuperäisestä.Näin luodussa laiskalistassa voi olla myös keskenään erilaisia alkioita:
LazyList.continually( Random.nextInt(100) ).takeWhile( _ <= 90 ).mkString(",")res182: String = 0,65,83,38,75,33,11,18,75,51,3
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.1 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
object SayPlease extends App {
def report(input: String) = "The input is " + input.length + " characters long."
def inputs = LazyList.continually( readLine("Enter some text: ") )
inputs.takeWhile( _ != "please" ).map(report).foreach(println)
}
readLine
a 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
-tehdasmetodilla:
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)
.
def positiiviset(eka: Int): LazyList[Int] = eka #:: positiiviset(eka + 1)positiiviset: (eka: Int)LazyList[Int]
positiiviset(1).take(3).foreach(println)1
2
3
#::
muodostaa LazyList
in
yhdistelmänä: alkuun tulee vasemmalla
mainittu yksittäinen arvo ja perään
laitetaan oikealla mainittu laiskalista.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) luku }printtaaJaPalauta: (luku: Int)Int def kokeilu(luku: Int, luvunTuottavaLauseke: =>Int) = if (luku >= 0) luvunTuottavaLauseke else -1kokeilu: (luku: Int, luvunTuottavaLauseke: => Int)Int
=>
. 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 10 Palautan parametrini 100 res183: Int = 100 kokeilu(printtaaJaPalauta(-10), printtaaJaPalauta(100))Palautan parametrini -10 res184: Int = -1
kokeilu
-funktiolle.printtaaJaPalauta
tulee toisen kerran kutsutuksi.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
lazy val
:
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) eka * 10 else toka * 10Palautan parametrini 1 res185: Int = 10 if (eka > 0) eka * 10 else toka * 10res186: 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.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ä
lazy val
.printtaaJaPalauta
-funktion
lisätulosteita, koska eka
-muuttujalla on jo arvo.toka
-muuttujan
arvoa tarvittu eikä sen arvoa ole vielä edes määritetty, vaikka
tuo muuttuja if
-käskyssä esiintyykin.Toistaminen silmukoilla
for
-silmukka
for
-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) for (alkio <- puskuri) { 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
for
-avainsanan perässä ovat pakolliset.<-
oikealla puolella on lauseke,
joka kertoo, mistä arvoja noukitaan vuoron perään käsiteltäviksi.Silmukan rungossa voi yhdistellä erilaisia käskyjä. Esimerkiksi if
-valintakäskyä voi
käyttää:
for (alkio <- puskuri) { if (alkio > 10) { println("Tämä alkio on kymppiä isompi: " + alkio) } else { println("Tässä kohdassa on pieni alkio.") } }Tä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
Läpikäytävä kokoelma voi olla muukin, vaikkapa Range
-tyyppinen lukuväli tai merkkijono
(luku 5.6):
for (luku <- 10 to 15) { println(luku) }10 11 12 13 14 15 for (indeksi <- puskuri.indices) { 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") { println(merkki) }t e s t i
Tässä vielä yksi silmukka, joka käy läpi pareja (ks. Parit ja muut monikot yllä),
joita on muodostettu zipWithIndex
-metodilla (ks. Yleisiä kokoelmien metodeita yllä):
for ((alkio, indeksi) <- puskuri.zipWithIndex) { 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
-silmukoista.
Monipuolisempaa for
-silmukan käyttöä
Scalan for
-silmukalla on puolia, joita ei Ohjelmointi 1 -kurssilla
varsinaisesti esitellä tai tarvita. Silmukalla voi esimerkiksi tilan
muuttamisen sijaan tuottaa uuden kokoelman. Tähän käytetään
yield
-avainsanaa:
val vektori = Vector(100, 0, 20, 5, 0, 50)vektori: Vector[Int] = Vector(100, 0, 20, 5, 0, 50) for (luku <- vektori) yield luku + 100res187: Vector[Int] = Vector(200, 100, 120, 105, 100, 150) for (sana <- Vector("laama", "alpakka", "vikunja")) yield sana.lengthres188: Vector[Int] = Vector(5, 7, 7)
Samassa yhteydessä voi myös suodattaa arvoja:
for (luku <- vektori; if luku != 0) yield 100 / lukures189: Vector[Int] = Vector(1, 5, 20, 2)
for
-silmukat ovat toisenlainen tapa kirjoittaa foreach
-, map
-,
flatMap
- ja filter
-kutsuja; vrt. Kokoelmien käsittely korkeamman
asteen metodeilla yllä.
Sisäkkäiset silmukat
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) { println("Ulomman kierros alkaa.") for (merkki <- merkkeja) { println(s"luku nyt $luku ja merkki nyt $merkki") } println("Ulomman kierros päättyy.") }Ulomman 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 kaksi koodia tekevät saman:
for (luku <- lukuja) {
for (merkki <- merkkeja) {
println(luku + "," + merkki)
}
}
for (luku <- lukuja; merkki <- merkkeja) {
println(luku + "," + merkki)
}
do
-silmukka
do
-silmukan loppuun kirjoitetaan ehtolauseke, joka määrää, kauanko silmukan runkoa
toistetaan. Tässä yksi pikkuesimerkki luvusta 8.3:
var luku = 1luku: Int = 1 do { println(luku) luku += 4 println(luku) } while (luku < 10)1 5 5 9 9 13
do
-silmukan määrittelyssä käytetään alussa sanaa do
ja
lopussa sanaa while
. Välissä on silmukan runko aaltosulkeissa.
Älä jätä aaltosulkeita pois.while
-sanan perään kaarisulkeisiin kirjoitetun ehtolausekkeen
tulee olla Boolean
-tyyppinen. Se evaluoidaan aina silmukan
rungon suorittamisen jälkeen. Jos saadaan false
, niin silmukan
suoritus päättyy, muuten aloitetaan taas rungon alusta.
Kaarisulkeet ehdon ympärillä ovat pakolliset.do
-silmukan runko tulee toistettua yhden tai useampia
kertoja. Tässä esimerkissä se toistetaan kolmesti. Ensimmäisen
suorituskerran lopussa luku
-muuttujan arvo on 5, toisella
kerralla 9 ja kolmannella 13, jolloin jatkamisehto ei enää ole
voimassa.while
-silmukka
while
-silmukka on samantapainen kuin do
-silmukka, mutta sen jatkamisehto kirjoitetaan
silmukan alkuun ja tarkistetaan jokaisen suorituskierroksen aluksi eikä lopuksi:
var luku = 1luku: Int = 1 while (luku < 10) { println(luku) luku += 4 println(luku) }1 5 5 9 9 13
while
ja sen perässä jatkamisehto
sulkeissa. Tämä jatkamisehto tarkistetaan ensimmäisen kerran jo
ennen kuin runkoa on suoritettu kertaakaan.luku
on aluksi 1, on jatkamisehto luku < 10
heti aluksi
voimassa. Tässä tapauksessa silmukka tuottaa täsmälleen saman
tulosteen kuin do
-silmukkakin yllä.Toisin kuin do
-silmukan, while
-silmukan runkoa ei välttämättä suoriteta kertaakaan:
var luku = 20luku: Int = 20 while (luku < 10) { println(luku) luku += 4 println(luku) }
Lisäesimerkkejä luvussa 8.3.
Hakurakenteet (Map
)
Hakurakenne on kokoelma, jonka alkioina on avain–arvo-pareja (luku 8.4). 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.
Hakurakenteen voi luoda näin:
val suomestaEnglanniksi = Map("kissa" -> "cat", "laama" -> "llama", "tapiiri" -> "tapir", "koira" -> "puppy")suomestaEnglanniksi: Map[String,String] = Map(koira -> puppy, tapiiri -> tapir, kissa -> cat, laama -> llama)
Arvojen hakeminen: get
, contains
, apply
contains
-metodilla voi tutkia, onko tietty avain käytössä:
suomestaEnglanniksi.contains("tapiiri")res190: Boolean = true suomestaEnglanniksi.contains("Juhan af Grann")res191: Boolean = false
Arvon hakeminen avaimen perusteella onnistuu get
-metodia käyttäen. Se palauttaa
arvon Option
-kääreessä:
suomestaEnglanniksi.get("kissa")res192: Option[String] = Some(cat) suomestaEnglanniksi.get("Juhan af Grann")res193: Option[String] = None
Lyhyemminkin saa haettua, mutta tällöin puuttuva arvo tuottaa ajonaikaisen virheen:
suomestaEnglanniksi("kissa")res194: String = cat suomestaEnglanniksi("Juhan af Grann")java.util.NoSuchElementException: key not found: Juhan af Grann ...
Hakurakenteen muokkaaminen
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. Nyt kuitenkin kokeillaan muuttuvatilaista hakurakennetta,
joka otetaan erikseen käyttöön:
import scala.collection.mutable.Mapimport scala.collection.mutable.Map val suomestaEnglanniksi = Map("kissa" -> "cat", "laama" -> "llama", "tapiiri" -> "tapir", "koira" -> "puppy")suomestaEnglanniksi: Map[String,String] = Map(koira -> puppy, tapiiri -> tapir, kissa -> cat, laama -> llama)
Muuttuvatilaiseen hakurakenteeseen voi lisätä avain–arvo-pareja. Tässä kaksi eri tapaa (luku 8.4):
suomestaEnglanniksi("hiiri") = "mouse"suomestaEnglanniksi += "sika" -> "pig"res195: Map[String, String] = Map(koira -> puppy, tapiiri -> tapir, kissa -> cat, sika -> pig, hiiri -> mouse, laama -> llama)
Samoja käskyjä voi käyttää myös parin korvaamiseen: jos lisätty avain on jo hakurakenteessa, uusi pari korvaa vanhan.
Tässä vastaavasti kaksi eri tapaa poistaa pari muuttuvatilaisesta hakurakenteesta:
suomestaEnglanniksi.remove("tapiiri")res196: Option[String] = Some(tapir) suomestaEnglanniksi -= "laama"res197: Map[String, String] = Map(koira -> puppy, kissa -> cat, sika -> pig, hiiri -> mouse)
Epäonnistuneet haut ja vara-arvot: getOrElse
, withDefault
ym.
getOrElse
-metodille voi antaa parametriksi lausekkeen, joka määrittää "vara-arvon"
(luku 8.4):
val suomestaEnglanniksi = Map("kissa" -> "cat", "laama" -> "llama", "tapiiri" -> "tapir", "koira" -> "puppy")suomestaEnglanniksi: Map[String,String] = Map(koira -> puppy, tapiiri -> tapir, kissa -> cat, laama -> llama) suomestaEnglanniksi.getOrElse("kissa", "tuntematon hakusana")res198: String = cat suomestaEnglanniksi.getOrElse("Juhan af Grann", "tuntematon hakusana")res199: String = tuntematon hakusana
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:
import scala.collection.mutable.Mapimport scala.collection.mutable.Map val suomestaEnglanniksi = Map("kissa" -> "cat", "laama" -> "llama", "tapiiri" -> "tapir", "koira" -> "puppy")suomestaEnglanniksi: Map[String,String] = Map(koira -> puppy, tapiiri -> tapir, kissa -> cat, laama -> llama) suomestaEnglanniksi.getOrElseUpdate("lude", "bug")res200: String = bug suomestaEnglanniksires201: Map[String,String] = Map(lude -> bug, koira -> puppy, tapiiri -> tapir, kissa -> cat, laama -> llama)
Vaihtoehto äsken mainituille metodeille on määritellä koko hakurakenteelle yleinen vara-arvo (luku 8.4):
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")res202: String = cat englanniksi("Juhan af Grann")res203: String = ähäkutti
withDefaultValue
-metodille ilmoitetaan, mitä halutaan
käyttää "vara-arvona" silloin, kun haku on huti.withDefault
Äskeisessä esimerkissä vara-arvo oli aina sama. Metodilla withDefault
voit
asettaa hakurakenteelle "varafunktion", joka määrittää palautusarvoja 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")res204: String = cat englanniksi("Juhan af Grann")res205: String = hait sanaa Juhan af Grann muttei löytynyt
Hakurakenteen muodostaminen kokoelmasta: toMap
, groupBy
Kutsumalla toMap
-metodia voi hakurakenteen luoda minkä tahansa sellaisen kokoelman
perusteella, jonka alkioina on pareja (luku 9.2):
val elaimia = Vector("koira", "kissa", "akvaariokala", "saukko", "laama", "porsas")elaimia: Vector[String] = Vector(koira, kissa, akvaariokala, saukko, laama, porsas) val lukumaaria = Vector(2, 12, 35, 5, 7, 5)lukumaaria: Vector[Int] = Vector(2, 12, 35, 5, 7, 5) val parejaVektorissa = elaimia.zip(lukumaaria)parejaVektorissa: Vector[(String, Int)] = Vector((koira,2), (kissa,12), (akvaariokala,35), (saukko,5), (laama,7), (porsas,5)) val hakurakenne = parejaVektorissa.toMaphakurakenne: Map[String,Int] = Map(saukko -> 5, koira -> 2, porsas -> 5, kissa -> 12, laama -> 7, akvaariokala -> 35) hakurakenne("laama")res206: Int = 7
zip
-metodilla. Syntyy pareja sisältävä vektori.toMap
voi luoda hakurakenteen.Metodilla groupBy
muodostetaan hakurakenne, johon alkuperäisen kokoelman alkiot on
ryhmitelty sen mukaan, mitä parametriksi annettu funktio alkion kohdalla palauttaa:
val lukumaaria = Vector(2, 12, 35, 5, 7, 5)lukumaaria: Vector[Int] = Vector(2, 12, 35, 5, 7, 5) val ryhmiteltyParillisuudenMukaan = lukumaaria.groupBy( _ % 2 == 0 )ryhmiteltyParillisuudenMukaan: Map[Boolean,Vector[Int]] = Map(false -> Vector(35, 5, 7, 5), true -> Vector(2, 12)) val elaimia = Vector("koira", "kissa", "akvaariokala", "saukko", "laama", "porsas")elaimia: Vector[String] = Vector(koira, kissa, akvaariokala, saukko, laama, porsas) val ryhmiteltySananPituudenMukaan = elaimia.groupBy( _.length )ryhmiteltySananPituudenMukaan: Map[Int,Vector[String]] = Map(5 -> Vector(koira, kissa, laama), 4 -> Vector(kala), 6 -> Vector(saukko, porsas))
Sekä toMap
että groupBy
luovat tilaltaan muuttumattomia hakurakenteita.
Lisäesimerkkejä luvussa 9.2.
Muita hakurakenteiden metodeita: keys
, values
, map
ym.
Hakurakenteet ovat alkiokokoelmia, ja niillä on koko joukko yhteisiä metodeita muiden
kokoelmatyyppien kanssa (ks. Kokoelmien alkeita, Yleisiä kokoelmien metodeita
ja Kokoelmien käsittely korkeamman asteen metodeilla). Numeerisin indekseihin
perustuvia metodeita niillä ei ymmärrettävästi ole, mutta esimerkiksi isEmpty
, size
ja foreach
ja monet muut toimivat kyllä:
val englanniksi = Map("kissa" -> "cat", "tapiiri" -> "tapir", "koira" -> "dog")englanniksi: Map[String,String] = Map(koira -> dog, tapiiri -> tapir, kissa -> cat) englanniksi.isEmptyres207: Boolean = false englanniksi.sizeres208: Int = 3 englanniksi.foreach(println)(koira,dog) (tapiiri,tapir) (kissa,cat)
Nimenomaan hakurakenteille ominaisia ovat metodit keys
ja values
(luku 8.4),
jotka palauttavat pelkät avaimet tai pelkät arvot sisältävän kokoelman:
englanniksi.keys.foreach(println)koira tapiiri kissa englanniksi.values.foreach(println)dog tapir cat
Map
-olion map
-metodi (luku 8.4) käsittelee avain–arvo-pareja:
englanniksi.map( pari => pari._1 -> pari._2.length )res209: Map[String,Int] = Map(kissa -> 3, tapiiri -> 5, koira -> 3)
Metodi tuottaa uuden muuttumattoman hakurakenteen, jossa alkuperäisten parien tilalla on parametrifunktion tuottamat parit.
Lisää hakurakenteidenkin metodeista virallisessa dokumentaatiossa.
Ylä- ja alakäsitteitä
Yläkäsitettä ja sen alakäsitteitä voi kuvata määrittelemällä piirreluokan (trait
;
luku 7.2) joka liitetään alakäsitteiden määrittelyyn, tai määrittelemällä yliluokan
(luku 7.3) ja periyttämällä siitä aliluokkia.
Piirreluokat
Piirreluokka määritellään samaan tapaan kuin luokka mutta sanaa trait
käyttäen
(luku 7.2). Tämä piirreluokka kuvaa abstraktia kuvion käsitettä:
trait Shape {
def isBiggerThan(another: Shape) = this.area > another.area
def area: Double
}
isBiggerThan
-metodi, jolla voi verrata
kuvioiden pinta-aloja keskenään.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 niissä luokissa, joihin piirre
Shape
liitetään (ks. alta).Shape
-tyyppiseen olioon. Kaikilla tällaisilla olioilla
on jonkinlainen area
-metodi, joten voimme kutsu tuota metodia
vertailumetodin parametrille.Piirreluokan liittäminen luokkaan
Seuraaviin kahteen luokkaan on liitetty Shape
-piirre (luku 7.2). Ne edustavat
kuviokäsitteen alakäsitteitä:
class Circle(val radius: Double) extends Shape {
def area = scala.math.Pi * this.radius * this.radius
}
class Rectangle(val sideLength: Double, val anotherSideLength: Double) extends Shape {
def area = this.sideLength * this.anotherSideLength
}
extends
. Tästä seuraa, että
kaikki Circle
-tyyppiset oliot ovat paitsi ympyröitä myös
kuvioita. Niillä on mm. piirreluokassa Shape
määritelty
isBiggerThan
-metodi.Luokkaan voi liittää useita piirreluokkia. Ensimmäinen yläkäsite mainitaan extends
-sanan
perässä ja loput with
-sanoin eroteltuina:
class X extends A with B with C with D with Etc
Staattiset ja dynaamiset tyypit
Luvussa 7.2 erotetaan toisistaan staattinen ja dynaaminen tyyppi:
var kuvio: Shape = new Circle(1)kuvio: o1.shapes.Shape = o1.shapes.Circle@1a1a02e kuvio = new Rectangle(10, 5)kuvio: o1.shapes.Shape = o1.shapes.Rectangle@7b519d
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.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:
kuvio.isInstanceOf[Rectangle]res210: Boolean = true kuvio.isInstanceOf[Shape]res211: Boolean = true
Yllä kuviomuuttujan tyyppi oli erikseen määritelty Shape
ksi. Tässä ei, minkä vuoksi
sijoitus epäonnistuu:
var kokeilu = new Circle(1)kokeilu: o1.shapes.Circle = o1.shapes.Circle@1c4207e
kokeilu = new Rectangle(10, 5)<console>:11: error: type mismatch;
found : o1.shapes.Rectangle
required: o1.shapes.Circle
kokeilu = new Rectangle(10, 5)
^
Circle
, jolloin siihen voi sijoittaa vain
Circle
-tyyppisiä arvoja eikä muita kuvioita.Staattinen tyyppi rajoittaa arvojen käyttöä (luku 7.2):
var testi: Shape = new Circle(10)testi: o1.shapes.Shape = o1.shapes.Circle@9c8b50
testi.radius<console>:12: error: value radius is not a member of o1.shapes.Shape
testi.radius
^
test
staattinen tyyppi on Shape
. Mielivaltaiselle
Shape
-oliolle ei ole määritelty radius
-muuttujaa, vaikka
ympyröille onkin.match
-käskyllä voi tehdä dynaamiseen tyyppiin perustuvan valinnan:
testi match { case ympyra: Circle => println("Se on ympyrä, ja sen säde on " + 1«ympyra.radius») case _ => println("Se ei ole ympyrä.") }Se on ympyrä, ja sen säde on 10.0
Periytyminen
Luokka voi periä toisen luokan. Tässä käytetään luvun 7.3 tapaan
Rectangle
-luokkaa, jonka perii luokka Square
:
class Rectangle(val sideLength: Double, val anotherSideLength: Double) extends Shape {
def area = this.sideLength * this.anotherSideLength
}
class Square(size: Double) extends Rectangle(size, size)
extends
-sanaa, jonka perään kirjoitetaan perityn
luokan nimi. Aliluokka Square
perii nyt yliluokan Rectangle
,
ja Square
-oliot ovat nyt myös Rectangle
-tyyppisiä (ja
Shape
-tyyppisiä, koska Rectangle
-luokkaan on liitetty
Shape
-piirre).Square
on yksi konstruktoriparametri, joka kertoo
kunkin sivun mitan.Square
-oliota luodaan, niin tehdään samat
alustustoimenpiteet kuin Rectangle
-oliolle, kuitenkin niin,
että molemmiksi suorakaiteiden konstruktoriparametreiksi (eli
molemmiksi sivunpituuksiksi) laitetaan neliöolion saaman
konstruktoriparametrin arvo.Konkreettisessa luokassa kaikilla metodeilla on toteutus. Voidaan myös määritellä abstrakti luokka, jollaisessa saa olla abstrakteja, toteutuksettomia metodeita kuten piirreluokassakin. Tässä esimerkki luvusta 7.3:
abstract class Tapahtuma(val alvLisatty: Boolean) {
def kokonaishinta: Double
def verotonHinta = if (this.alvLisatty) this.kokonaishinta / 1.24 else this.kokonaishinta
}
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
Tapahtuma
-tyyppiset oliot kykenevät tämän metodin suorittamaan.Tässä vertailutaulukko samasta luvusta 7.3:
Piirreluokka | Abstrakti yliluokka | Konkreettinen yliluokka | |
---|---|---|---|
Voiko sisältää abstrakteja metodeita? | Voi. | Voi. | Ei voi. |
Voiko luoda suoraan ilmentymiä new llä? |
Ei voi. | Ei voi. | Voi. |
Voiko olla konstruktoriparametreja? | Ei voi. | Voi. | Voi. |
Voiko käyttää useita yläkäsitteinä
(
with -sanojen perässä)? |
Voi. | Ei voi. | Ei voi. |
Näitä tekniikoita voi myös yhdistellä keskenään. Luokka voi esimerkiksi periä yhden yliluokan ja siihen voi lisäksi liittyä piirreluokkia. Tai piirreluokka voi periä luokan.
Myös yksittäisoliot voivat periä luokkia ja niihin voi liittää piirteitä.
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:
class Yli {
def eka() = {
println("Yliluokan eka")
}
def toka() = {
println("Yliluokan toka")
}
}
class Ali extends Yli {
override def eka() = {
println("Aliluokan eka")
}
override def toka() = {
println("Aliluokan toka")
super.toka()
}
}
val kokeilu = new Alikokeilu: Ali = Ali@1bd9da5 kokeilu.eka()Aliluokan eka kokeilu.toka()Aliluokan toka Yliluokan toka
Ali
-tyyppisen olion eka
-metodi toimii yliluokan
toteutuksesta riippumattomasti. Korvaava toteutus ratkaisee.toka
-metoditoteutusta kutsutaan yliluokan
versiota metodista, joten...Ali
-tyyppisen olion metodi tuottaa ensin aliluokassa
määritellyn tulosteen ja tekee sitten sen, mitä korvattu
Yli
-luokan 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.
Scalan luokkahierarkia
Kaikki Scala-oliot ovat kattotyyppiä Any
. Sillä on välittömät aliluokat AnyVal
ja
AnyRef
:
AnyVal
-luokasta periytyvät tutut tietotyypitInt
,Double
,Boolean
,Char
,Unit
, ja muutama muu. Sille harvemmin laaditaan itse uusia aliluokkia ja moinen pitää erikseen ilmoittaa.AnyRef
, toiselta nimeltäänObject
, puolestaan on yliluokka kaikille muille (ei-piirre-)luokille ja yksittäisolioille. Esimerkiksi luokatString
jaVector
periytyvät tästä luokasta. Myös itse laatimasi luokat periytyvät automaattisestiAnyRef
istä ellet erikseen toisin määrittele.
Lisää aiheesta luvussa 7.3.
Alakäsitteiden rajaaminen: sealed
ja final
Sana sealed
piirreluokan tai tavallisen luokan alussa tarkoittaa, että tuolle luokalle
ei voi määritellä muita välittömiä alityyppejä kuin ne, jotka on samaan
tiedostoon kirjattu (luku 7.2). Esimerkiksi Option
-luokan määrittely alkaa näin:
sealed abstract class Option /* Etc. */
Option
-luokan perivä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.
Sana final
(luku 7.3) vastaavasti 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 alityypeissä.
Satunnaislukuja
Yksittäisolion Random
-metodit tuottavat (näennäis)satunnaislukuja:
import scala.util.Randomimport scala.util.Random Random.nextInt(10)res212: Int = 8 Random.nextInt(10)res213: Int = 6 Random.nextInt(10)res214: Int = 2
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 = new Random(74534161)generaattori1: Random = scala.util.Random@75fbc2df val generaattori2 = new Random(74534161)generaattori2: Random = scala.util.Random@3f92984e
generaattori1.nextInt(100)res215: Int = 53 generaattori1.nextInt(100)res216: Int = 38 generaattori1.nextInt(100)res217: Int = 97 generaattori2.nextInt(100)res218: Int = 53 generaattori2.nextInt(100)res219: Int = 38 generaattori2.nextInt(100)res220: Int = 97
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 7.4):
val lukuja = (1 to 10).toVectorlukuja: Vector[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) Random.shuffle(lukuja)res221: Vector[Int] = Vector(8, 9, 7, 4, 6, 1, 10, 2, 5, 3) Random.shuffle(lukuja)res222: Vector[Int] = Vector(8, 6, 4, 5, 9, 1, 3, 7, 2, 10)
Lisää satunnaisuudesta luvussa 3.6.
Tiedostonkäsittelyä
Tässä esimerkki tekstitiedoston lukemisesta (luku 12.1). Ohjelma tulostaa tiedoston
example.txt
kunkin rivin ja sen eteen rivinumeron:
import scala.io.Source
object TulostaNumeroituna extends App {
val tiedosto = Source.fromFile("alikansio/esimerkki.txt")
try {
var rivinumero = 1
for (rivi <- tiedosto.getLines) {
println(rivinumero + ": " + rivi)
rivinumero += 1
}
} finally {
tiedosto.close()
}
}
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.getLines
ia kutsumalla saadaan. (On myös muita tapoja käydä
läpi tiedoston sisältöä kuin rivi kerrallaan; ks. luku 12.1.)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:
import java.io.PrintWriter
import scala.util.Random
object WritingExample extends App {
val fileName = "examplefolder/random.txt"
val file = new PrintWriter(fileName)
try {
for (n <- 1 to 10000) {
file.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.")
} finally {
file.close()
}
}
PrintWriter
-olion voi luoda näin. Parametriksi annetaan
kirjoitettavan tiedoston nimi.println
-metodilla voi kirjoittaa tiedostoon yhden rivin
tekstiä.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.
class Kappale(var vari: Color) {
var sijainti = new Pos(10, 10)
def liiku() = {
this.sijainti = this.sijainti.add(1, 1)
}
def palaa() = {
this.sijainti = new Pos(10, 10)
}
}
Laaditaan tämän näköinen käyttöliittymä, jossa kappale on piirretty kaksiväristä taustaa vasten ympyränä:
Toteuttava koodi:
object ViewExample extends App {
val kappale = new Kappale(Blue)
val tausta = rectangle(200, 400, Red).leftOf(rectangle(200, 400, Blue))
val gui = new View(kappale, 10, "A Diagonally Moving Thing") {
def makePic = {
val kappaleenKuva = circle(20, kappale.vari)
tausta.place(kappaleenKuva, kappale.sijainti)
}
override def onTick() = {
kappale.liiku()
}
override def onMouseMove(kursori: Pos) = {
kappale.vari = if (kursori.x < 200) Red else Blue
}
override def onClick(klikkaus: MouseClicked) = {
if (klikkaus.clicks > 1) {
kappale.palaa()
}
}
override def isDone = kappale.pos.x > 400
}
gui.start()
}
View
-oliota luodessa on mainittava, mihin olioon tarjotaan
näkymä (tässä: kappaleolioon). Vapaaehtoisina lisäparametreina
voi antaa mm. ajan tikitysnopeuden (tässä: 10) ja ikkunan otsikon.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.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ä laita sitä näkyviin eikä aloita
ajan "tikitystä". Nämä hoituvat start
-metodia kutsumalla.Lisätietoja luvuista 3.1, 3.6 ja luokan dokumentaatiosta.
Swing-käyttöliittymäkirjasto
GUI-ohjelmointiin tarkoitettua Swing-kirjastoa on esitelty luvussa 12.3. Tässä kokoava esimerkki sieltä:
import scala.swing._
import scala.swing.event._
object Tapahtumakokeilu extends SimpleSwingApplication {
val ekaNappi = new Button("Paina minua")
val tokaNappi = new Button("Ei kun MINUA!")
val kehote = new Label("Paina jompaakumpaa napeista.")
val kaikkiJutut = new BoxPanel(Orientation.Vertical)
kaikkiJutut.contents += kehote
kaikkiJutut.contents += ekaNappi
kaikkiJutut.contents += tokaNappi
val nappulaikkuna = new MainFrame
nappulaikkuna.contents = kaikkiJutut
nappulaikkuna.title = "Kokeiluikkuna"
this.listenTo(ekaNappi)
this.listenTo(tokaNappi)
this.reactions += {
case painallus: ButtonClicked =>
val lahdenappula = painallus.source
val nappulanTeksti = lahdenappula.text
Dialog.showMessage(kaikkiJutut, "Painoit nappia, jossa lukee: " + nappulanTeksti, "Viesti")
}
def top = this.nappulaikkuna
}
painallus
, johon on tallentunut
tapahtumaa kuvaava ButtonClicked
-tyyppinen olio.SimpleSwingApplication
it tarvitsevat pääikkunan (top
), joka
tulee näkyviin käynnistäessä.Varatut sanat
Scala-kielen varatut sanat eli sanat, joita ei voi käyttää tunnuksina, ovat:
abstract case catch class def do else extends false final
finally for forSome if implicit import lazy match new null
object override package private protected return sealed super then this
throw trait try true type val var while with yield
_ : = => <<: <% >: # @ ⇒ ←
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.
Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.
Tehtävien automaattisen arvioinnin ovat toteuttaneet: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä ja Aleksi Vartiainen.
Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.
Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista suunnittelivat Juha Sorva ja Teemu Sirkiä. Teemu Sirkiä ja Riku Autio toteuttivat ne apunaan Teemun aiemmin rakentamat työkalut Jsvee- ja Kelmu.
Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset laati Juha Sorva.
O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.
Tapa, jolla käytämme O1Libraryn työkaluja (kuten Pic
) yksinkertaiseen graafiseen
ohjelmointiin, on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi
oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.
Oppimisalusta A+ luotiin alun perin Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Nykyään tätä avoimen lähdekoodin projektia kehittää Tietotekniikan laitoksen opetusteknologiatiimi ja tarjoaa palveluna laitoksen IT-tuki. Pääkehittäjänä on tällä hetkellä Markku Riekkinen, jonka lisäksi A+:aa ovat kehittäneet kymmenet Aallon opiskelijat ja muut.
A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen ovat luoneet Nikolai Denissov, Olli Kiljunen, Nikolas Drosdek, Styliani Tsovou, Jaakko Närhi ja Paweł Stróżański yhteistyössä Juha Sorvan, Otto Seppälän, Arto Hellaksen ja muiden kanssa.
Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.
Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.