Scalaa kootusti

Tämä sivu kuvailee valikoituja Scala-kielen ominaisuuksia ja Scalan oheiskirjastojen sisältämiä työkaluja. Sivulla on niistä pieniä, irrallisia esimerkkejä. Voit oppimateriaalin varsinaisiin lukuihin jo tutustuttuasi käydä kertaamassa yksityiskohtia täältä.

Tämä kooste ei kata koko Scala-kieltä, vaan siinä painottuvat rakenteet, joita Ohjelmointi 1 -kurssilla muutenkin käsitellään. Mukana on muutamia Ohjelmointi 1:n oman apukirjaston työkaluja.

Tämä on ainoastaan luettelo eräistä työkaluista. Alla ei opeteta periaatteita tai käsitteitä eikä kerrota, mitä esitellyillä välineillä kannattaa tehdä; niistä asioista opit oppimateriaalin varsinaisissa luvuissa. Sivu ei etene nikamalleen samassa järjestyksessä kuin nuo luvut.

Etkö löydä etsimääsi?

Vastauksia voi löytyä myös näiden linkkien kautta:

Pidemmän päälle kannattaa opetella lukemaan Scalan omaa dokumentaatiota, vaikka se alkeiskurssilaiselle osin vaikeaselkoista ovatkin.

Jos olisit kaivannut tälle sivulle jotakin, mitä täällä ei ole, voit kertoa asiasta sivun lopun palautelomakkeella tai suoraan sähköpostitse osoitteeseen juha.sorva@aalto.fi.

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 mukaisesti):

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
"nefer" * 3res7: String = nefernefernefer

Yksittäisiä merkkejä voi kuvata tyypillä Char (luku 5.2). Char-literaali muodostetaan heittomerkeillä:

'a'res8: Char = a
'!'res9: Char = !

Lisää merkkijonojen käsittelyä löytyy 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):

muutettavissares12: Int = 151
muutettavissa += 10muutettavissa -= 100muutettavissa *= 2muutettavissamuutettavissa: Int = 122

Kommentteja

Ohjelmakoodin kirjoitetut kommentit (luku 1.2) eivät vaikuta ohjelman suoritukseen.

// Tämä on yksirivinen kommentti. Se alkaa kahdella kauttaviivalla ja päättyy rivin loppuun.

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 voi automaattisesti tuottaa Scaladoc-sivuja.

Pakkaukset ja kirjastot

Pakkausten käyttö

Scalan peruskirjastojen (luku 3.2) ja muiden pakkausten työkaluja voi käyttää kirjaamalla pakkauksen nimen käytetyn funktion tai muun työkalun nimen eteen. Tässä käytetään abs-itseisarvofunktiota pakkauksesta scala.math:

scala.math.abs(-50)res13: Int = 50

Itse asiassa yleispakkauksen scala sisältö on aina automaattisesti käytössä, joten saman voi sanoa lyhyemminkin viittaamalla vain alipakkaukseen math:

math.abs(-50)res14: Int = 50

Yleispakkauksesta scala löytyvät mm. tietotyypit Int ja Double, kokoelmatyypit Vector ja List sekä tulostusfunktio println. Näitä työkaluja voi käyttää mainitsematta pakkauksen nimeä lainkaan. Ei siis tarvitse kirjoittaa esimerkiksi scala.Int, vaikka se sallittua onkin.

Usein pakkausten nimien toistuvan kirjoittamisen voi välttää import-käskyllä:

Käyttöönotto import-käskyllä

Pakkauksen sisältämän työkalun käyttöönotto (luku 1.6):

import scala.math.absabs(-50)res15: Int = 50
abs(100)res16: Int = 100

Nyt ei tarvita pakkauksen nimeä.

Näin otetaan käyttöön pakkauksen koko sisältö kerralla:

import scala.math.*

import-käskyt kirjoitetaan usein kooditiedoston alkuun, jolloin mainitut työkalut ovat käytössä koko tiedoston sisältämässä koodissa. Käskyn voi sijoittaa myös muualle: esimerkiksi import funktion rungon alussa tuo työkalun käyttöön vain kyseiseen funktioon.

Pakkauksen määritteleminen

Itse laadittujen työkalujen pakkaukset merkitään Scala-kooditiedostojen alkuun tällaisella määrittelyllä (luku 2.6):

package pakkauksen.kenties.moniosainen.nimi

Tiedostot tallennetaan pakkausten nimiä vastaaviin kansioihin.

Yleisiä funktioita scala.math-pakkauksesta

Muutama yleishyödyllinen funktio pakkauksesta scala.math:

import scala.math.*val itseisarvo = abs(-50)itseisarvo: Int = 50
val potenssi = pow(10, 3)potenssi: Double = 1000.0
val neliojuuri = sqrt(25)neliojuuri: Double = 5.0
val sini = sin(1)sini: Double = 0.8414709848078965
val kahdestaIsompi = max(2, 10)kahdestaIsompi: Int = 10
val kahdestaPienempi = min(2, 10)kahdestaPienempi: Int = 2

Samasta pakkauksesta löytyvät mm. muut trigonometriset funktiot (cos, atan jne.), cbrt (kuutiojuuri), hypot (hypotenuusa; parametreiksi kaksi kateetinmittaa), floor (alaspäin pyöristys), ceil (ylöspäin pyöristys), round (lähimpään pyöristys), log ja log10 (logaritmeja). Koko luettelo löytyy pakkauksen dokumentaatiosta.

Osia muiden Scala APIn pakkausten sisällöstä on esitelty tämän sivun muissa kappaleissa aiheittain.

Syötettä ja tulostetta: println, readLine

Tekstikonsoliin tai REPLiin tulostaminen onnistuu println-käskyllä:

println(100 + 1)101
println("laama")laama

Alla on esimerkkejä näppäimistösyötteen lukemisesta tekstikonsolissa (luku 2.7). Esimerkeissä oletetaan, että käsky 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

Parametrien tyypit on kirjattava kaksoispisteiden perään.

Muista muutkin välimerkit.

Kun funktion rungon muodostaa vain yksi lauseke, funktion paluuarvo saadaan evaluoimalla tuo lauseke.

Funktion kutsuminen

Funktiokutsu:

keskiarvo(10.0, 12.5)res17: Double = 11.25

Funktiokutsu on lauseke, jonka arvo on funktion palauttama arvo.

Monirivinen funktio

Kun funktion runko koostuu useasta peräkkäisestä käskystä, se jaetaan usealle riville ja sisennetään def-riviä syvemmälle. Tässä esimerkki luvusta 1.7:

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

Tavanomaisesti sisennetään kahdella välilyönnillä, kuten tässä, mutta tärkeintä on, että sisennykset ovat yhtenevät.

Viimeiseksi evaluoitavan lausekkeen arvosta tulee funktion paluuarvo.

Kun funktio on vaikutuksellinen, on sen runko tapana rivittää ja sisentää noin, vaikka rungossa olisi vain yksikin rivi; ks. tyyliopas.

Monelle riville jaetun funktion loppuun voi kirjoittaa loppumerkin:

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
end verot

Näin on yleensä tapana tehdä lähinnä silloin, jos funktion koodi on pitkä tai sisältää tyhjiä rivejä (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) = toka + eka * lisaparametri
kokeilu(10, "laama")(100)res18: String = laama1000

Paluuarvoista

Kaikissa yllä olevissa esimerkeissä paluuarvon tyyppi on jätetty kirjaamatta koodiin, mikä on sallittua tyyppipäättelyn vuoksi. Paluuarvon tyypin saa erikseen kirjatakin (luku 1.8), kuten näissä esimerkeissä:

def keskiarvo(eka: Double, toka: Double): Double = (eka + toka) / 2

def palautaTeksti: String = "Funktiokutsu palautaTeksti tuottaa aina tämän merkkijonon."

Tietyissä tilanteissa paluuarvon tyyppi on pakko kirjata. Näin on eritoten silloin, jos funktio kutsuu itsensä kanssa samannimistä funktiota eli joko

Arvon palauttaminen return-käskyllä

Arvon voi (muttei ole Scalassa tapana) määrätä palautettavaksi myös return-käskyllä (luku 9.1), 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.

Funktiolle, jossa return-käskyä käytetään, on kirjattava paluuarvon tyyppi.

Yksittäisoliot

Olion määritteleminen: metodit, muuttujat, this

Yksittäisen olion määrittely luvussa 2.2 tarkemmin kuvaillusta esimerkistä:

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"

end tyontekija

Avainsanan object perään kirjoitetaan oliolle valittu nimi ja kaksoispiste. (Ei yhtäsuuruusmerkkiä kuten funktioiden määrittelyissä.)

Sisennykset kertovat, mitkä osat kuuluvat olion runkoon.

Olion rungon loppuun kirjoitetaan usein loppumerkki. Se ei ole pakollinen mutta selkiyttää tällaista koodia, jossa on välissä tyhjiä rivejä (ks. tyyliopas.

Olion tietoja tallennettuna muuttujiin. Osa on tässä muuttumattomia (val), osa ei (var).

Olioon liitetyt funktiot eli metodit alkavat def-sanalla.

Olion metodia suoritettaessa this viittaa olioon itseensä. Esimerkiksi tässä lausekkeen this.nimi arvo on olion oman nimi-muuttujan arvo. this-sana ei kuitenkaan ole aina pakollinen; ks. luku 2.2.

Yksittäisolion käyttö: pistenotaatio

Olion muuttujien käyttö:

tyontekija.kkpalkkares19: Double = 5000.0
tyontekija.tyoaika = 0.6

Metodikutsuja:

tyontekija.korotaPalkkaa(1.1)tyontekija.ikaVuonna(2024)res20: Int = 59

"Pakkausoliot" ja import

Yksittäisoliota voi käyttää "pakkauksen kaltaisesti": varastona, josta voi importata käyttöön työkaluja kuten funktioita, luokkia ja toisia olioita (luku 5.3). Esimerkki:

package omat

object kokeilu:
  def tuplaa(luku: Int) = luku * 2
  def triplaa(luku: Int) = luku * 3

Nämä funktiot on määritelty kokeilu-nimisen olion sisään, jota on tarkoitus käyttää "pakkausmaisesti".

kokeilu-olio on määritelty omat-nimisen pakkauksen yhteyteen. Se on tavallaan omat-pakkauksen alipakkaus.

Tuo koodi tulee tallentaa omat-nimisessä kansiossa tiedostoon, jonka nimi voi olla esimerkiksi kokeilu.scala. Oliosta voi nyt ottaa työkaluja käyttöön tavallisella import-käskyllä:

import omat.kokeilu.*tuplaa(10)res21: Int = 20
triplaa(10)res22: Int = 30

Mitään teknisesti poikkeavaa kokeilu-yksittäisoliossa ei verrattuna muihin yksittäisolioihin ole. Tässä vain päätimme käyttää sitä pakkauksen kaltaisesti ja importata sen osia.

Sovelluksen käynnistäminen

O1-kurssilla esitellään kaksi tapaa määritellä, miten sovellusohjelma lähtee liikkeelle: käynnistysfunktio ja käynnistysolio. Ensimmäinen tapa on tietyin tavoin joustavampi ja monissa lähteissä suositellumpi, mutta tämän kurssin ohjelmissa kumpikin tapa toimii ihan hyvin; käytämme joissakin ohjelmissa yhtä ja toisissa toista.

Vaihtoehto 1: käynnistysfunktio

Käynnistysfunktio (luku 2.7) on funktio, joka toimii sovelluksen käynnistyskohtana:

@main def kaynnistaTestiohjelma() =
  println("Nämä rivit suoritetaan, kun sovellus ajetaan.")
  println("Tässä yksinkertaisessa sovelluksessa ei muuta olekaan kuin nämä tulostuskäskyt.")
  println("Monimutkaisemmassa ohjelmassa täältä käynnistettäisiin muita ohjelman osia.")

@main merkitsee, että tämä muuten ihan tavallinen funktio toimii käynnistysfunktiona.

@main-merkintää ei voi lisätä mille tahansa funktiolle, vaan kyseessä on oltava joko suoraan pakkauksen sisään deffattu funktio tai yksittäisolion metodi. (Käynnistysfunktioksi ei voi merkitä luokan metodia, koska luokasta pitää olla ilmentymä ennen kuin sellaista metodia voi kutsua, eikä ilmentymää käynnistyshetkellä ole.)

Vaihtoehto 2: käynnistysolio

Käynnistysolio (luku 2.7) on yksittäisolio, joka toimii sovelluksen käynnistyskohtana:

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ä käynnistettäisiin muita ohjelman osia.")

extends App määrittelee, että kyseessä on käynnistysolio. (Tarkemmin sanoen oliosta tulee App-piirreluokan yksittäistapaus; ks. Piirreluokat alempaa.)

Luokat (ja lisää olioista)

Luokan määrittely

Tässä luvusta 2.4 esimerkkiluokka, jolla voi kuvata keskenään erilaisia työntekijöitä. Kukin tämän luokan ilmentymä on oma Tyontekija-tyyppinen olionsa, jolla on omat tiedot:

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.
end Tyontekija

Avainsana class luokan nimen edessä. Kuten yksittäisolioiden myös luokkien määrittelyyn tulee kaksoispiste, sisennykset ja (vapaaehtoinen) loppumerkki.

Luontiparametrit: kun tästä luokasta luodaan ilmentymä, on annettava nimi, syntymävuosi ja palkka.

Luokan sisään kirjoitettu koodi metodien määrittelyt poislukien toimii konstruktorina: se suoritetaan kunkin uuden ilmentymän alustamiseksi. Tässä alustetaan useimmat olion ilmentymämuuttujat luontiparametrien arvoilla sekä asetetaan työajaksi luontiparametreista riippumatta aina alkuarvo 1.0.

Metodit määritellään aivan kuin yksittäisolioille. Sana this viittaa luokankin koodissa siihen olioon, jolle metodia kutsutaan. Esimerkiksi tässä lasketaan ikä juuri metodia suorittavan ilmentymän syntynyt-muuttujan arvon perusteella.

Yllä olevan luokkamäärittelyn voi kirjoittaa lyhyemminkin (luku 2.4):

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.
end Tyontekija

Tässä hyödynnetään mahdollisuutta määritellä kerralla sekä ilmentymämuuttujat että ne luontiparametrit, jotka määräävät noiden ilmentymämuuttujien (alku)arvot.

Neljäs ilmentymämuuttujista ei saa arvoaan suoraan luontiparametrista. Se määritellään erikseen.

Ilmentymien luominen ja käyttö

Yllä kuvattua Tyontekija-luokkaa voi käyttää näin (luku 2.3):

Tyontekija("Eugenia Enkeli", 1963, 5500)res23: o1.Tyontekija = o1.Tyontekija@1145e21

Luodaan ilmentymä kirjoittamalla luokan nimi ja sulkeissa arvot luontiparametreille. (Jos luontiparametreja ei olisi, kirjoitettaisiin nimen perään tyhjät kaarisulkeet.)

Lausekkeen arvo on viittaus uuteen olioon, joka on luokan ilmentymä.

Muuttujaan voi tallentaa viittauksen luotuun olioon. Tällöin oliota voi käyttää helposti muuttujan nimen kautta.

val juuriPalkattu = Tyontekija("Teija Tonkeli", 1985, 3000)juuriPalkattu: o1.Tyontekija = o1.Tyontekija@704234
juuriPalkattu.ikaVuonna(2024)res24: Int = 39
println(juuriPalkattu.kuvaus)Teija Tonkeli (s. 1985), palkka 1.0 * 3000.0 euroa

Luokan räätälöinti yksittäisolioksi

Voidaan määritellä yksittäisolio, joka on luokan ilmentymien kaltainen mutta poikkeaa niistä tavalla tai toisella.

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ää. Seuraava yksittäinen henkilöolio kuitenkin osaa. Lisäksi yksi sen metodeista toimii toisin kuin muiden henkilöolioiden:

object realistinenTerasmies extends Henkilo("Clark"):
  def lenna = "WOOSH!"
  override def reagoiKryptoniittiin = "GARRRRGH!"

Määrittelemme yksittäisolion tavalliseen tapaan, mutta ilmoitamme sen olevan eräänlainen henkilö. Räätälöimme tämän "erikoishenkilön" seuraavilla riveillä omanlaisekseen.

Tällä oliolla henkilöllä on lisämetodi lenna.

Toinen oliokohtaisista metodeista korvaa luokan yleisemmän määrittelyn, mikä tulee merkitä override-sanalla.

(Tämä on itse asiassa esimerkki käsitteiden välisestä periytymisestä; ks. Periytyminen alla)

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)res25: Int = 2

Tutumpi lauseke 1 + 1 toimii myös, koska yksiparametrista metodia voi kutsua myös operaattorinotaatiolla, jossa piste ja sulkeet jätetään pois. Sama käy myös itse laadituille metodeille:

juuriPalkattu ikaVuonna 2024res26: Int = 39

Operaattorinotaatiota kannattaa kuitenkin yleensä välttää, jos metodin nimi on sanallinen eikä operaattorimainen symboli kuten +.

Kuvankäsittelyä O1:n kirjastolla

Kurssin oheismoduuli O1Library on ohjelmakirjasto, jonka pakkaus o1 tarjoaa työkaluja mm. graafiseen ohjelmointiin ja on runsaassa käytössä kurssilla.

O1Libraryn työkaluja esitellään kurssimateriaalin luvuissa ja tuon moduulin dokumentaatiossa. Alla on lyhyt yhteenveto tärkeimmistä.

Värit: o1.Color

Värejä kuvaa luokka Color (luku 1.3). Useita tämän luokan ilmentymiä on määritelty vakioiksi o1-pakkaukseen:

import o1.*Redres27: Color = Red
RoyalBlueres28: Color = RoyalBlue

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.redres29: Int = 220
RoyalBlue.blueres30: Int = 225

Värillä on R-, G- ja B-komponenttien lisäksi myös ns. alfakanava eli läpinäkymättömyys.

Red.opacityres31: Int = 255
val lapikuultavaPunainen = Color(255, 0, 0, 100)lapikuultavaPunainen: Color = Color(255, 0, 0, opacity: 100)

Väri, jonka opacity on vain 100 on varsin läpikuultava. Nolla olisi tarkoittanut täysin läpinäkyvää ja 255 läpinäkymätöntä. Ellei toisin määritellä, väri on täysin läpinäkymätön.

Sijainnit: o1.Pos

Sijainteja kaksiulotteisessa koordinaatistossa kuvaa luokka o1.Pos (luku 2.5).

 val eka = 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.

Kumpaakin koordinaattia voi tutkia erikseen:

eka.xres32: Double = 15.5
toka.yres33: 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. luvussa 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

Moduulin sisältä ladattu kuvatiedosto voi olla samassa moduulissa lataavan koodin kanssa tai O1Library-moduulin pics-kansiossa (tai jossain muualla ohjelman luokkapolussa).

Kuvilla on leveys ja korkeus pikseleinä:

netistaLadattu.widthres34: Double = 135.0
netistaLadattu.heightres35: 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
../_images/pic_leftOf_below_onto.png

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):

../_images/pic_place.png
val pikkukuva = rectangle(10, 20, Black)pikkukuva: Pic = rectangle-shape
val pikkukuvaTaustaaVasten = kaide.place(pikkukuva, Pos(30, 80))pikkukuvaTaustaaVasten: Pic = combined pic
val ympyrakinSamaanKuvaan = pikkukuvaTaustaaVasten.place(ympyra, Pos(150, 150))ympyrakinSamaanKuvaan: Pic = combined pic

Tässä annetaan place-metodille sijoituskohta koordinaattiparina (jossa x kasvaa oikealle, y alas.) Asemoitavan kuvan keskikohta tulee noihin koordinaatteihin.

Suurehko ympyrä ei mahdu kokonaisuudessaan taustana toimivaa suorakaidetta vasten. Tällöin place jättää ylijäämän pois näkyvistä.

Tässä osittainen luettelo kuvaolioiden toiminnoista:

  • Sijoittelu vierekkäin ja allekkain: above, below, leftOf, rightOf (luku 2.3).

  • Sijoittelu eteen ja taakse: onto, against, place (luvut 2.3 ja 2.5).

  • Asemointi ankkureilla (esim. "tämän kuvan vasen yläkulma tuon kuvan yläreunan keskelle"): ks. luvun 2.5 loppu.

  • 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

Colorin, Posin ja Picin 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 kaksiulotteisessa Pos-koordinaatistossa (luvut 3.6 ja 4.4; dokumentaatio).

  • Luokka Grid kuvaa kaksiulotteisia ruudukkoja (luku 8.1; dokumentaatio). Sen kanssa yhteen sopivat

    • luokka GridPos, joka kuvaa sijainteja ruudukossa (luvut 6.3 ja 8.1; dokumentaatio), ja

    • luokka CompassDir, joka kuvaa pääilmansuuntia ruudukkotyyppisessä koordinaatistossa (luku 6.3; dokumentaatio).

  • 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.

falseres36: Boolean = false
val tamanMuuttujanArvoOnTosi = truetamanMuuttujanArvoOnTosi: Boolean = true

Vertailuoperaattorit

Vertailuoperaattorit tuottavat totuusarvoja (luku 3.3):

10 <= 10res37: Boolean = true
20 < (10 + 10)res38: Boolean = false
val ikavuodet = 20ikavuodet: Int = 20
val onAikuinen = ikavuodet >= 18onAikuinen: Boolean = true
ikavuodet == 30res39: Boolean = false
20 != ikavuodetres40: Boolean = false

Yhtäsuuruutta vertaillessa on käytettävä kahta yhtäsuuruusmerkkiä.

Operaattorilla != 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)res41: Boolean = true
jakaja != 0 && jaettava / jakaja < 10res42: Boolean = false
jakaja == 0 || jaettava / jakaja >= 10res43: Boolean = true
jaettava / jakaja >= 10 || jakaja == 0res44: 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 >= 10res45: Boolean = true
jakaja != 0 && jaettava / jakaja < 10res46: Boolean = false

Olemattomia arvoja

Option, Some ja None

Tämän esimerkkifunktion paluuarvo on tyyppiä Option[Int] (luku 4.3). Funktio palauttaa joko jakolaskun lopputuloksen käärittynä Some-olioon tai None, jos osamäärää ei voi määrittää:

def osamaara(jaettava: Int, jakaja: Int) =
  if jakaja == 0 then None else Some(jaettava / jakaja)
osamaara(100, 5)res47: Option[Int] = Some(20)
osamaara(100, 0)res48: 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.

Hakasulkeisiin kirjoitetaan tyyppiparametri eli kääreen sisällä mahdollisesti olevan arvon tyyppi.

Jos olisimme jättäneet tyyppimerkinnän tästä pois, ei sijoituskäskystä voisi päätellä, millainen Some-arvo testi-muuttujaan voitaisiin sijoittaa.

Option-tyypin sijaan voisi käyttää null-arvoa, mutta se ei ole useimmissa Scala-ohjelmissa lainkaan kannatettavaa (luku 4.3).

Option-olioiden metodeita

Metodeilla isDefined ja isEmpty voi tutkia, onko kääre tyhjä:

val kaarittyLuku = Some(100)kaarittyLuku: Option[Int] = Some(100)
kaarittyLuku.isDefinedres49: Boolean = true
kaarittyLuku.isEmptyres50: Boolean = false
None.isDefinedres51: Boolean = false
None.isEmptyres52: Boolean = true

contains-metodi tarkistaa, onko kääreessä tietty arvo:

kaarittyLuku.contains(123456)res53: Boolean = false
kaarittyLuku.contains(100)res54: Boolean = true
None.contains(123456)res55: Boolean = false
None.contains(100)res56: Boolean = false

Metodi getOrElse palauttaa arvon kääreen sisältä. Sille annetaan parametrilauseke, joka määrää, mitä metodi palauttaa kääreen ollessa tyhjä:

kaarittyLuku.getOrElse(12345)res57: Int = 100
None.getOrElse(12345)res58: Int = 12345

Samantapainen metodi orElse palauttaa Option-olion itsensä, jos kyseessä on Some, tai sille annetun parametrilausekkeen arvon, jos kyseessä on None. Se siis eroaa getOrElsestä sikäli, ettei se pura käärettä:

kaarittyLuku.orElse(Some(54321))res59: Option[Int] = Some(100)
None.getOrElse(Some(54321))res60: Option[Int] = Some(54321)

Lisää Optionista

Option-tyyppisten arvojen käsittelyyn sopivat myös

  • match-valintakäsky, josta kerrotaan lisää tuossa alla, ja

  • monet korkeamman asteen metodit, joista on kooste alempana kohdassa Option kokoelmatyyppinä.

Valintakäskyt if ja match

if-perusteet

if-käsky (luku 3.4) valitsee kahdesta vaihtoehdosta evaluoimalla ehtolausekkeen:

val luku = 100luku: Int = 100
if luku > 0 then luku * 2 else 10res61: Int = 200
if luku < 0 then luku * 2 else 10res62: Int = 10

Ehtolauseke kirjoitetaan if ja then sanojen väliin. Ehtona voi olla mikä tahansa Boolean-tyyppinen lauseke.

if-käskyllä muodostettua lauseketta voi käyttää muiden lausekkeiden tapaan esimerkiksi muuttujaan sijoitettaessa tai funktion parametrina:

val valinnanTulos = if luku > 100 then 10 else 20valinnanTulos: Int = 20
println(if luku > 100 then 10 else 20)20

Jos valinnaisessa osassa on peräkkäisiä käskyjä, rivitetään ja sisennetään (mikä on muutenkin tapana, jos käsky on vaikutuksellinen; ks. tyyliopas):

if luku > 0 then
  println("Luku on positiivinen.")
  println("Tarkemmin sanoen se on: " + luku)
else
  println("Kyseessä ei ole positiivinen luku.")Luku on positiivinen.
Tarkemmin sanoen se on: 100

Jos riittää, että ehdon ollessa totta suoritetaan tietty toimenpide ja muuten ei tehdä mitään, niin else-osion voi jättää pois:

if luku != 0 then
  println("Osamäärä on: " + 1000 / luku)
println("Loppu")Osamäärä on: 10
Loppu.

Viimeinen tulostuskäsky ei kuulu if-käskyyn vaan on sen perässä. Tämä koodinpätkä siis tulostaa lopuksi "Loppu" riippumatta siitä, onko luku-muuttujan arvo nolla vai ei. Jos olisi ollut, ei tämä koodi muuta olisi muuta tulostanutkaan.

if-käskyjen yhdisteleminen

Yksi tapa valita useasta vaihtoehdosta on kirjoittaa if-käsky toisen if-käskyn else-osioksi:

val luku = 100luku: Int = 100
if luku < 0 then "negatiivinen" else if luku > 0 then "positiivinen" else "nolla"res63: String = positiivinen
if luku < 0 then
  println("Luku on negatiivinen.")
else if luku > 0 then
  println("Luku on positiivinen.")
else
  println("Luku on nolla.")Luku on positiivinen.

if-käskyt voi muutenkin kirjoittaa sisäkkäin:

if luku > 0 then
  println("On positiivinen.")
  if luku > 1000 then
    println("On yli tuhat.")
  else
    println("On positiivinen muttei yli tuhat.")
 On positiivinen.
On positiivinen muttei yli tuhat.

Tässä else-sana on sisennetty samalle tasolle kuin sisempi if-käsky ja kytkeytyy näin siihen. Tuo else-osa suoritettiin siksi, että ulompi ehto toteutui mutta sisempi ei.

Ulommassa käskyssä ei tässä ole else-osiota ollenkaan. Jos luku ei olisi ollut positiivinen, ei olisi tulostunut mitään.

Seuraavassa esimerkissä, joka on sisennetty toisin, sisemmällä if-käskyllä ei ole else-osiota, mutta ulommalla on:

if luku > 0 then
  println("On positiivinen.")
  if luku > 1000 then
    println("On yli tuhat.")
else
  println("On nolla tai negatiivinen.")On positiivinen.

Näitäkin esimerkkejä on selostettu tarkemmin luvussa 3.4. Luvun 3.5 lopussa taas on esimerkkejä virhetilanteista, joita voi syntyä, kun käyttää if-käskyä funktion paluuarvon määrittämiseen.

Loppumerkinnät valintakäskyissä

Halutessasi voit kirjoittaa loppumerkin end if päättääksesi valintakäskyn. Joissain mutkikkaissa tilanteissa tämä saattaa selkiyttää koodia, mutta hyvin kirjoitetussa koodissa nämä loppumerkit ovat harvoin tarpeen (ks. tyyliopas).

Esimerkki-if loppumerkeillä:

if luku > 0 then
  println("On positiivinen.")
  if luku > 1000 then
    println("On yli tuhat.")
  end if
else
  println("On nolla tai negatiivinen.")
end if

match-valintakäsky

match-käsky (luvut 4.3 ja 4.4) määrittää lausekkeen arvon ja valitsee luetelluista vaihtoehdoista ensimmäisen sellaisen, joka vastaa saatua arvoa. Käskyn yleinen muoto on:

  lauseke L match
    case hahmo A => koodia, joka suoritetaan, jos L:n arvo sopii hahmoon A
    case hahmo B => koodia, joka suoritetaan, jos L:n arvo sopii hahmoon B (muttei A:han)
    case hahmo C => koodia, joka suoritetaan, jos L:n arvo sopii hahmoon C (muttei A:han tai B:hen)
    Ja niin edelleen. (Tyypillisesti katetaan kaikki mahdolliset tapaukset.)
  end match

match-sanaa edeltävän lausekkeen arvoa verrataan...

... ns. hahmoihin, joilla kuvataan erilaisia tapauksia.

Loppuun saa kirjoittaa loppumerkin, jos kokee sen selkiyttävän koodia.

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

Tutkitaan kertolaskulausekkeen arvoa suoritettavan koodin valitsemikseksi.

Lausekkeen arvoa yritetään sovittaa järjestyksessä hahmoihin, joita on tässä kolme, kunnes sopiva löytyy.

Hahmona voi käyttää literaalia; tässä on käytetty Int-literaaleja. Näistä tapauksista ensimmäinen valitaan, jos luvun kuutio oli nolla, toinen jos se oli tuhat.

Hahmoksi voi myös kirjoittaa uuden muuttujanimen; tässä on valittu nimi muuKuutio. Tällainen tapaus sopii yhteen minkä tahansa arvon kanssa ja tulee siis tässä valituksi mikäli kuutio ei ollut nolla eikä tuhat.

Kun tällainen tapaus kohdataan, syntyy uusi paikallinen muuttuja, jonka arvoksi tapaukseen "osunut" arvo tallentuu. Muuttujan nimeä voi käyttää tapauksen koodissa.

Eräs käyttö match-käskylle on arvon poimiminen Option-kääreestä:

// Tätä käytetään alla match-esimerkissä.
def osamaara(jaettava: Int, jakaja: Int) =
  if jakaja == 0 then None else Some(jaettava / jakaja)
osamaara(ekaLuku, tokaLuku) match
  case Some(tulos) => "Tulos on: " + tulos
  case None        => "Tulosta ei ole."

Hahmossa määritellään rakenne: jos kyseessä on Some, niin sen sisällä on jokin arvo. Tuo arvo "puretaan esiin" ja poimitaan muuttujaan tulos.

(Tosin Option-luokan yhteydessä korkeamman asteen metodit ovat usein vähintään yhtä hyvä vaihtoehto kuin match; ks. luku 8.3 ja Option kokoelmatyyppinä alempana.)

Alla on vielä yksi esimerkki, joka esittelee eräitä match-käskyn ominaisuuksia. Esimerkki on luvusta 4.4, josta löytyy enemmänkin vapaaehtoista materiaalia tästä monipuolisesta käskystä.

def kokeilu(jonkinlainenArvo: Matchable) =
  jonkinlainenArvo match
    case jono: String          => "kyseessä on merkkijono " + jono
    case luku: 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"

Esimerkkifunktiomme parametrityyppi on Matchable, mikä tarkoittaa että sille voi antaa minkä tahansa "match-kelpoisen" arvon parametriksi.

Hahmoihin on kirjattu tietotyyppi. Kukin näistä hahmoista tärppää vain, jos tutkittava arvo on kyseistä tyyppiä.

Lisäehto rajaa tapausta: kyseinen tapaus valitaan vain, jos kyseessä on nollaa suurempi kokonaisluku. (match-käskyn osana käytetään samaa if-sanaa kuin erillisessä if-valintakäskyssäkin.)

Alaviivahahmo sopii mihin tahansa arvoon ja tulee valituksi, jos mikään edellisistä ei tullut. Tähän olisi voinut myös kirjoittaa uuden muuttujan nimen (kuten ylempänä tehtiinkin), mutta jos muuttujalle ei ole käyttöä, pelkkä alaviiva kelpaa.

Käyttöalue ja näkyvyysmääreet

Ohjelman osien — muuttujien, funktioiden, luokkien tai yksittäisolioiden — sallittu käyttöalue eli skooppi määräytyy sen mukaan, missä tuo osa on määritelty (luku 5.6). Lisäksi käyttöaluetta voi säädellä näkyvyysmääreillä kuten private (luku 3.2).

Luokan ja sen osien käyttöalue

class Esimerkki(luontiparametri: Int):

  val julkinenIlmentymamuuttuja = luontiparametri * 2
  private val yksityinenIlmentymamuuttuja = luontiparametri * 3

  def julkinenMetodi(parametri: Int) = parametri * this.yksityinenMetodi(parametri)

  private def yksityinenMetodi(parametri: Int) = parametri + 1 + this.yksityinenIlmentymamuuttuja

end Esimerkki

Julkisen ilmentymämuuttujan käyttöalueeseen sisältyy koko luokka. Lisäksi siihen voi viitata luokan ulkopuolelta: olio.julkinenIlmentymamuuttuja. Samoin julkista metodia voi käyttää mistä tahansa päin ohjelmaa. Ilmentymämuuttuja tai metodi on julkinen ellei toisin määritellä.

Yksityisen ilmentymämuuttujan ja yksityisen metodin käyttöalue on koko kyseinen luokka.

Tämä luokka itse on julkinen, joten sitä voi käyttää muualta ohjelmasta vapaasti.

Funktioiden rungot ovat aina yksityisiä. Niiden sisässä oleviin määrittelyihin ei pääse käsiksi mistään kyseisen funktion ulkopuolelta.

Paikallisten muuttujien käyttöalue

Kun siirrät hiiren kursorin laatikoiden päälle, korostuvat mainittujen muuttujien käyttöalueet.

def funktio(parametri: Int) =
  var paikallinen = parametri + 1
  var toinenPaikallinen = paikallinen * 2
  if paikallinen > toinenPaikallinen then
    val vainIffissa = toinenPaikallinen
    toinenPaikallinen = paikallinen
    paikallinen = vainIffissa
  end if
  toinenPaikallinen - paikallinen
end funktio

Parametrimuuttuja kuten parametri on määritelty koko kyseisen funktion ohjelmakoodissa. Sitä voi käyttää sieltä mistä vain.

Funktion koodissa uloimmalla tasolla määritelty muuttuja, kuten paikallinen, on käytettävissä määrittelykohdasta alkaen metodin koodin loppuun.

Samoin toinenPaikallinen.

Kun ulompi käsky sisältää muuttujamäärittelyn, niin määritelty muuttuja on käytettävissä vain kyseisen ulomman käskyn sisällä. Esimerkiksi tässä muuttuja vainIffissa on määritelty vain if-käskyn sisällä.

Mutkikkaampia esimerkkejä löytyy luvusta 5.6.

Paikalliset funktiot

Vastaavasti myös funktioita voi määritellä toisten funktioiden sisään paikallisiksi, mistä kerrotaan luvussa 7.1. 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.

Ulompi funktio kutsuu apufunktiotaan (tässä esimerkissä kahdesti).

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
end Asiakas

class Asiakas(val nimi: String):
  Asiakas.montakoLuotu += 1
  val numero = Asiakas.montakoLuotu

  override def toString = "#" + this.numero + " " + this.nimi
end Asiakas

Kumppaniolio on yksittäisolio, jolle annetaan prikulleen sama nimi kuin luokalle itselleen ja jonka määrittely kirjoitetaan samaan tiedostoon.

Kumppaniolioon voi kirjata luokkaan yleisellä tasolla liittyviä tietoja (kuten tämä ilmentymälaskuri) tai metodeita. Muuttujasta montakoLuotu on muistissa vain yksi kopio, koska kumppanioliotakin on vain yksi. Vrt. asiakasolioiden nimet ja numerot, joita on yksi per asiakasolio.

Asiakas-luokka ja sen kumppaniolio ovat "kavereita", joilla ei ole salaisuuksia. Ne pääsevät poikkeuksellisesti käsiksi myös toistensa yksityisiin tietoihin.

Parit ja muut monikot

Monikko on tilaltaan muuttumaton rakenne, joka muodostuu kahdesta tai useammasta keskenään mahdollisesti eri tyyppisestä arvosta (luku 9.2). Monikon voi määritellä käyttäen sulkeita ja pilkkuja:

val nelikko = ("Tässä monikossa on neljä erilaista jäsentä.", 100, 3.14159, false)nelikko: (String, Int, Double, Boolean) = (Tässä monikossa on neljä erilaista jäsentä.,100,3.14159,false)
nelikko(0)res64: String = Tässä monikossa on neljä erilaista jäsentä.
nelikko(2)res65: Double = 3.14159

Tämän nelikon jäsenet ovat keskenään erityyppisiä.

Pari on yleinen erikoistapaus monikosta. Tässä parissa molemmat jäsenet ovat merkkijonoja:

val pari = ("laama", "llama")pari: (String, String) = (laama,llama)

Monikon osat voi sijoittaa useaan muuttujaan kerralla:

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).

Toinen indeksointitapa

On vaihtoehtoinenkin merkintä, jolla monikosta voi — ja vanhoissa Scala-versioissa oli tarpeenkin — poimia jäseniä. Huomaa alaviivat ja ykkösestä alkava indeksointi.

val nelikko = ("Tässä monikossa on neljä erilaista jäsentä.", 100, 3.14159, false)nelikko: (String, Int, Double, Boolean) = (Tässä monikossa on neljä erilaista jäsentä.,100,3.14159,false)
nelikko._1res66: String = Tässä monikossa on neljä erilaista jäsentä.
nelikko._3res67: Double = 3.14159

Monikoiden erikoispiirre on, että Scala osaa muodostaa niitä automaattisesti, jos parametriksi välitetään "irrallisia" arvoja mutta kaivataan monikkoa (luku 9.2):

def absDiff(pairOfNumbers: (Int, Int)) =
  (pairOfNumbers(0) - pairOfNumbers(1)).absdef absDiff(pairOfNumbers: (Int, Int)): Int
absDiff((-300, 100))res68: Int = 400
absDiff(-300, 100)res69: Int = 400

Funktio vastaanottaa parin.

Sitä kutsuessa voi antaa joko parin tai kaksi irrallista arvoa, joista Scala automaattisesti muodostaa parin (ns. auto-tupling).

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.lengthres70: Int = 26
jono.sizeres71: Int = 26

Kirjainkokojen muokkausta:

val viesti = "five hours of Coding can save 15 minutes of Planning"viesti: String = five hours of Coding can save 15 minutes of Planning
viesti.toUpperCaseres72: String = FIVE HOURS OF CODING CAN SAVE 15 MINUTES OF PLANNING
viesti.toLowerCaseres73: String = five hours of coding can save 15 minutes of planning
viesti.capitalizeres74: String = Five hours of Coding can save 15 minutes of Planning

Osamerkkijono:

"Olavi Eerikinpoika Stålarm".substring(6, 11)res75: String = Eerik
"Olavi Eerikinpoika Stålarm".substring(3)res76: String = vi Eerikinpoika Stålarm

Merkkijonon jakaminen osiin:

"Olavi Eerikinpoika Stålarm".split(" ")res77: Array[String] = Array(Olavi, Eerikinpoika, Stålarm)
"Olavi Eerikinpoika Stålarm".split("la")res78: 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.trimres79: String = tyhjät merkit poistuvat    ympäriltä mutteivät keskeltä

Merkkijonon sisältämien numeromerkkien tulkitseminen luvuksi:

"100".toIntres80: Int = 100
"100".toDoubleres81: Double = 100.0
"100.99".toDoubleres82: Double = 100.99
"sata".toIntjava.lang.NumberFormatException: For input string: "sata"
...
" 100".toIntjava.lang.NumberFormatException: For input string: " 100"
...
" 100".trim.toIntres83: Int = 100

Äskeiset toimet voi tehdä turvallisemmin Option-päätteisillä metodeilla:

"100".toIntOptionres84: Option[Int] = Some(100)
"sata".toIntOptionres85: Option[Int] = None
"100.99".toDoubleOptionres86: Option[Double] = Some(100.99)

Vertailua Unicode-aakkoston mukaan:

"abc" < "bcd"res87: Boolean = true
"abc" >= "bcd"res88: Boolean = false
"abc".compare("bcd")res89: Int = -1
"bcd".compare("abc")res90: Int = 1
"abc".compare("abc")res91: Int = 0
"abc".compare("ABC")res92: Int = 32
"abc".compareToIgnoreCase("ABC")res93: Int = 0

Paluuarvon etumerkki kertoo vertailun tuloksen.

Arvojen yhdistäminen osaksi merkkijonoa

Lausekkeiden arvoja voi upottaa merkkijonoon (luku 1.4):

val luku = 10luku: Int = 10
val upotuksin = s"Muuttujassa on $luku, ja sitä yhtä suurempi luku on ${luku + 1}."upotuksin: String = Muuttujassa on 10, ja sitä yhtä suurempi luku on 11.

Alkuun s-kirjain.

Dollarimerkin perään voi kirjoittaa muuttujan nimen. Muuttujan arvo upotetaan merkkijonoon.

Lauseke rajataan tarvittaessa aaltosulkeilla.

Plus-operaattorillakin voi yhdistää merkkijonon perään erilaisia arvoja, kuten tässä kokonaislukuja:

val samaPlussalla = "Muuttujassa on " + luku + ", ja sitä yhtä suurempi luku on " + (luku + 1) + "."samaPlussalla: String = Muuttujassa on 10, ja sitä yhtä suurempi luku on 11.
"luku on " + lukures94: String = luku on 10
"kuor" + 100res95: String = kuor100

Tuossa yhdistettiin lukuja nimenomaan merkkijonojen perään. Samaa ei kuitenkaan tule tehdä toisin päin — eli muuntyyppinen arvo ennen plussaa:

luku + " on talletettu muuttujaan"luku + " on talletettu muuttujaan"
     ^
warning: method + in class Double is deprecated (since 2.13.0):
Adding a number and a String is deprecated. Use the string interpolation `s"$num$str"`

Erikoismerkit merkkijonoissa

Erikoismerkkejä voi kirjoittaa merkkijonoon kenoviivan avulla (luku 5.2):

val rivinvaihto = "\n"rivinvaihto: String =
"
"
println("eka rivi\ntoka rivi")eka rivi
toka rivi
val sarkainEliTabulaattori = "eka\ttoka\tkolmas"sarkainEliTabulaattori: String = eka   toka    kolmas
"tässä lainausmerkki \" ja toinenkin \""res96: String = tässä lainausmerkki " ja toinenkin "
"tässä kenoviiva \\ ja toinenkin \\"res97: String = tässä kenoviiva \ ja toinenkin \

Merkkijonoliteraaliin, joka on rajattu kummastakin päästä kolmella lainausmerkillä yhden sijaan, voi kirjoittaa erikoismerkkejä sellaisenaan:

"""Tässä merkkijonossa on lainausmerkki " ja
kenoviiva \ kahdella eri rivillä."""res98: String =
Tässä merkkijonossa on lainausmerkki " ja
kenoviiva \ kahdella eri rivillä.

toString-metodi

Kaikilla Scala-olioilla on toString-niminen parametriton metodi, joka palauttaa merkkijonokuvauksen oliosta:

100.toStringres99: String = 100
false.toStringres100: 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 = Kokeilu(10)kokeiluolio: Kokeilu = Kokeilu@56181
kokeiluolio.toStringres101: String = Kokeilu@56181
kokeiluoliores102: Kokeilu = Kokeilu@56181

Oletusarvoinen toString-metodi tuottaa tämän näköisen merkkijonon (luku 2.5).

REPL käyttää juuri toString-metodia kuvatakseen olioita. Yllä siis kutsuttiin toString-metodia yhteensä kolmeen kertaan.

Oletusarvoisen toString-metodin voi korvata (luku 2.5 ja ks. Periytyminen):

class Testi(val muuttuja: Int):
  override def toString = "OLIOLLA ON ARVO " + this.muuttuja// defined class Testi
val testiolio = Testi(11)testiolio: Testi = OLIOLLA ON ARVO 11

toString-metodi tulee kutsutuksi ilman erillistä käskyä, kun olio määrätään tulostettavaksi tai yhdistetään merkkijonoon:

println(testiolio)OLIOLLA ON ARVO 11
testiolio + "!!!"res103: String = OLIOLLA ON ARVO 11!!!
s"Testiolion toString-paluuarvo upotetaan tähän väliin $testiolio ja täältä jatkuu."res104: String = Testiolion toString-paluuarvo upotetaan tähän väliin OLIOLLA ON ARVO 11 ja täältä jatkuu.

Kokoelmien alkeita

Puskurien peruskäyttöä

Puskurit ovat eräänlaisia alkiokokoelmia (luvut 1.5 ja 4.2). Puskureita kuvaava tyyppi Buffer löytyy pakkauksesta scala.collection.mutable:

import scala.collection.mutable.Buffer

Puskurien luominen:

Buffer("eka", "toka", "kolmas", "vielä neljäskin")res105: 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)

Puskuri voi olla tyhjä:

val tanneVoiLisataLukuja = Buffer[Double]()tanneVoiLisataLukuja: Buffer[Double] = ArrayBuffer()

Tyyppiparametrilla (luku 1.5) voi kirjata, millaisia alkioita puskuriin varastoidaan. Tämä on erityisen tarpeellista silloin, kun haluttua alkioiden tyyppiä ei voi päätellä, kuten tässä tyhjää puskuria luodessa.

Puskurissa on nolla tai useampia alkioita järjestyksessä, kukin omalla indeksillään. Indeksit alkavat nollasta, eivät ykkösestä.

Yksittäisen alkion voi katsoa indeksin perusteella näin:

lukuja(0)res106: Int = 12
lukuja(3)res107: Int = 7

Nämä ovat itse asiassa lyhennysmerkintöjä, jotka vastaavat näitä puskuriolion apply-metodin kutsuja (luku 5.3):

lukuja.apply(0)res108: Int = 12
lukuja.apply(3)res109: Int = 7

Metodi lift on samaa sukua. Se palauttaa tuloksen Option-tyyppisenä eikä kaadu ajonaikaiseen virheeseen indeksin ollessa epäkelpo:

lukuja(10000)java.lang.IndexOutOfBoundsException: 10000
...
lukuja.lift(10000)res110: Option[Int] = None
lukuja.lift(-1)res111: Option[Int] = None
lukuja.lift(3)res112: 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 += 11res113: Buffer[Int] = ArrayBuffer(12, 2, 4, 1, 4, 4, 10, 3, 11)
lukuja += -50res114: Buffer[Int] = ArrayBuffer(12, 2, 4, 1, 4, 4, 10, 3, 11, -50)

Operaattorilla -= voi poistaa yhden alkion:

lukuja -= 4res115: Buffer[Int] = ArrayBuffer(12, 2, 1, 4, 4, 10, 3, 11, -50)
lukuja -= 4res116: 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)lukujares117: Buffer[Int] = ArrayBuffer(1000, 12, 2, 1, 4, 10, 3, 11, -50, 100)
lukuja.insert(5, 50000)lukujares118: Buffer[Int] = ArrayBuffer(1000, 12, 2, 1, 4, 50000, 10, 3, 11, -50, 100)
val poistettuNeljasAlkio = lukuja.remove(3)poistettuNeljasAlkio: Int = 1
lukujares119: 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 käytettiin yllä olevissa esimerkeissä. Kuitenkaan siis vektoreita ei voi muuttaa. 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)res120: Int = 10
vektori.lift(10000)res121: 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 vastaavilla käskyillä kuten vektoreita ja puskureitakin (luku 12.1).

  • Listat (List) ovat kokoelmia, jotka sopivat erityisen hyvin alkioiden käymiseen läpi järjestyksessä. Niitä käsitellään lyhyesti luvussa 10.3.

  • Laiskalistat (LazyList) muistuttavat "tavallisia" listoja ja sopivat alkioiden käsittelyyn järjestyksessä. Niiden erikoisuus on, että laiskalistan alkiot muodostetaan ja varastoidaan muistiin vain tarpeen mukaan. Laiskalistoista kertoo luku 7.2 ja niistä on myös oma kappaleensa jäljempänä tällä sivulla.

  • Joukossa (Set) voi olla vain yksi kappale samanlaista alkiota. Joukon alkioilla ei ole järjestystä samassa mielessä kuin yllä mainituissa kokoelmatyypeissä. Joukkoja käsitellään lyhyesti luvussa 10.1.

  • 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 10.3).

  • IArray-kokoelmat ovat muuttumattomia ja muistuttavat siltä osin vektoreita mutta tehokkuusominaisuuksiltaan taulukoita. Niistä on lyhyt maininta luvussa 12.1.

  • Option on niukka-alkioinen kokoelmatyyppi.

Valintaan kokoelmatyyppien välillä vaikuttavat mm. ohjelmointiparadigma ja laadulliset seikat kuten luettavuus ja tehokkuus.

Kokoelmia voi panna sisäkkäin niin, että ulomman kokoelman alkioina on viittauksia toisiin kokoelmiin. Tätä esittelee mm. luku 6.1.

Merkkijonot kokoelmina

Merkkijono on kokoelma (ks. luvut 5.2 ja 5.6), ja sitä voi käsitellä pitkälti samoin kuin vektoria. Merkkijonon alkioina on Char-arvoja.

val jono = "laama"jono: String = laama
jono(3)res122: Char = m
jono.lift(3)res123: Option[Char] = Some(m)

Tavalliset String-tyyppiset merkkijonot ovat muuttumattomia, ja esimerkiksi niiden yhdisteleminen tuottaa uusia merkkijonoja eikä muokkaa alkuperäisiä. (Muuttuvatilaisestikin merkkijonoja voi kuvata; luku 11.2.)

Lukuvälit: Range

Range-oliot ovat muuttumattomia kokoelmia, jotka kuvaavat lukuja tietyltä väliltä (luvut 5.2 ja 5.6).

val kouluarvosanat = Range(4, 11)kouluarvosanat: Range = Range 4 until 11
kouluarvosanat(0)res124: Int = 4
kouluarvosanat(2)res125: Int = 6

Annettu alkukohta sisältyy väliin mutta loppukohta ei.

Range-olion voi luoda myös käyttämällä Int-olioiden until- tai to-metodia (luku 5.2). Jälkimmäinen laskee mainitun loppukohdankin osaksi väliä. Nämä kaksi tuottavat keskenään samanlaiset seitsemän luvun mittaiset lukuvälit.

val samaKuinEdella = 4 until 11samaKuinEdella: Range = Range 4 until 11
val samaTamakin = 4 to 10samaTamakin: Range = Range 4 to 10

Osan välille sijoittuvista luvuista voi ohittaa:

val jokaToinen = 1 to 10 by 2jokaToinen: Range = Range 1 to 10 by 2
val jokaKolmas = 1 to 10 by 3jokaKolmas: Range = Range 1 to 10 by 3

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).sizeres126: Int = 4
Vector().sizeres127: Int = 0
Vector(10, 100, 100, -20).isEmptyres128: Boolean = false
Vector(10, 100, 100, -20).nonEmptyres129: Boolean = true
Vector().isEmptyres130: Boolean = true
Vector().nonEmptyres131: Boolean = false
"laama".isEmptyres132: Boolean = false
"".isEmptyres133: 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 9.2):

val teksti = "kruuna/klaava"teksti: String = kruuna/klaava
val pariJossaAlkuJaLoppu = teksti.splitAt(6)pariJossaAlkuJaLoppu: (String, String) = (kruuna,/klaava)
val samaMonimutkaisemmin = (teksti.take(6), teksti.drop(6))samaMonimutkaisemmin: (String, String) = (kruuna,/klaava)

Pätkä keskeltä slice-metodilla:

Vector("eka/0", "toka/1", "kolmas/2", "neljäs/3", "viides/4").slice(1, 4)res134: 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 +:)

Luulin olevani tulossa hulluksi, kun sain toistuvia virheilmoituksia. Sitten tajusin käyttäneeni +:-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>)

Monelle kokoelmatyypille (muttei kaikille) on valmis metodi: toVector, toBuffer jne.

Syntyvä kokoelma noudattaa tyyppinsä sääntöjä. Esimerkiksi joukoksi muuttaminen poistaa duplikaattialkiot. Joukko ei myöskään säilytä alkioiden järjestystä.

Yleiskäyttöiselle metodille to voi kertoa parametrilla, millaisen kokoelman haluaa.

newBuilder ja toinen tapa alustaa kokoelma

Joskus on kätevää kerätä alkioita yksi tai muutama kerrallaan ja—kun valmista— lopuksi muodostaa kerätyistä alkioista vakiokokoinen kokoelma, vaikkapa vektori. Keräysvaiheessa avuksi voi olla väliaikainen, muuttuvatilainen apuolio, joka pitää kirjaa käsitellyistä alkioista.

Scala API tarjoaa tuollaisiksi apulaisiksi Builder-oliot. Monelle kokoelmatyypille löytyy valmiina newBuilder-metodi, jolla sopivan ja tehokkaan Builder-apuolion voi luoda. Tässä esimerkki vektorin muodostamisesta:

val vektorinrakentaja = Vector.newBuilder[String]vektorinrakentaja: ReusableBuilder[String,Vector[String]] = VectorBuilder(...)
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".indicesres135: Range = Range 0 until 5
Vector(100, 20, 30).indicesres136: Range = Range 0 until 3

Kahden kokoelman yhdistäminen pareja sisältäväksi kokoelmaksi (luku 9.2):

val lajit = Vector("laama", "alpakka", "vikunja")lajit: Vector[String] = Vector(laama, alpakka, vikunja)
val korkeudet = Vector(120, 90, 80)korkeudet: Vector[Int] = Vector(120, 90, 80)
val korkeudetJaLajit = korkeudet.zip(lajit)korkeudetJaLajit: Vector[(Int, String)] = Vector((120,laama), (90,alpakka), (80,vikunja))
val kolmePariaKoskaKorkeuksiaVainKolme = korkeudet.zip(Vector("laama", "alpakka", "vikunja", "guanako"))kolmePariaKoskaKorkeuksiaVainKolme: Vector[(Int, String)] = Vector((120,laama), (90,alpakka), (80,vikunja))
val parivektoriKokoelmapariksi = korkeudetJaLajit.unzipparivektoriKokoelmapariksi: (Vector[Int], Vector[String]) = (Vector(120, 90, 80), 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".reverseres137: String = amaal
Vector(10, 20, 15).reverseres138: 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))

Ensimmäiseksi parametriksi annetaan jokin sellainen funktio, joka ottaa parametriksi yhden kokonaisluvun ja joka myös palauttaa kokonaisluvun. Viittaus tähän funktioon tallentuu toiminto-muuttujaan.

kahdesti-funktio kutsuu parametriksi saamaansa funktiota ensin kerran ja sitten näin tuotetulle paluuarvolle uudestaan.

Tässä pari tavallista funktiota, jotka sopivat kahdesti-funktion parametriksi:

def seuraava(luku: Int) = luku + 1

def tuplaa(tuplattava: Int) = 2 * tuplattava

Käyttöesimerkkejä:

kahdesti(seuraava, 1000)res139: Int = 1002
kahdesti(tuplaa, 1000)res140: 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)res141: Int = 1002
kahdesti(n => 2 * n, 1000)res142: Int = 4000

Funktioliteraali määrittelee nimettömän funktion, joka palauttaa parametriaan yhtä isomman luvun. kahdesti-metodille välitetään parametriksi viittaus tähän nimettömään funktioon.

Funktioliteraalin merkkinä on oikealle osoittava nuoli. Sen vasemmalla puolella mainitaan parametrit (joita on tässä vain yksi) ja oikealla puolella on funktion runko.

Voidaan kirjoittaa (luku: Int) => luku + 1, mutta tuo pidempi merkintä ei ole tässä tapauksessa tarpeen, koska käyttöyhteydestä on automaattisesti pääteltävissä, että parametrin tyyppi on Int.

Tässä toinen korkeamman asteen funktio (luvuista 6.1 ja 6.2):

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.

Parametrina saatu funktio määrää kriteerin, jolla muita parametriarvoja vertaillaan keskenään.

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

Viimeinen parametriarvo on muodostettu kirjoittamalla funktioliteraali, joka määrää vertailutavan.

Sulkeet ovat pakolliset, kun nimetön funktio ottaa useita parametreja.

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, 7.1, 10.1 ja 10.2), joille annetaan parametriksi funktio, jota sovelletaan kokoelman alkioihin. Alla on esimerkkejä eräistä. Esimerkeissä käytetään merkkijonoja ja vektoreita, mutta samoja metodeita löytyy muiltakin kokoelmilta.

Toistaminen joka alkiolle: foreach

Metodilla foreach voi toistaa saman käskyn kullekin alkiolle (luku 6.3):

Vector(10, 50, 20).foreach(println)10
50
20
"laama".foreach( kirjain => println(kirjain.toUpper + "!") )L!
A!
A!
M!
A!

Parametriksi voi antaa esimerkiksi nimettömän funktion.

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" )res143: Vector[String] = Vector(laamanni, Tyranni, Normanni, ritarit sanoo: nni)
sanoja.map( sana => sana.length )res144: Vector[Int] = Vector(5, 4, 5, 15)

Sama lyhennetyillä funktioliteraaleilla:

sanoja.map( _ + "nni" )res145: Vector[String] = Vector(laamanni, Tyranni, Normanni, ritarit sanoo: nni)
sanoja.map( _.length )res146: Vector[Int] = Vector(5, 4, 5, 15)

Jos mapille välitetty parametrifunktio palauttaa kokoelman, syntyy sisäkkäinen rakenne:

val lukuja = Vector(100, 200, 150)lukuja: Vector[Int] = Vector(100, 200, 150)
lukuja.map( luku => Vector(luku, luku + 1) )res147: Vector[Vector[Int]] = Vector(Vector(100, 101), Vector(200, 201), Vector(150, 151))

Metodi flatMap tekee saman kuin map ja flatten yhdessä ja tuottaa "litteän" lopputuloksen (luku 6.3):

lukuja.flatMap( luku => Vector(luku, luku + 1) )res148: Vector[Int] = Vector(100, 101, 200, 201, 150, 151)

Arvioimista kriteerin perusteella: exists, forall, filter, takeWhile, ym.

exists-metodilla voi selvittää, toteutuuko annettu kriteeri millekään alkiolle (luku 6.3); forall vastaavasti selvittää, toteutuuko annettu kriteeri kaikille alkioille; count laskee, monelleko kriteeri toteutuu:

val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20)
luvut.exists( _ < 0 )res149: Boolean = true
luvut.exists( _ < -100 )res150: Boolean = false
luvut.forall( _ > 0 )res151: Boolean = false
luvut.forall( _ > -100 )res152: Boolean = true
luvut.count( _ > 0 )res153: 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 7.1):

val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20)
luvut.find( _ < 5 )res154: Option[Int] = Some(4)
luvut.find( _ == 100 )res155: Option[Int] = None
luvut.indexWhere( _ < 5 )res156: Int = 2
luvut.indexWhere( _ == 100 )res157: 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 10.1); sortBy muodostaa järjestetyn kokoelman:

import scala.math.absval luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20)
val isoinItseisarvo = luvut.maxBy(abs)isoinItseisarvo: Int = -20
val pieninItseisarvo = luvut.minBy(abs)pieninItseisarvo: Int = 4
val jarjestettyItseisarvonMukaan = luvut.sortBy(abs)jarjestettyItseisarvonMukaan: Vector[Int] = Vector(4, 5, 5, 10, -20)
val sanat = Vector("kaikkein pisin", "lyhin", "keskipitkä", "lyhyehkö")sanat: Vector[String] = Vector(kaikkein pisin, lyhin, keskipitkä, lyhyehkö)
val pisin = sanat.maxBy( _.length )pisin: String = kaikkein pisin
val jarjestettyPituudenMukaan = sanat.sortBy( _.length )jarjestettyPituudenMukaan: Vector[String] = Vector(lyhin, lyhyehkö, keskipitkä, kaikkein pisin)

Suurimman tai pienimmän arvon etsiminen epäonnistuu, jos kokoelma on tyhjä. Tämän erikoistapauksen käsittely käy kätevästi Option-päätteisillä versioilla yllä mainituista metodeista:

sanat.maxByOption( _.length )res158: Option[String] = Some(kaikkein pisin)
sanat.minByOption( _.length )res159: Option[String] = Some(lyhin)
sanat.drop(100).minByOption( _.length )res160: 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 10.1). Tässä esimerkkejä järjestämisestä:

val numerojarjestys = luvut.sortednumerojarjestys: Vector[Int] = Vector(-20, 4, 5, 5, 10)
val unicodenMukainenJarjestys = sanat.sortedunicodenMukainenJarjestys: Vector[String] = Vector(kaikkein pisin, keskipitkä, lyhin, lyhyehkö)
val samaKuinAsken = sanat.sortBy( sana => sana )samaKuinAsken: Vector[String] = Vector(kaikkein pisin, keskipitkä, lyhin, lyhyehkö)
val samaTamakin = sanat.sortBy(identity)samaTamakin: Vector[String] = Vector(kaikkein pisin, keskipitkä, lyhin, lyhyehkö)
val kirjaimetJarjestyksessa = "Let's offroad!".sortedkirjaimetJarjestyksessa: String = " !'Ladeffoorst"

Jos vertailtavina tai järjestettävinä on Double-arvoja, on tarkennettava, mitä vertailutapaa niille käytetään. Tarjolla on kaksi valmista tapaa, TotalOrdering ja IeeeOrdering, joista kumpi tahansa toimii useimpiin tarkoituksiin. (Tarkemmat tiedot API-dokumentaatiossa.)

import scala.Ordering.Double.TotalOrderingVector(1.1, 3.0, 0.0, 2.2).sortedres161: Vector[Double] = Vector(0.0, 1.1, 2.2, 3.0)
Vector(1.1, 3.0, 0.0, 2.2).maxres162: Double = 3.0
Vector(-10.0, 1.5, 9.5).maxBy( _.abs )res163: 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 paluuarvo muodostetaan kokoelman alkioiden perusteella (luku 7.1). Tässä ensin foldLeft:

val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20)
val summa = luvut.foldLeft(0)( (osasumma, seuraava) => osasumma + seuraava )summa: Int = 4
val samaLyhyemmin = luvut.foldLeft(0)( _ + _ )samaLyhyemmin: Int = 4

Kaksi parametriluetteloa: ensimmäiseen kirjoitetaan alkuarvo, joka on samalla lopputulos siinä tapauksessa, ettei kokoelmassa olisi alkioita lainkaan, ja...

... toisessa on funktio, jolla yhdistetään välitulos ja seuraava alkio. Tässä esimerkissä kyseessä on yksinkertainen summafunktio.

reduceLeft on samansuuntainen, mutta se käyttää ensimmäistä alkiota lähtöarvona eikä siis tarvitse parametrikseen kuin yhdistämisfunktion:

import scala.math.minval luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20)
val summa = luvut.reduceLeft( _ + _ )summa: Int = 4
val pienin = luvut.reduceLeft(min)pienin: Int = -20

reduceLeftin paluuarvo on samaa tyyppiä kuin käsiteltävän kokoelman alkiot, kun taas foldLeft voi tuottaa muunkintyyppisen tuloksen:

val onkoIsoaLukua = luvut.foldLeft(false)( (loytyiJo, seuraava) => loytyiJo || seuraava > 10000 )onkoIsoaLukua: Boolean = false

Koska reduceLeft olettaa, että kokoelmassa on ainakin yksi alkio, se tuottaa ajonaikaisen virheen, mikäli näin ei olekaan:

val tyhja = Vector[Int]()tyhja: Vector[Int] = Vector()
val tyhjanSummaFoldilla = tyhja.foldLeft(0)( _ + _ )tyhjanSummaFoldilla: Int = 0
val tyhjanSummaReducella = tyhja.reduceLeft( _ + _ )java.lang.UnsupportedOperationException: empty.reduceLeft
...

reduceLeftOption on kuin reduceLeft, muttei kaadu tyhjän listan tapauksessa, vaan palauttaa Option-tyyppisen tuloksen:

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 puidaan tarkemmin luvussa 8.3. Alla on ainoastaan valikoima esimerkkejä kokoelmien metodeista Option-arvoihin sovellettuina.

Käytetään kokeiluissa seuraavia muuttujia:

val jotain: Option[Int] = Some(100)jotain: Option[Int] = Some(100)
val eiMitaan: Option[Int] = NoneeiMitaan: Option[Int] = None

size:

jotain.sizeres164: Int = 1
eiMitaan.sizeres165: Int = 0

foreach:

jotain.foreach(println)100
eiMitaan.foreach(println) // ei tulosta mitään

contains:

jotain.contains(100)res166: Boolean = true
jotain.contains(50)res167: Boolean = false
eiMitaan.contains(100)res168: Boolean = false

exists:

jotain.exists( _ > 0 )res169: Boolean = true
jotain.exists( _ < 0 )res170: Boolean = false
eiMitaan.exists( _ > 0 )res171: Boolean = false

forall:

jotain.forall( _ > 0 )res172: Boolean = true
jotain.forall( _ < 0 )res173: Boolean = false
eiMitaan.forall( _ > 0 )res174: Boolean = true

filter:

jotain.filter( _ > 0 )res175: Option[Int] = Some(100)
jotain.filter( _ < 0 )res176: Option[Int] = None
eiMitaan.filter( _ > 0 )res177: Option[Int] = None

map:

jotain.map( 2 * scala.math.Pi * _ )res178: Option[Double] = Some(628.3185307179587)
kokeilu2.map( 2 * scala.math.Pi * _ )res179: Option[Double] = None

flatten:

Some(jotain)res180: Some[Option[Int]] = Some(Some(100))
Some(eiMitaan)res181: Some[Option[Int]] = Some(None)
Some(jotain).flattenres182: Option[Int] = Some(100)
Some(eiMitaan).flattenres183: Option[Int] = None

flatMap:

def tuhatPer(luku: Int) = if luku != 0 then Some(1000 / luku) else NonetuhatPer(luku: Int): Option[Int]
jotain.flatMap(tuhatPer)res184: Option[Int] = Some(10)
Some(0).flatMap(tuhatPer)res185: Option[Int] = None
eiMitaan.flatMap(tuhatPer)res186: Option[Int] = None

Alkioiden alustaminen funktiolla: tabulate

Kokoelmatyyppien yhteyteen on määritelty tabulate-funktio, jolla voi luoda kokoelmia kätevästi alkiot tietyn "kaavan" mukaan alustaen (luvut 6.1 ja 6.2).

Tälle metodille annetaan kaksi parametriluetteloa. Ensimmäisessä on haluttu alkioiden määrä eli luotavan kokoelman koko ja toisessa funktio, jota kutsutaan kullekin indeksille kyseisen alkion muodostamiseksi:

Vector.tabulate(10)( indeksi => indeksi * 2 )res187: 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 )res188: 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.2). Se sopii käytäväksi läpi järjestyksessä. Laiskalistan alkiot voi käsitellä yksi kerrallaan varastoimatta niitä kaikkia yhtaikaisesti muistiin.

Monilta osin laiskalista muistuttaa edellä esiteltyjä kokoelmatyyppejä. Sellaisen voi esimerkiksi luoda alkiot luettelemalla tai olemassa olevasta kokoelmasta kopioimalla:

val laiskaaDataa = LazyList(10.2, 32.1, 3.14159)laiskaaDataa: LazyList[Double] = LazyList(<not computed>)
laiskaaDataa.mkString(" ")res189: 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).headres190: 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-funktio tuottaa loputtoman listan:

val lista = LazyList.continually("Oliver")lista: LazyList[String] = LazyList(<not computed>)
lista.take(5).foreach(println)Oliver
Oliver
Oliver
Oliver
Oliver

Laiskalistassa on loputtomasti alkioita, jotka saadaan evaluoimalla lauseke "Oliver" (toistamiseen aina tarvittaessa). Koska kyseessä on literaalilauseke, tämän listan alkiot ovat keskenään identtisiä.

Alkuperäinen laiskalista on ääretön, mutta take palauttaa parametrinsa mittaisen laiskalistan, joka on pätkä alkuperäisestä.

Näin luodussa laiskalistassa voi olla myös keskenään erilaisia alkioita:

LazyList.continually( Random.nextInt(100) ).takeWhile( _ <= 90 ).mkString(",")res191: String = 0,65,83,38,75,33,11,18,75,51,3

Tämä laiskalista muodostuu satunnaisluvuista. Uusia lukuja kuitenkin arvotaan vain sitä mukaa kun tarvitaan.

Muodostetaan loputtomasta lukulistasta katkaistu versio, joka päättyy, kun vastaan arvotuksi riittävän iso luku. Tämäkään ei vielä arvo satunnaislukuja vaan vain määrittää laiskalistan, joka osaa niitä tiettyyn pisteeseen saakka tuottaa.

mkString muodostaa kokoelman perusteella merkkijonon, jossa arvotut luvut on lueteltu. Tämä pakottaa LazyList-olion evaluoimaan alkioita muodostavan arpomiskäskyn toistuvasti.

Laiskalistaa voi käyttää myös vuorovaikutteisen ohjelman toteuttamiseen. Seuraava luvusta 7.2 toistettu ohjelma kysyy käyttäjältä syötteitä kunnes tämä sanoo "please" ja raportoi syötteiden pituudet tähän tapaan:

Enter some text: hello
The input is 5 characters long.
Enter some text: stop
The input is 4 characters long.
Enter some text: please
@main def sayPlease() =
  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)

Laiskalista "tuo" syötteitä ohjelman käsiteltäväksi. Se on päättymätön merkkijonojen lista, jonka kukin alkio saadaan kysymällä sitä käyttäjältä. Tässä kuitenkin vasta määritellään lista, jonka alkiot tuotetaan kutsumalla readLinea aina, kun tarvitaan uusi alkio.

takeWhile palauttaa lopetussanaan "please" rajatun laiskalistan.

map palauttaa raporttien listan, jonka kukin alkio muodostetaan (tarvittaessa) evaluoimalla readLine-käsky ja soveltamalla sen palauttamaan arvoon report-funktiota. Tämäkään käsky ei vielä silti kysy käyttäjältä mitään eikä kutsu report-funktiota.

foreach määrää raporttilistan alkiot tulostettavaksi. Jotta alkion voi käsitellä, se on ensin määritettävä kysymällä syötettä käyttäjältä. Tuloksena syntyy ohjelma, joka toistuvasti kyselee käyttäjältä syötteitä ja raportoi niiden mitat.

Loputtoman lukulistan voi luoda helposti LazyList.from-funktiolla:

val positiiviset = LazyList.from(1)positiiviset: LazyList[Int] = LazyList(<not computed>)
positiiviset.take(3).foreach(println)1
2
3
LazyList.from(0, 10).take(3).foreach(println)0
10
20
val ekaIsoNelio = LazyList.from(0).map( n => n * n ).dropWhile( _ <= 1234567 ).headekaIsoNelio: Int = 1236544

Lisää tapoja luoda LazyList-olio

iterate-metodi luo laiskalistan, jossa seuraava alkio saadaan edellisestä tiettyä funktiota aina uudelleen soveltamalla:

def vaihteleva = LazyList.iterate(1)( x => -2 * x )vaihteleva: LazyList[Int]
vaihteleva.take(4).foreach(println)1
-2
4
-8

Rekursiivisella funktiolla voi määritellä minkälaisen vain laiskalistan. Tämä yksinkertainen rekursioesimerkki tekee saman kuin LazyList.from(1).

def positiiviset(eka: Int): LazyList[Int] = eka #:: positiiviset(eka + 1)positiiviset(eka: Int): LazyList[Int]
positiiviset(1).take(3).foreach(println)1
2
3

Operaattori #:: muodostaa LazyListin yhdistelmänä: alkuun tulee vasemmalla mainittu yksittäinen arvo, perään oikealla mainittu laiskalista.

Määritelmä on rekursiivinen eli itseensä viittaava: positiivisten lukujen sarja muodostetaan laittamalla alkuarvon perään kaikkien sitä suurempien positiivisten lukujen sarja.

Evaluoimattomat eli by name -parametrit

Laiskalistat perustuvat ajatukseen, että metodin parametriksi välitetään evaluoimaton lauseke eikä tuon lausekkeen arvoa. Tällainen evaluoimatonta parametria eli by name -parametria evaluoidaan vasta kun (tai jos) metodin suorituksessa päästään kohtaan, jossa kyseistä parametria käytetään.

By name -parametrin voi määritellä itsekin, mistä on alla pieni esimerkki.

def printtaaJaPalauta(luku: Int) =
  println("Palautan parametrini " + luku)
  lukuprinttaaJaPalauta(luku: Int): Int
def kokeilu(luku: Int, luvunTuottavaLauseke: =>Int) = if luku >= 0 then luvunTuottavaLauseke else -1kokeilu(luku: Int, luvunTuottavaLauseke: => Int): Int

Ensimmäinen funktiomme vain raportoi, milloin sitä on kutsuttu.

Jälkimmäisen funktion toinen parametri on by name -parametri, mikä merkitään nuolella =>. Tämä parametri evaluoidaan vasta kun tai jos sitä käytetään kokeilu-funktiota suorittaessa.

Asia näkyy tulosteesta:

kokeilu(printtaaJaPalauta(10), printtaaJaPalauta(100))Palautan parametrini 10
Palautan parametrini 100
res192: Int = 100
kokeilu(printtaaJaPalauta(-10), printtaaJaPalauta(100))Palautan parametrini -10
res193: Int = -1

Ensimmäinen parametri on ihan tavallinen. Parametrilauseke evaluoidaan joka tapauksessa ennen kuin luku (10 tai -10) välitetään kokeilu-funktiolle.

Kun ensimmäinen parametri on positiivinen, päädytään haaraan, jossa jälkimmäinen parametrilauseke evaluoidaan ja printtaaJaPalauta tulee toisen kerran kutsutuksi.

Kun ensimmäinen parametri on negatiivinen, päädytään haaraan, jossa palautetaan -1. Jälkimmäistä parametria ei tarvita eikä evaluoida lainkaan.

Laiskat muuttujat

Laiska muuttuja on yksittäinen muuttuja, joka toimii kuin laiskalistan alkiot: se saa arvonsa evaluoimalla siihen sijoitetun lausekkeen, kun sen arvoa ensi kerran tarvitaan. Siitä eteenpäin muuttuja säilöö tuon arvon, eikä sijoitettua lauseketta evaluoida uudelleen.

Scalassa tällainen muuttuja määritellään sanoilla 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 then eka * 10 else toka * 10Palautan parametrini 1
res194: Int = 10
if eka > 0 then eka * 10 else toka * 10res195: Int = 10

if-käskyn ehdon evaluoiminen vaatii eka-muuttujalle arvon, joten tämän laiska muuttujan arvo määritetään printtaaJaPalauta-funktiota kutsumalla. Tuloste ilmestyy näkyviin.

Valituksi tulee ensimmäinen haara jossa if-lausekkeen arvoksi saadaan eka * 10. eka-muuttujalle on jo laskettu arvo joten sitä ei lasketa uudestaan (eikä funktiomme tulosta toista riviä, kuten olisi käynyt, jos eka olisi def eikä lazy val.

Käskyn uusiminenkaan ei tuota printtaaJaPalauta-funktion lisätulosteita, koska eka-muuttujalla on jo arvo.

Koska jälkimmäistä haaraa ei valittu, ei toka-muuttujan arvoa tarvittu eikä sen arvoa ole vielä edes määritetty, vaikka tuo muuttuja if-käskyssä esiintyykin.

Toistaminen silmukoilla

fordo-silmukka

fordo-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 do
  println("Nyt käsiteltävä alkio: " + alkio)
  println("Sitä yhtä suurempi: " + (alkio + 1))Nyt käsiteltävä alkio: 100
Sitä yhtä suurempi: 101
Nyt käsiteltävä alkio: 20
Sitä yhtä suurempi: 21
Nyt käsiteltävä alkio: 5
Sitä yhtä suurempi: 6
Nyt käsiteltävä alkio: 50
Sitä yhtä suurempi: 51

Silmukan alussa määritellään, millaisille alkioille silmukan runkoa toistetaan. Huomaa avainsanat for ja do.

Nuolen <- perässä on lauseke, joka kertoo, mistä arvoja noukitaan vuoron perään käsiteltäviksi.

Nuolen vasemmalla puolella on muuttujan nimi, joka ohjelmoijan sopii valita. Tämän niminen muuttuja on käytettävissä silmukan rungossa ja sisältää aina parhaillaan käsiteltävän arvon (tässä: vuorossa olevan alkion puskurista).

Silmukan runko suoritetaan kullekin alkiolle vuoron perään. Huomaa sisennykset.

Silmukan rungossa voi yhdistellä erilaisia käskyjä. Esimerkiksi if-valintakäskyä voi käyttää:

for alkio <- puskuri do
  if alkio > 10 then
    println("Tämä alkio on kymppiä isompi: " + alkio)
  else
    println("Tässä kohdassa on pieni alkio.")
end forTämä alkio on kymppiä isompi: 100
Tämä alkio on kymppiä isompi: 20
Tässä kohdassa on pieni alkio.
Tämä alkio on kymppiä isompi: 50

Loppumerkki on vapaaehtoinen mutta selkiyttää joskus.

Läpikäytävä kokoelma voi olla muukin, vaikkapa Range-tyyppinen lukuväli tai merkkijono (luku 5.6):

for luku <- 10 to 15 do
  println(luku)10
11
12
13
14
15
for indeksi <- puskuri.indices do
  println("Indeksillä " + indeksi + " on luku " + puskuri(indeksi))Indeksillä 0 on luku 100
Indeksillä 1 on luku 20
Indeksillä 2 on luku 5
Indeksillä 3 on luku 50
for merkki <- "testi" do
  println(merkki)t
e
s
t
i

Alla on vielä yksi silmukka, joka käy läpi pareja (ks. Parit ja muut monikot yllä). Nuo parit on muodostettu zipWithIndex-metodilla (ks. Yleisiä kokoelmien metodeita yllä).

for (alkio, indeksi) <- puskuri.zipWithIndex do
  println("Indeksillä " + indeksi + " on luku " + alkio)Indeksillä 0 on luku 100
Indeksillä 1 on luku 20
Indeksillä 2 on luku 5
Indeksillä 3 on luku 50

Mm. luvut 5.5 ja 5.6 sisältävät runsaasti lisäesimerkkejä fordo-silmukoista.

foryield ja monipuolisempia for-silmukoita

Scalan for-silmukalla on puolia, joita ei Ohjelmointi 1 -kurssilla varsinaisesti esitellä tai tarvita. Silmukalla voi esimerkiksi tilan muuttamisen sijaan tuottaa uuden kokoelman. Tällöin käytetään do-sanan sijaan sanaa yield:

val vektori = Vector(100, 0, 20, 5, 0, 50)vektori: Vector[Int] = Vector(100, 0, 20, 5, 0, 50)
for luku <- vektori yield luku + 100res196: Vector[Int] = Vector(200, 100, 120, 105, 100, 150)
for sana <- Vector("laama", "alpakka", "vikunja") yield sana.lengthres197: Vector[Int] = Vector(5, 7, 7)

Samassa yhteydessä voi myös suodattaa arvoja:

for luku <- vektori if luku != 0 yield 100 / lukures198: Vector[Int] = Vector(1, 5, 20, 2)

Rivitys voi selkiyttää. Äskeisen voi rivittää esimerkiksi näin tai toisin:

for
  luku <- vektori
  if luku != 0
yield 100 / lukures199: Vector[Int] = Vector(1, 5, 20, 2)

for-silmukat ovat toisenlainen tapa kirjoittaa foreach-, map-, flatMap- ja filter-kutsuja, joita esiteltiin yllä kohdassa Kokoelmien käsittely korkeamman asteen metodeilla.

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 do
  println("Ulomman kierros alkaa.")
  for merkki <- merkkeja do
    println(s"luku nyt $luku ja merkki nyt $merkki")
  end for
  println("Ulomman kierros päättyy.")
end forUlomman kierros alkaa.
luku nyt 5 ja merkki nyt a
luku nyt 5 ja merkki nyt b
luku nyt 5 ja merkki nyt c
luku nyt 5 ja merkki nyt d
Ulomman kierros päättyy.
Ulomman kierros alkaa.
luku nyt 3 ja merkki nyt a
luku nyt 3 ja merkki nyt b
luku nyt 3 ja merkki nyt c
luku nyt 3 ja merkki nyt d
Ulomman kierros päättyy.

Sisäkkäisyys ja for

Yhteen for-silmukkaan voi yhdistää useita "sisäkkäisiä" läpikäyntejä. Seuraavat kolme koodia tekevät keskenään saman:

for luku <- lukuja do
  for merkki <- merkkeja do
    println(s"$luku, $merkki")
for luku <- lukuja; merkki <- merkkeja do
  println(s"$luku, $merkki")
for
  luku <- lukuja
  merkki <- merkkeja
do
  println(s"$luku, $merkki")

while-silmukka

while-silmukan alkuun kirjoitetaan ehtolauseke, joka määrää, kauanko silmukan runkoa toistetaan. Esimerkki:

var luku = 1luku: Int = 1
while luku < 10 do
  println(luku)
  luku += 4
  println(luku)1
5
5
9
9
13

Esimerkin ensimmäinen käsky alustaa muuttujan, jota jäljempänä käytetään. Tämä alustus ei ole varsinaisesti osa silmukkaa.

Määrittelyn alussa on sanat while ja do ja niiden välissä ehtolauseke.

Perässä on silmukan runko sisennettynä.

Ehtolausekkeen on oltava Boolean-tyyppinen. Se evaluoidaan aina juuri ennen kutakin silmukan rungon suorituskertaa. Jos saadaan false, niin silmukan suoritus päättyy, muuten suoritetaan runko ja palataan jälkeen evaluoimaan tämä sama ehtolauseke.

Tässä esimerkissä silmukan runko toistetaan kolmesti. Ensimmäisen suorituskerran lopussa luku-muuttujan arvo on 5, toisella kerralla 9 ja kolmannella 13. Kun jatkamisehtoa tämän jälkeen tarkastetaan, se ei enää ole voimassa.

On mahdollista, että toistokertoja on nolla: jatkamisehto tarkistetaan ensimmäisen kerran jo ennen kuin runkoa on suoritettu kertaakaan. Edellisessä esimerkissä luku oli aluksi 1 ja jatkamisehto luku < 10 siksi aluksi true. Alla näin ei ole:

var luku = 20luku: Int = 20
while luku < 10 do
  println(luku)
  luku += 4
  println(luku)
end while

Ehto ei ole aluksi voimassa, eikä runkoa suoriteta kertaakaan. Tämä koodi ei tulosta mitään.

Sivuhuomio: Myös while-silmukan voi päättää loppumerkkiin, kuten tässä on esimerkin vuoksi näytetty. Merkintä on vapaaehtoinen kuten loppumerkit Scalassa muutenkin.

Lisäesimerkkejä on luvussa 9.1.

Hakurakenteet (Map)

Hakurakenne on kokoelma, jonka alkioina on avain–arvo-pareja (luku 9.2). Se ei perustu numeerisiin indekseihin vaan arvojen hakemiseen avainten perusteella. Avain–arvo-pareina käytetään tavallisia kaksijäsenisiä monikkoja (ks. Parit ja muut monikot). Hakurakenteessa voi esiintyä sama arvo useasti, mutta avainten on oltava keskenään erilaisia.

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)

Hakurakenteen alkiot ovat aina avain–arvo-pareja.

Hakurakenteella on kaksi tyyppiparametria: avainten tyyppi ja arvojen tyyppi. Tässä esimerkissä sekä avaimet että arvot ovat merkkijonoja.

Arvojen hakeminen: get, contains, apply

contains-metodilla voi tutkia, onko tietty avain käytössä:

suomestaEnglanniksi.contains("tapiiri")res200: Boolean = true
suomestaEnglanniksi.contains("insulintialainen kummitussirkka")res201: Boolean = false

Arvon hakeminen avaimen perusteella onnistuu get-metodia käyttäen. Se palauttaa arvon Option-kääreessä:

suomestaEnglanniksi.get("kissa")res202: Option[String] = Some(cat)
suomestaEnglanniksi.get("insulintialainen kummitussirkka")res203: Option[String] = None

Lyhyemminkin saa haettua, mutta tällöin puuttuva arvo tuottaa ajonaikaisen virheen:

suomestaEnglanniksi("kissa")res204: String = cat
suomestaEnglanniksi("insulintialainen kummitussirkka")java.util.NoSuchElementException: key not found: insulintialainen kummitussirkka
...

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. Tässä kuitenkin kokeilemme muuttuvatilaista hakurakennetta, joka otetaan erikseen käyttöön:

import scala.collection.mutable.Mapval 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 9.2):

suomestaEnglanniksi("hiiri") = "mouse"suomestaEnglanniksi += "sika" -> "pig"res205: 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")res206: Option[String] = Some(tapir)
suomestaEnglanniksi -= "laama"res207: 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 9.2):

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")res208: String = cat
suomestaEnglanniksi.getOrElse("insulintialainen kummitussirkka", "tuntematon hakusana")res209: String = tuntematon hakusana

Metodin paluuarvo on String eikä Option[String] kuten get-metodin tapauksessa.

Jos kyseessä on muuttuvatilainen hakurakenne, voi käyttää myös metodia getOrElseUpdate. Haun epäonnistuessa se lisää hakurakenteeseen jälkimmäisen parametrinsa määräämän arvon, joten haku lopulta onnistuu aina:

import scala.collection.mutable.Mapval 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")res210: String = bug
suomestaEnglanniksires211: 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 9.2):

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")res212: String = cat
englanniksi("insulintialainen kummitussirkka")res213: String = ähäkutti

withDefaultValue-metodille ilmoitetaan, mitä halutaan käyttää "vara-arvona" silloin, kun haku on huti.

Nyt kun hakurakenteesta haetaan olematonta avainta, ei synny virhettä vaan saadaan tämä vara-arvo.

withDefault

Äskeisessä esimerkissä vara-arvo oli aina sama. Metodilla withDefault voit asettaa hakurakenteelle "varafunktion", joka määrittää paluuarvoja hutihaun tuottaneen avaimen perusteella:

def raportti(haettu: String) = "hait sanaa " + haettu + " muttei löytynyt"raportti(haettu: String): String
val englanniksi = Map("kissa" -> "cat", "tapiiri" -> "tapir", "koira" -> "dog").withDefault(raportti)englanniksi: Map[String,String] = Map(koira -> dog, tapiiri -> tapir, kissa -> cat)
englanniksi("kissa")res214: String = cat
englanniksi("insulintialainen kummitussirkka")res215: String = hait sanaa insulintialainen kummitussirkka muttei löytynyt

Hakurakenteen muodostaminen kokoelmasta: toMap, groupBy

Kutsumalla toMap-metodia voi hakurakenteen luoda minkä tahansa sellaisen kokoelman perusteella, jonka alkioina on pareja (luku 10.1):

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")res216: Int = 7

Esimerkissä ensin luodaan pari erillistä kokoelmaa ja yhdistetään ne zip-metodilla. Syntyy pareja sisältävä vektori.

Tällaisen vektorin perusteella toMap voi luoda hakurakenteen.

Metodilla groupBy muodostetaan hakurakenne, johon alkuperäisen kokoelman alkiot on ryhmitelty sen mukaan, mitä parametriksi annettu funktio alkion kohdalla palauttaa:

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 10.1.

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.isEmptyres217: Boolean = false
englanniksi.sizeres218: Int = 3
englanniksi.foreach(println)(koira,dog)
(tapiiri,tapir)
(kissa,cat)

Nimenomaan hakurakenteille ominaisia ovat metodit keys ja values (luku 9.2), 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 9.2) käsittelee avain–arvo-pareja.

englanniksi.map( finEngPari => finEngPari(0) -> finEngPari(1).length )res219: Map[String,Int] = Map(kissa -> 3, tapiiri -> 5, koira -> 3)

Metodi tuottaa uuden muuttumattoman hakurakenteen, jossa alkuperäisten parien tilalla on parametrifunktion tuottamat parit.

map toimii myös kaksiparametrisilla funktioilla, kuten toimivat muutkin metodit, jotka vastaavasti ottavat parin parametriksi (luku 9.2):

englanniksi.map( (fin, eng) => fin -> eng.length )res220: Map[String,Int] = Map(kissa -> 3, tapiiri -> 5, koira -> 3)
englanniksi.map( (fin, eng) => fin.toUpperCase -> eng.length )res221: Map[String,Int] = Map(KISSA -> 3, TAPIIRI -> 5, KOIRA -> 3)
englanniksi.filter( (fin, eng) => fin.length == 5 && eng.length == 3 )res222: Map[String, String] = Map(kissa -> cat, koira -> dog)
englanniksi.filter( _.length == 5 && _.length == 3 )res223: Map[String, String] = Map(kissa -> cat, koira -> dog)

Lisää hakurakenteidenkin metodeista virallisessa dokumentaatiossa.

Periytyminen

Yläkäsitettä ja sen alakäsitteitä voi kuvata määrittelemällä piirreluokan (trait; luku 7.3) tai yliluokan (luku 7.5), josta alakäsitteet periytyvät.

Myös yksittäisoliot voivat periä luokkia ja piirteitä.

Piirreluokat

Piirreluokka määritellään samaan tapaan kuin luokka mutta sanaa trait käyttäen (luku 7.3). Tämä piirreluokka kuvaa abstraktia kuvion käsitettä:

trait Shape:

  def isBiggerThan(another: Shape) = this.area > another.area

  def area: Double    

end Shape

Kaikilla kuvioilla on isBiggerThan-metodi, jolla voi verrata kuvioiden pinta-aloja keskenään.

Kaikilla kuvioilla on myös area-metodi pinta-alan laskemiseen. Tämä metodi on abstrakti: sillä ei ole runkoa eikä sitä voi sellaisenaan kutsua. Pinta-alan laskentatapa määritellään erikseen alakäsitteille eli niille luokille, jotka perivät Shape-piirteen (ks. alta).

Vertailumetodille voi antaa parametriksi viittauksen mihin tahansa Shape-tyyppiseen olioon. Kaikilla tällaisilla olioilla on jonkinlainen area-metodi, joten voimme kutsu tuota metodia vertailumetodin parametrille.

Alakäsite piirreluokalle

Seuraavat kaksi luokkaa perivät Shape-piirteen (luku 7.3). Ne edustavat kuviokäsitteen alakäsitteitä:

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

Periytyminen merkitään extends-sanalla. Tästä seuraa, että kaikki Circle-tyyppiset oliot ovat paitsi ympyröitä myös kuvioita. Niillä on mm. piirreluokassa Shape määritelty isBiggerThan-metodi.

Luokissa voidaan tarjota toteutukset piirreluokan abstrakteille metodeille. Esimerkiksi tässä määritellään, että ympyrä on sellainen kuvio, jonka pinta-ala lasketaan pii * r2, ja suorakaide on sellainen kuvio, jonka pinta-ala lasketaan sivujen kertolaskulla.

Luokka voi periytyä useasta piirreluokasta:

class X extends A, B, C, D, Etc

Piirreluokka voi periytyä toisesta (tai useammastakin):

trait FilledShape extends Shape

Staattiset ja dynaamiset tyypit

Luvussa 7.3 erotetaan toisistaan staattinen ja dynaaminen tyyppi:

var kuvio: Shape = Circle(1)kuvio: o1.shapes.Shape = o1.shapes.Circle@1a1a02e
kuvio = Rectangle(10, 5)kuvio: o1.shapes.Shape = o1.shapes.Rectangle@7b519d

Muuttujan kuvio staattinen tyyppi on Shape. Sillä voi viitata mihin tahansa Shape-tyyppiseen, esimerkiksi ympyrään tai suorakaiteeseen. Staattinen tyyppi käy ilmi pelkästä ohjelmakoodista.

Muuttujaan kuvio on tässä esimerkissä ensin sijoitettu arvo, jonka dynaaminen tyyppi on Circle. Se korvataan arvolla, jonka dynaaminen tyyppi on Rectangle. Dynaamisen tyypin on oltava yhteensopiva muuttujan staattisen tyypin kanssa.

Kaikille Scala-olioille yhteisen isInstanceOf-metodin avulla voi tutkia arvon dynaamista tyyppiä. Tässä todetaan, että kuvio-muuttujassa on parhaillaan viittaus olioon, joka on sekä Rectangle että Shape-tyyppinen:

kuvio.isInstanceOf[Rectangle]res224: Boolean = true
kuvio.isInstanceOf[Shape]res225: Boolean = true

Yllä kuviomuuttujan tyyppi oli erikseen määritelty Shapeksi. Tässä ei, minkä vuoksi sijoitus epäonnistuu:

var kokeilu = Circle(1)kokeilu: o1.shapes.Circle = o1.shapes.Circle@1c4207e
kokeilu = Rectangle(10, 5)-- Error:
  |kokeilu = Rectangle(10, 5)
  |          ^^^^^^^^^^^^^^^^
  |          Found:    o1.shapes.Rectangle
  |          Required: o1.shapes.Circle

Muuttujan staattiseksi tyypiksi tulee alkuarvon perusteella päätellyksi Circle, jolloin siihen voi sijoittaa vain Circle-tyyppisiä arvoja eikä muita kuvioita.

Staattinen tyyppi rajoittaa arvojen käyttöä (luku 7.3):

var testi: Shape = Circle(10)testi: o1.shapes.Shape = o1.shapes.Circle@9c8b50
testi.radius-- Error:
  |testi.radius
  |^^^^^^^^^^^^
  |value radius is not a member of o1.shapes.Shape

Lausekkeen test staattinen tyyppi on Shape. Mielivaltaiselle Shape-oliolle ei ole määritelty radius-muuttujaa, vaikka ympyröille onkin.

match-käskyllä voi valita dynaamisen tyypin mukaan:

testi match
  case ympyra: Circle =>
    println("Se on ympyrä, ja sen säde on " + ympyra.radius)
  case _ =>
    println("Se ei ole ympyrä.")Se on ympyrä, ja sen säde on 10.0

Luontiparametrit piirreluokilla

Piirreluokalla voi olla luontiparametreja. Esimerkiksi tässä määritellään, että PersonAtAalto-olioilla on luontiparametrina nimi ja toimenkuva:

trait PersonAtAalto(val name: String, val occupation: String)

Kun tavallinen luokka tai yksittäisolio perii tämän piirreluokan, välitetään piirreluokalle parametrit. Tässä esimerkkejä:

object President extends PersonAtAalto("Ilkka", "preside over the university")

class Employee(name: String, job: String) extends PersonAtAalto(name, job)

class LocalStudent(name: String, val id: String, val admissionYear: Int)
      extends PersonAtAalto(name, "study for a degree")

class ExchangeStudent(name: String, val aaltoID: String, val homeUniversity: String, val homeID: String)
      extends PersonAtAalto(name, "study temporarily")

Yksittäisolio välittää piirreluokalle luontiparametrit. Tässä President-olio välittää kaksi merkkijonoa piirreluokan muuttujien name ja occupation arvoiksi.

Kun luokka perii piirteen, välitetään piirreluokalle parametreja vastaavasti.

Usein (muttei aina) kyseessä ovat luokan omien luontiparametrien arvot, jotka välitetään "ylöspäin" piirreluokalle.

Tässä ovat siis kyseessä tavalliset luokat/yksittäisoliot, jotka perivät piirreluokan. (Vrt. jatkoesimerkki alla.)

Sanotaan, että haluamme lisäksi piirreluokan Student edustamaan erilaisia opiskelijoita yleisesti — niin paikallisia kuin vaihto-opiskelijoitakin. Tämä versio ei toimi:

trait Student(name: String, val id: String) extends PersonAtAalto(name, "study for a degree")

Tässä yritetään välittää piirreluokan määrittelystä luontiparametreja yläkäsitteelle PersonAtAalto. Yritys tuottaa käännösaikaisen virheilmoituksen: piirreluokasta ei voi näin välittää parametria "ylöspäin".

Sen sijaan seuraava toimii:

trait Student(val id: String) extends PersonAtAalto 

PersonAtAalto ei saa tästä luontiparametreja. Toisaalta ei tarvitsekaan; ne voi kirjata toisaalle. Tässä vain todetaan, että opiskelijat ovat aaltolaisia, joilla on (aaltolaisten muiden ominaisuuksien lisäksi) opiskelijanumero.

class LocalStudent(name: String, id: String, val admissionYear: Int)
      extends PersonAtAalto(name, "study for a degree"), Student(id)

class ExchangeStudent(name: String, aaltoID: String, val homeUniversity: String, val homeID: String)
      extends PersonAtAalto(name, "study temporarily"), Student(aaltoID)

Välitämme parametrit sekä PersonAtAalto- että Student-piirreluokalle näistä tavallisista luokista.

Metodin korvaaminen: override

Alakäsitteessä voi korvata yläkäsitteelle määritellyn metodin käyttämällä override-sanaa (luvut 2.4 ja 7.3). Eräs yleinen korvattava on toString-metodi. Tässä toisenlainen esimerkki:

trait Ylakasite:
  def eka() =
    println("Yläkäsitteen eka")
  def toka() =
    println("Yläkäsitteen toka")
end Ylakasite
class Alakasite extends Ylakasite:
  override def eka() =
    println("Alakäsitteen eka")
  override def toka() =
    println("Alakäsitteen toka")
    super.toka()
end Alakasite
val kokeilu = Alakasite()kokeilu: Alakasite = Alakasite@1bd9da5
kokeilu.eka()Alakäsitteen eka
kokeilu.toka()Alakäsitteen toka
Yläkäsitteen toka

Tässä on korvattu molemmat yläkäsitteen metodit.

Alakasite-tyyppisen olion eka-metodi toimii yläkäsitteen toteutuksesta riippumattomasti. Korvaava toteutus ratkaisee.

Osana alakäsitteen toka-metoditoteutusta kutsutaan yläkäsitteen versiota metodista, joten...

... Alakasite-tyyppisen olion metodi tuottaa ensin alakäsitteelle määritellyn tulosteen ja tekee sitten sen, mitä korvattu Ylakasite-tyypin metodikin tekee.

super-sanaa voi käyttää yläkäsitteen määrittelyyn viittaamiseen muutenkin kuin korvatussa metodissa, mutta tuo on suhteellisen yleinen käyttötapaus.

Periytyminen yliluokasta

Luokka voi periytyä paitsi piirreluokasta myös toisesta "tavallisesta" luokasta. Tässä käytetään luvun 7.5 tapaan Rectangle-luokkaa, jonka perii luokka Square:

open class Rectangle(val sideLength: Double, val anotherSideLength: Double) extends Shape:
  def area = this.sideLength * this.anotherSideLength
class Square(size: Double) extends Rectangle(size, size)

open-sana kertoo luokan olevan avoin. Avoimesta luokasta voi periyttää muita luokkia vapaasti. Ilman tätä määrettä luokka olisi suljettu.

Käytetään extends-sanaa, jonka perään kirjoitetaan perityn luokan nimi. Aliluokka Square periytyy nyt yliluokasta Rectangle, ja Square-oliot ovat nyt myös Rectangle-tyyppisiä (ja Shape-tyyppisiä, koska Rectangle-luokka perii Shape-piirteen).

Luokalla Square on yksi luontiparametri, joka kertoo kunkin sivun mitan.

Kun aliluokasta luodaan ilmentymä, on aluksi suoritettava myös yliluokassa määritellyt alustustoimenpiteet. Periytyvä luokka voi samalla välittää luontiparametreja yliluokalleen. Esimerkiksi tässä määritellään, että kun Square-oliota luodaan, tehdään aluksi samat alustustoimenpiteet kuin Rectangle-oliolle. Tämä tehdään niin, että kummaksikin suorakaiteen luontiparametriksi (eli kummaksikin sivunpituudeksi) tulee neliöolion saaman luontiparametrin arvo.

Konkreettisessa luokassa kaikilla metodeilla on toteutus. Voidaan myös määritellä abstrakti luokka, jollaisessa sopii olla abstrakteja, toteutuksettomia metodeita kuten piirreluokassakin. Tässä esimerkki:

abstract class Tuote(val alvLisatty: Boolean):

  def kokonaishinta: Double

  def verotonHinta =
    if this.alvLisatty then this.kokonaishinta / 1.24 else this.kokonaishinta

end Tuote

Sana abstract tekee luokasta abstraktin. Tästä luokasta ei voi luoda suoraan ilmentymiä.

kokonaishinta-metodi on abstrakti. Konkreettisten aliluokkien on tarjottava toteutus tälle metodille, jotta kaikki Tuote-tyyppiset oliot kykenevät tämän metodin suorittamaan.

Tässä vertailutaulukko luvusta 7.5:

Piirreluokka

Abstrakti
yliluokka
Konkreettinen
yliluokka

Voiko se sisältää abstrakteja metodeja?

Voi.

Voi.

Ei voi.

Voiko siitä luoda suoraan ilmentymiä?

Ei voi.

Ei voi.

Voi.

Voiko se välittää luontiparametreja yläkäsitteilleen?

Ei voi.

Voi.

Voi.

Voiko sellaisia periä useita (extends-sanan perässä)?

Voi.

Ei voi.

Ei voi.

Näitä tekniikoita voi myös yhdistellä keskenään. Luokka voi esimerkiksi periytyä yhdestä yliluokasta ja yhdestä tai useammasta piirreluokasta. Tai piirreluokka voi periytyä tavallisesta luokasta.

Scalan luokkahierarkia

Kaikki Scala-oliot ovat kattotyyppiä Any. Sillä on välittömät aliluokat AnyVal ja AnyRef:

  • AnyVal-luokasta periytyvät tutut tietotyypit Int, Double, Boolean, Char, Unit, ja muutama muu. Sille harvemmin laaditaan itse uusia aliluokkia, ja moinen pitää erikseen ilmoittaa. (JVM ei käsittele AnyVal-olioita viittausten kautta. AnyValien on oltava muuttumattomia ja täyttää muitakin tiukkoja ehtoja. Oikeissa paikoissa käytettyinä näillä tyypeillä voi parantaa suoritustehokkuutta.)

  • AnyRef, toiselta nimeltään Object, on yliluokka muille luokille ja yksittäisolioille. Esimerkiksi luokat String ja Vector periytyvät tästä luokasta. Myös itse laatimasi (ei-piirre-)luokat periytyvät automaattisesti AnyRefistä ellet erikseen toisin määrittele. (JVM käsittelee AnyRef-olioita viittausten kautta.)

Lisäksi on piirreluokka Matchable, joka kattaa kaikki sellaiset yläkäsitteenä kaikki sellaiset Scala-tyypit, joita on luvallista käyttää match-käskyssä. Sekä AnyRefillä että AnyValilla on tämä piirre, ja Matchable kattaakin lähes kaikki Scala-tyypit, poislukien eräät erikoistapaukset.

Lisää aiheesta luvussa 7.5.

Alakäsitteiden rajaaminen: sealed, open ja final

Sana sealed piirre- tai muun luokan alussa määrittelee suljetun tyypin. Se tarkoittaa, että tuolle luokalle ei voi määritellä muita välittömiä alatyyppejä kuin ne, jotka on samaan tiedostoon kirjattu (luku 7.4). Esimerkiksi Option-luokan määrittely alkaa näin:

sealed abstract class Option /* Etc. */

Option-luokasta periytyvät vain samassa kooditiedostossa määritellyt yksittäisolio None ja aliluokka Some. Näin taataan, että mikä tahansa Option on aina joko None tai jokin Some-olio.

Tavalliset konkreettiset luokat ovat aina "melkein suljettuja" ellei erikseen open- määreellä mainita. "Melkein suljettuja" siinä mielessä, että kääntäjä varoittaa, jos luokalle määrittelee välittömän aliluokan muussa tiedostossa — mutta kuitenkin sallii tuon. open-määreellisestä luokasta voi johtaa aliluokkia vapaasti (luku 7.5).

Sana final (luku 7.5) on vielä jyrkempi kuin sealed: se estää alakäsitteiden määrittelyn kokonaan. Sen voi kirjoittaa myös yksittäisen metodin määrittelyn alkuun (ennen def-sanaa), jolloin kyseistä metodia ei voi korvata alatyypeissä.

Luetelmatyypit: enum

Jos tyypin kukin olio on etukäteen tiedossa, voi nuo oliot määritellä luetelmatyypiksi. Luetelmatyyppi on kuin tavallinen luokka, mutta siitä ei voi luoda mitään muita ilmentymiä kuin koodiin erikseen listatut. Kaksi esimerkkiä luvusta 7.4:

enum Weekday:
  case Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
enum Month:
  case January, February, March, April, May, June, July,
       August, September, October, November, December

Alussa on enum.

Ilmentymät eli kaikki "tapaukset", jotka tästä tietotyypistä on olemassa, luetellaan. Kun nuo oliot eroavat toisistaan vain nimiensä osalta, kuten tässä, riittää yksi case-sana ja pilkuin eroteltu luettelo.

Kun luetelmatyypit on noin määritelty, niitä voi käyttää näin:

val today = Weekday.Mondaytoday: Weekday = Monday
val cruelest = Month.Aprilcruelest: Month = April
import Weekday.*val deadlineDay = WednesdaydeadlineDay: Weekday = Wednesday

Kaikkien luetelmatyyppien yhteyteen tulee automaattisesti määritellyksi eräitä metodeita, kuten values ja fromOrdinal:

Month.fromOrdinal(0)res226: Month = January
Month.fromOrdinal(11)res227: Month = December
Weekday.valuesres228: Array[Weekday] = Array(Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday)

Kuten tavalliselle luokalle myös luetelmatyypille voi määritellä ilmentymämuuttujia, luontiparametreja ja metodeita, kuten tässä lukuun 7.4 perustuvassa esimerkissä:

enum Rhesus(val isPositive: Boolean):
  case RhPlus  extends Rhesus(true)
  case RhMinus extends Rhesus(false)

  def isNegative = !this.isPositive
end Rhesus

Satunnaislukuja

Yksittäisolion Random-metodit tuottavat (näennäis)satunnaislukuja:

import scala.util.RandomRandom.nextInt(10)res229: Int = 8
Random.nextInt(10)res230: Int = 6
Random.nextInt(10)res231: Int = 2

Tässä arvotut luvut ovat väliltä 0–9 eli parametria 10 pienempiä.

Yllä käytetty yksittäisolio käyttää satunnaislukujen siemenenä tietokoneen kellonaikaa. Siemenen voi myös määrätä itse ja välittää Random-luokasta erikseen luodulle ilmentymälle:

val generaattori1 = Random(74534161)generaattori1: Random = scala.util.Random@75fbc2df
val generaattori2 = Random(74534161)generaattori2: Random = scala.util.Random@3f92984e

Tässä luotiin kaksi lukugeneraattoria ja määrättiin molemmille sama mielivaltaisesti valittu siemen.

generaattori1.nextInt(100)res232: Int = 53
generaattori1.nextInt(100)res233: Int = 38
generaattori1.nextInt(100)res234: Int = 97
generaattori2.nextInt(100)res235: Int = 53
generaattori2.nextInt(100)res236: Int = 38
generaattori2.nextInt(100)res237: Int = 97

Molemmat generaattorit käyttävät samaa näennäissatunnaislukuja tuottavaa algoritmia. Kun siemenluku on sama, identtinen nextInt-metodikutsujen sarja tuottaa samat luvut.

Random-olioilla on myös muita "arpomiseen" perustuvia metodeita kuin nextInt. Mainitsemisen arvoinen on ainakin kokoelman järjestyksen uusiva shuffle (luku 8.1):

val lukuja = (1 to 10).toVectorlukuja: Vector[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
Random.shuffle(lukuja)res238: Vector[Int] = Vector(8, 9, 7, 4, 6, 1, 10, 2, 5, 3)
Random.shuffle(lukuja)res239: 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.2). Ohjelma tulostaa tiedoston example.txt kunkin rivin ja sen eteen rivinumeron:

import scala.io.Source

@main def tulostaNumeroituna() =

  val tiedosto = Source.fromFile("alikansio/esimerkki.txt")

  try
    var rivinumero = 1
    for rivi <- tiedosto.getLines do
      println(rivinumero + ": " + rivi)
      rivinumero += 1
    end for
  finally
    tiedosto.close()

end tulostaNumeroituna

Metodi fromFile ottaa parametriksi tiedostopolun ja palauttaa Source-tyyppisen olion, jonka kauttaa voi pyytää tiedoston sisältöä. Polku voi olla suhteellinen (kuten tässä) tai absoluuttinen.

Silmukalla käydään tässä läpi kukin niistä riveistä, jotka getLinesia kutsumalla saadaan. (On myös muita tapoja käydä läpi tiedoston sisältöä kuin rivi kerrallaan; ks. luku 12.2.)

tryfinally-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

@main def writingExample() =

  val fileName = "examplefolder/random.txt"
  val file = PrintWriter(fileName)
  try
    for n <- 1 to 10000 do
      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()

end writingExample

PrintWriter-olion voi luoda näin. Parametriksi annetaan kirjoitettavan tiedoston nimi.

println-metodilla voi kirjoittaa tiedostoon yhden rivin tekstiä.

Yhteyden sulkeminen on erityisen tärkeää, kun tiedostoon kirjoitetaan, koska vasta yhteyttä suljettaessa vahvistuu viimeisten merkkien tallennus levylle.

Graafiset käyttöliittymät

Graafisia käyttöliittymiä laaditaan apukirjastoa käyttäen. O1:n materiaalissa esiintyy kaksi eri kirjastoa: O1Library-moduulin GUI-työkalut sekä yleisempi Swing-kirjasto.

o1-kirjaston työkalut

Kurssin oman GUI-työkalupakin keskeisin osa on luokka o1.View. Alla on pääasiat kokoava esimerkki.

View-luokan ajatuksena on, että View-olio tarjoaa ikkunanäkymän johonkin olioon, joka toimii sovelluksen aihealueen mallina (luku 2.7). Seuraavassa esimerkissämme mallin muodostaa yksi tämän pikkuluokan ilmentymä:

// Kappale on muuttuvatilainen olio. Sillä on sijainti ja väri.
class Kappale(var vari: Color):
  var sijainti = Pos(10, 10)

  def liiku() =
    this.sijainti = this.sijainti.add(1, 1)

  def palaa() =
    this.sijainti = Pos(10, 10)

end Kappale

Laaditaan tämän näköinen käyttöliittymä, jossa kappale on piirretty kaksiväristä taustaa vasten ympyränä:

../_images/o1_view_example.png

Esimerkkiohjelman "kappale" liikkuu vähitellen oikealle ja alas. Se palaa takaisin alkuun tuplaklikkauksella ja vaihtaa väriä sen mukaan, kummalla taustalla hiiren kursori on.

Toteuttava koodi:

val kappale = Kappale(Blue)
val tausta = rectangle(200, 400, Red).leftOf(rectangle(200, 400, Blue))

object testiGUI extends View("A Diagonally Moving Thing", 10):

  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 then Red else Blue

  override def onClick(klikkaus: MouseClicked) =
    if klikkaus.clicks > 1 then
      kappale.palaa()

  override def isDone = kappale.sijainti.x > 400
end testiGUI

@main def kaynnistaSovellus() =
  testiGUI.start()

Käyttöliittymämme on yksittäisolio, joka on erikoistapaus View-tyypistä.

Annamme View-oliolle parametriksi ikkunan otsikon. Valinnaisena lisäparametrina voi antaa mm. ajan tikitysnopeuden (tässä: 10).

View-oliolle on määriteltävä makePic-metodi, joka määrittää, millainen kuva kullakin ajanhetkellä piirretään näkyviin. Tässä muodostamme kuvan asettamalla ympyrän kuvan taustaneliötä vasten.

Tapahtumankäsittelijämetodit (luku 3.1) reagoivat ajan kulumiseen ja käyttäjän toimiin. Tässä muutama esimerkki: kappale etenee tikittäessä, vaihtaa väriä hiiren liikkuessa ja palaa alkuun tuplaklikkauksella.

Poimimme talteen hiiren klikkausta kuvaavan MouseClicked-olion ja kysymme siltä klikkausten lukumäärää (luku 3.6).

isDone-metodi määrää, milloin käyttöliittymä lakkaa reagoimasta tapahtumiin. Tässä esimerkissä se tapahtuu, kun kappale on liikkunut tietyn matkaa oikealle.

View-olion luominen ei vielä tuo mitään näkyviin eikä aloita ajan "tikitystä". Nämä hoituvat start-metodia kutsumalla.

Lisätietoja luvuista 3.1, 3.6 ja luokan dokumentaatiosta.

Swing-käyttöliittymäkirjasto

GUI-ohjelmointiin tarkoitettua Swing-kirjastoa on esitelty luvussa 12.4. Tässä kokoava esimerkki sieltä:

../_images/gui6-fi.png
import scala.swing.*
import scala.swing.event.*

object Tapahtumakokeilu extends SimpleSwingApplication:
  val ekaNappi = Button("Paina minua")( () )
  val tokaNappi = Button("Ei kun MINUA!")( () )
  val kehote = Label("Paina jompaakumpaa napeista.")

  val kaikkiJutut = BoxPanel(Orientation.Vertical)
  kaikkiJutut.contents ++= Vector(kehote, ekaNappi, tokaNappi)
  val nappulaikkuna = MainFrame()
  nappulaikkuna.contents = kaikkiJutut
  nappulaikkuna.title = "Kokeiluikkuna"

  this.listenTo(ekaNappi, tokaNappi)
  this.reactions += {
    case painallus: ButtonClicked =>
      val lahdenappula = painallus.source
      val viesti = "Painoit nappia, jossa lukee: " + lahdenappula.text
      Dialog.showMessage(kaikkiJutut, viesti, "Viesti")
      lahdenappula.text = lahdenappula.text + "!"
  }

  def top = this.nappulaikkuna
end Tapahtumakokeilu

Sovellusta kuvaa yksittäisolio, joka periytyy Swing-käyttöliittymien laatimiseen sopivasta luokasta.

Luodaan käyttöliittymäelementtejä kuvaavat oliot.

Asemoidaan elementit paneeliin allekkain.

Sijoitetaan paneeli ikkunan sisällöksi ja alustetaan ikkunan ominaisuudet muutenkin.

Asetetaan olio (tässä: sovellusolio itse) tapahtumankuuntelijaksi nappuloille.

Määritellään, miten havaittuihin tapahtumiin reagoidaan.

Kun tapahtuma havaitaan, suoritetaan apuikkunan näyttävä koodi. Koodissa voi käyttää muuttujaa painallus, johon on tallentunut tapahtumaa kuvaava ButtonClicked-tyyppinen olio.

SimpleSwingApplicationit tarvitsevat pääikkunan (top), joka tulee näkyviin käynnistäessä.

Yllä esitetty tapa käsitellä tapahtumia on yleispätevämpi, mutta yksinkertaisiin tilanteisiin riittää, kun annat Button-oliolle koodia, joka ajetaan, kun nappulaa painetaan:

val nappula = Button("Nappulan teksti")( koodiJokaAjetaanPainettaessa() )

Lisää Swing-esimerkkejä löytyy luvusta 12.4.

Varatut sanat

Scala-kielen varatut sanat eli sanat, joita ei voi käyttää tunnuksina, ovat:

abstract case   catch    class     def      do       else       enum    export  extends
false    final  finally  for       given    if       implicit   import  lazy    match
new      null   object   override  package  private  protected  return  sealed  super
then     throw  this     trait     true     try      type       val     var     while
with     yield
:        =      <-       =>        <:       >:       #          @       =>>     ?=>

Lisäksi seuraavat sanat ovat "pehmeästi varattuja", mikä tarkoittaa, ettei niiden käyttö tunnuksina ole kielletty, mutta niillä on tietyissä yhteyksissä erityismerkityksiä:

as      derives  end   extension   infix   inline   opaque  open  transparent  using
|       *        +     -

Palaute

Tekijät

Tämän oppimateriaalin kehitystyössä on käytetty apuna tuhansilta opiskelijoilta kerättyä palautetta. Kiitos!

Materiaalin luvut tehtävineen ja viikkokoosteineen on laatinut Juha Sorva.

Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.

Tehtävien automaattisen arvioinnin ovat toteuttaneet: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Joel Toppinen, Anna Valldeoriola Cardó ja Aleksi Vartiainen.

Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.

Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista suunnittelivat Juha Sorva ja Teemu Sirkiä. Teemu Sirkiä ja Riku Autio toteuttivat ne apunaan Teemun aiemmin rakentamat työkalut Jsvee ja Kelmu.

Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset laati Juha Sorva.

O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen, Juha Sorva ja Jaakko Nakaza. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.

Tapa, jolla käytämme O1Libraryn työkaluja (kuten Pic) yksinkertaiseen graafiseen ohjelmointiin, on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.

Oppimisalusta A+ luotiin alun perin Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Nykyään tätä avoimen lähdekoodin projektia kehittää Tietotekniikan laitoksen opetusteknologiatiimi ja tarjoaa palveluna laitoksen IT-tuki; sitä ovat kehittäneet kymmenet Aallon opiskelijat ja muut.

A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen suunnitteluun ja toteutukseen on osallistunut useita opiskelijoita yhteistyössä O1-kurssin opettajien kanssa.

Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.

Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.

a drop of ink
Palautusta lähetetään...