Luku 5.3: Oliot funktioina, luokat olioina
Johdanto
Tässä melko lyhyessä luvussa jatkamme sen setvimistä, miten laaja merkitys olioilla Scala-ohjelmissa onkaan. Kohtaamme muutaman erillisen aiheen, jotka liittyvät tuohon teemaan.
Erikoismetodi apply
Pohjustava esimerkki
val vektori = Vector("eka", "toka", "kolmas", "neljäs", "viides")res0: Vector[String] = Vector(eka, toka, kolmas, neljäs, viides) vektori.apply(3)res1: String = neljäs vektori(3)res2: String = neljäs
Esimerkki saattaa nostattaa pari kysymystä:
Vektorin yksittäisen alkion voi katsoa ilmaisulla
vektori(indeksi)
tai pidemmällä ilmaisullavektori.apply(indeksi)
. Miksi nämä kaksi erinäköistä mutta pohjimmiltaan samanlaista tapaa tehdä sama asia?Olioitahan kuuluu komentaa metodikutsuilla, joten
vektori.apply(indeksi)
näyttää järkevältä. Mutta eikö ilmaisuvektori(indeksi)
ole vähän outo? Siinähän on viittausvektori
-muuttujan osoittamaan olioon, jolle sitten jotenkin annetaan parametriksiindeksi
-lausekkeen arvo. Käskyllä on funktiokutsun muoto mutta se on pikemminkin "oliokutsu". Ei kai oliota voi "suorittaa"?
Vastaukset liittyvät toisiinsa. Aloitetaan vyyhden purkaminen.
"Olio funktiona"
Oliota ei varsinaisesti voi määrätä suoritettavaksi. Oliohan on kuvaus jostakin, johon liittyy metodeita, ja nimenomaan olion metodeita voi määrätä suoritettaviksi kutsumalla niitä.
Mistä sitten on kysymys käskyssä vektori(indeksi)
? Vastaus löytyy Scala-kielen
määrittelystä ja siitä, että apply
-nimi on kielessä erikoisasemassa.
Kun olioon viittaavan lausekkeen perään kirjoitetaan parametreja sulkeisiin, niin ilmaisu
tulkitaan kyseisen olion apply
-nimisen metodin kutsuksi. vektori(indeksi)
on siis
toinen tapa antaa käsky vektori.apply(indeksi)
.
Sama toimii esimerkiksi merkkijonoille: "kissa".apply(0)
ja "kissa"(0)
tarkoittavat
samaa ja palauttavat molemmat merkin k
.
Kyseessä on yleinen sääntö, joka ei liity nimenomaisesti vektoreihin tai merkkijonoihin.
Voit ajatella vaikkapa niin, että apply
-metodi — jos oliolle sellainen on määritelty
— on eräänlainen "olion oletusmetodi", jonka olio suorittaa, kun ei muutakaan metodia
ole pisteen perässä mainittu.
Toinen tapa ajatella asiaa on, että apply
-metodi mahdollistaa "olion käyttämisen ikään
kuin se olisi funktio". On silti tärkeää hahmottaa, että kutsu olio(parametrit)
määrää suoritettavaksi nimenomaan olion apply
-metodin eikä oliota itseään jossakin
yleisemmässä mielessä. Olio ei varsinaisesti ole funktio, vaan kyseessä on vain
lyhennysmerkintä.
Erilaisia apply
-metodeita
Eri luokkiin määritellyt apply
-metodit voivat tehdä erilaisia asioita. Esimerkkinä näit
jo vektorien, merkkijonojen ja muiden kokoelmien apply
n, joka on määritelty Scala-peruskirjastossa.
Scalan luojat ovat katsoneet käteväksi, että ilmaisulla kokoelma(indeksi)
voi poimia
tietyn alkion kokoelmasta. Niinpä kyseiselle metodille on annettu nimeksi juuri apply
.
apply
-metodin voi laatia itsekin mihin tahansa luokkaan tai yksittäisoliolle. Ohjelmoija
voi määrätä sen tarvitsemat parametrit ja muun toiminnan. Esimerkiksi näin:
object testiolio:
def apply(sana: String, toinen: String) =
println(sana + ", " + toinen + "!")
Nyt sekä käsky testiolio.apply("Ave", "Munde")
että käsky testiolio("Ave", "Munde")
kutsuvat apply
-metodia, joka tulostaa Ave, Munde!
.
Tällä kurssilla sinun ei useinkaan tarvitse itse laatia apply
-metodeita. Silti niiden
perusajatus on hyvä ymmärtää, jotta hahmotat paremmin esimerkiksi juuri vektoreita ja
merkkijonoja käsittelevät käskyt ja osaat tulkita virheilmoituksia sujuvammin.
Kumppanioliot eli "luokka oliona"
Nyt toinen aihe. (Senkin yhteydessä tosin apply
nousee vielä ohimennen esiin.)
Johdanto: asiakasluokka
Tarkastellaan pientä esimerkkiä. Olemme laatimassa luokkaa — vaikkapa Asiakas
—
ja tavoitteenamme on numeroida kaikki tästä luokasta luodut ilmentymät positiivisilla
kokonaisluvuilla. Kokemus kertoo, että aloittelevan ohjelmoijan ensimmäinen luonnos
ratkaisusta voi näyttää suunnilleen tältä:
class Asiakas(val nimi: String): Käytetään askeltajamuuttujaa montakoLuotu kirjaamaan, montako asiakasta on; alkuarvo 0. Kun asiakas luodaan, kasvatetaan askeltajan arvoa yhdellä. val numero = askeltajan arvo eli tähän mennessä luotujen asiakkaiden lukumäärä override def toString = "#" + this.numero + " " + this.nimi end Asiakas
Pseudokoodiluonnos kääntyy suoraan Scalaksi näin:
class Asiakas(val nimi: String):
private var montakoLuotu = 0
this.montakoLuotu += 1
val numero = this.montakoLuotu
override def toString = "#" + this.numero + " " + this.nimi
end Asiakas
Kun tätä luokkaa käyttää, ei homma toimikaan niin kuin piti. Mieti koodia ja seuraavaa animaatiota sen suorituksesta ja selvitä, missä vika on.
Luokkakohtaiset ominaisuudet
Luvusta 2.3 asti on ollut esillä ajatus siitä, että luokka kuvaa tietynlaisten olioiden tietotyypin. Luokka määrittelee yleisiä piirteitä ilmentymistään; kullakin oliolla on omat kappaleensa luokan kuvaamista ilmentymämuuttujista. Juuri viimeksi mainittu seikka sai äskeisen asiakkaiden numerointiyrityksen epäonnistumaan.
On tilanteita, joissa haluamme liittää luokan kuvaamaan käsitteeseen yleisesti tiettyjä ominaisuuksia tai toimintoja. Haluamme siis, että nämä ominaisuudet tai toiminnot eivät liity kuhunkin kyseisentyyppiseen olioon erikseen vaan käsitteeseen itseensä kokonaisuutena. Esimerkkimme oliolaskuri on juuri tällainen ominaisuus: kullakin oliolla on numeronsa, mutta olioiden kokonaismäärä ei ole minkään yhden olion ominaisuus vaan koko asiakaskäsitteen.
Haluaisimme siis, että asiakasesimerkkimme toimisi suunnilleen näin:
Animaatio korostaa: haluaisimme tässä tapauksessa käsitellä luokkaa ikään kuin sekin olisi olio — ei eräs asiakasolio, vaan asiakaskäsitettä kuvaava olio, jolla on ominaisuutena laskurimuuttuja.
Scala-luokka ei itse ole olio, jota voisi käyttää aivan siihen tapaan kuin äskeisessä animaatiossa. Mutta luokalle voidaan määritellä kaveri, joka on olio ja jonka avulla ylle piirretty algoritmi voidaan toteuttaa.
Luokan ja olion läheinen suhde (Katso kuvat!)
Tämä versio asiakasohjelmasta toimii halutulla tavalla:
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 + " " + nimi
end Asiakas
Kumppaniolio Asiakas
ei ole asiakasluokan ilmentymä eli sellainen olio, joka
luodaan käskyllä Asiakas(nimi)
. Sen tyyppi ei ole Asiakas
! Kumppaniolio on
erillinen olio, johon on määritelty Asiakas
-käsitteeseen yleisesti liittyviä
piirteitä (tässä vain yksi).
Asiakas
-kumppanioliolla on muuttuja montakoLuotu
.
Tästä muuttujasta on muistissa vain yksi kopio, koska
kumppanioliotakin on vain yksi (ks. animaatio alla).
Vrt. asiakasolioiden nimet ja numerot, joita on yksi per
asiakasolio. Samoin alustus nollaksi tehdään vain kerran,
kun ohjelma ladataan käyttöön ja kumppaniolio tulee luoduksi.
Määrittelemme, että aina uutta Asiakas
-tyyppistä
oliota luotaessa kasvatetaan Asiakas
-kumppaniolion
montakoLuotu
-muuttujan arvoa yhdellä ja sitten sen uusi
arvo kopioidaan asiakasolion ilmentymämuuttujaan numero
.
Asiakas
-luokka ja sen kumppaniolio ovat "kavereita", jotka
pääsevät poikkeuksellisesti käsiksi myös toistensa yksityisiin
tietoihin.
Kumppanioliot ovat siis yksittäisolioita. Scalan yksittäisoliot voidaan jakaa:
kumppaniolioihin, joilla on sama nimi jonkin samassa tiedostossa määritellyn luokan kanssa, ja
itsenäisiin yksittäisolioihin (standalone object), joilla ei ole luokkakumppania vaan jokin muu tarkoitus ohjelmassa.
Lähestulkoon kaikki aiemmissa luvuissa kohtaamasi yksittäisoliot olivat itsenäisiä.
Yksi lisäesimerkki kumppanioliosta löytyy Stars-moduulin StarCoords
-luokan
yhteydestä: samassa tiedostossa on määritelty myös StarCoords
-niminen kumppaniolio.
Metodit kumppanioliossa
Kumppaniolioon voi myös määritellä metodeita aivan samoin kuin muihinkin yksittäisolioihin.
Lisätään kokeeksi yksi metodi Asiakas
-kumppaniolioon.
object Asiakas:
private var montakoLuotu = 0
def tulostaMaara() =
println("On luotu " + this.montakoLuotu + " asiakasoliota.")
end Asiakas
Nyt käskyllä Asiakas.tulostaMaara()
saa tulostettua raportin luotujen asiakasolioiden
lukumäärästä.
Kumppaniolion tietotyyppi
Kuten todettu, kumppaniolio ei ole ilmentymä siitä luokasta, jonka kumppani se on, vaan erillinen olio. Se ei siis ole tuon luokan määrittelemää tietotyyppiä.
Kullakin kumppanioliolla on oma tietotyyppinsä, johon ei kuulu
mikään muu olio. Samahan pätee yksittäisolioihin yleisemminkin
(luku 2.3). Tässä näkyy ero Asiakas
-luokan ilmentymien tyypin
ja Asiakas
-kumppaniolion tyypin välillä:
Asiakas("Maija Mikälienen")res3: Asiakas = #1 Maija Mikälienen Asiakas("Matti Mikälienen")res4: Asiakas = #2 Matti Mikälienen Asiakasres5: Asiakas.type = Asiakas$@185ea23
Asiakas
on tyyppi, jonka ilmentymiä ovat
kaikki asiakasluokasta luodut oliot.
Asiakas.type
on asiakasluokan kumppaniolion
(eikä minkään muun olion) tietotyyppi.
static
?
Tiedoksi Javalla ja sen lähisukulaiskielillä aiemmin ohjelmoineille:
kumppaniolioilla (kuten joillakin muillakin yksittäisolioilla) on
Scala-ohjelmissa monesti samansuuntainen tehtävä kuin Java-ohjelmissa
on sellaisilla muuttujilla ja metodeilla, jotka on määritelty sanaa
static
käyttäen. Scalassa ei tarvita (eikä ole) static
-määrettä.
Kumppaniolioiden käyttötarkoituksia
Luokan ilmentymälaskuri on klassinen johdantoesimerkki, joka selventää käsitteitä, mutta sitä yleisempiä käyttötarkoituksia kumppaniolioille ovat:
Vakiot: Kumppaniolio voi olla hyvä sijoituspaikka vakioille. Monet vakiot liittyvät tiettyyn luokkaan yleisesti eivätkä ole ilmentymäkohtaisia. Esimerkiksi Stars-moduulin
StarCoords
-luokan kumppanioliossa on määritelty vakiotMinValue
jaMaxValue
, joihin on tallennettu koordinaattien raja-arvot (-1.0 ja +1.0).Apufunktiot: Joskus on tarvetta tiettyyn luokkaan liittyville apufunktioille, jotka eivät ole ilmentymäkohtaisia. Sellaisen funktion voi kirjoittaa metodiksi luokan kumppanioliolle. Kumppaniolio toimii tällöin sijoituspaikkana luokkaan liittyville apufunktioille. Nuo apufunktiot voivat olla joko julkisia tai — jos tarkoitettu vain luokan omaan käyttöön — yksityisiä.
Kumppaniolio ei ole pakollinen siinä mielessä, etteikö moisia vakioita tai apufunktioita voisi muuallekin sijoittaa. Se on kuitenkin usein siisti ja hyvä ratkaisu mainittuihin tarpeisiin. Monilla luokilla Scala APIssa on kumppaniolio.
Lisämateriaalia
Seuraavat asiat eivät ole tämän kurssin tai yleisen ohjelmointiosaamisen kannalta välttämättömiä, mutta niiden tuntemisesta on joskus iloa Scala-kielellä ohjelmoivalle.
Yksittäisolion käyttö pakkauksen tapaan
Olion metodeita — ja muuttujiakin — voi ottaa käyttöön import
-käskyllä.
Kokeillaan:
object tyokalusto: def kolmenSumma(a: Int, b: Int, c: Int) = a + b + c val Greeting = "Ave!"// defined object tyokalusto import tyokalusto.*
Tuo import
-käsky ottaa käyttöön tyokalusto
-yksittäisolion
sisällön. Tämän jälkeen kyseisiä metodeita ja metodeita voi
käyttää mainitsematta olion nimeä, kuten alla.
kolmenSumma(100,10,1)res6: Int = 111 Greetingres7: String = Ave!
Nyt esimerkiksi kolmenSumma
-metodia kutsuessa ei näytä siltä,
että kutsuisimme tuon olion metodia. Käytämme kolmenSumma
a ikään
kuin irrallisena funktiona, kuin mitään oliota ei olemassa olisikaan.
Silloin tällöin on näppärää määritellä yksittäisolio, joka sisältää
valikoiman enemmän tai vähemmän toisiinsa liittyviä metodeita ja
jota on tarkoituskin käyttää tuolla tavoin "pakkauksen kaltaisesti",
oliosta import
aten.
println
, readLine
ja "pakkausmaiset" oliot Scala APIssa
Usein käyttämämme println
-funktio on itse asiassa Predef
-nimisen
yksittäisolion metodi. Tuo yksittäisolio, jonka nimi tulee sanasta
predefined, on Scalassa erikoisasemassa sikäli, että sen metodeita
voi aina käyttää missä vain Scala-ohjelmassa ilman erillisiä import
-
käskyjä ja mainitsematta olion nimeä.
Luvussa 2.7 käytimme puolestaan StdIn
-nimistä yksittäisoliota
"pakkausmaisesti", kun poimimme käyttöön mm. readLine
-metodin käskyllä
import scala.io.StdIn.*
.
import
in käyttö muun koodin seassa
Nähdyissä esimerkeissä olemme tavanneet sijoittaa import
-käskyt
Scala-kooditiedostojen alkuihin. Se onkin varsin yleistä muutenkin.
import
-käskyä voi käyttää Scalassa toisaallakin, esimerkiksi
paikallisesti luokan tai yksittäisen metodinkin sisällä:
import pakkaus1.*
class X:
import pakkaus2.*
def metodiA =
// Täällä voi käyttää pakkauksia 1 ja 2.
def metodiB =
import pakkaus3.*
// Täällä voi käyttää pakkauksia 1, 2 ja 3.
end X
class Y:
// Täällä voi käyttää vain pakkausta 1.
end Y
Tämä joskus selkiyttää koodia.
Ilmentymistä import
aaminen
Yllä selvisi, että yksittäisoliota voi käyttää kuin pakkausta ja
siitä voi siis poimia metodeita käyttöön import
-käskyllä. Itse
asiassa saman voi tehdä myös luokan ilmentymälle, jos sattuu
haluamaan.
class Ihminen(val nimi: String): val onKuolevainen = true def tervehdys = "Moi, olen " + this.nimi// defined class Ihminen val sokke = Ihminen("Sokrates")sokke: Ihminen = Ihminen@1bd0b5e import sokke.*tervehdysres8: String = Moi, olen Sokrates
Yllä siis viimeinen käsky on lyhennysmerkintä ilmaisusta
sokke.tervehdys
.
Tällainen ilmentymästä import
aaminen kylläkin helposti lähinnä
sekavoittaa koodia.
Scalan toteutusyksityiskohtia ja new
-sana
Tämä laatikko tarinoi hieman siitä, miten ilmentymien luonti
on Scalaan sisäisesti toteutettu. Voit mainiosti ohittaa tämän.
Mutta lukaise, jos aihe sattuu kiinnostamaan tai jos new
-käsky
on sinulle muista kielistä tuttu ja mietit sen roolia Scalassa.
Joissain muissa ohjelmointikielissä oliot luodaan new
-avainsanalla.
Esimerkiksi jos on määritelty luokka Elain
, ilmentymä luodaan näin:
new Elain("laama")
Scalassahan olemme tottuneet kirjoittamaan yksinkertaisemmin:
Elain("laama")
Myös ensimmäinen, new
-sanallinen versio toimii Scalassa, ja new
on yksi Scalan varatuista sanoista. Sen nimenomainen
merkitys on luoda uusi ilmentymä luokasta. Kuitenkaan tuota sanaa
ei (lähes koskaan) ole pakko Scala-sovellukseen kirjoittaa. Miksei?
Sisäisesti Scala toimii seuraavasti. Kun kirjoitat vaikkapa tällaisen luokan:
class Elain(val laji: String)
niin määritellyksi tulee automaattisesti sille myös kumppaniolio,
jossa on luontiparametreja vastaava apply
-metodi. Siis
tällainen:
object Elain:
def apply(laji: String) = new Elain(laji)
Automaattisesti luodulla oliolla on apply
-metodi, joka
luo ilmentymän käyttäen sisäisesti new
-käskyä.
Tuo automaattinen kumppaniolio ei näy koodissasi mutta on kuitenkin
olemassa. Ellei sitä olisi, täytyisi Elain
-luokan ilmentymät
Scalassakin luoda aina käskyllä new Elain(...)
. Kumppaniolion ja
sen apply
-metodin ansiosta toimivat myös käskyt Elain.apply(...)
ja Elain(...)
. Näistä jälkimmäinen on lyhyin ja tavallisin tapa
luoda olio. (Näin on Scalan versiosta 3 alkaen. Kielen vanhoissa
versioissa new
-sanaa käytettiin runsaasti.) Lisätietoja:
Universal Apply Methods.
Mainitaanpa vielä, että tuollaisessa automaattisesti tuotetussa
apply
-metodissa on itse asiassa def
in edessä vielä yksi sana.
Tuo lisäsana, inline
, ei ole tässä varsinaisesti välttämätön mutta
tehostaa koodia. Näin määritelty metodi ei saa kutsupinossa omaa
kehystään kuten tavalliset metodit, vaan metodia kutsuva kooditeksti
käytännössä korvautuu metodin rungolla new Elain(...)
jo ennen
ohjelma-ajoa. Teemasta on yleisempää lisätietoa Wikipedian artikkelissa
Inline expansion.
Yhteenvetoa
Jos oliolla on
apply
-niminen metodi, se toimii ikään kuin "oletusmetodina": kutsuolio(parametrit)
laventuu automaattisesti muotoonolio.apply(parametrit)
.apply
-metodillista oliota voi siis käyttää "vähän kuin se olisi funktio".Tätä tekniikkaa on käytetty mm. Scalan merkkijono- ja vektoriluokissa. Niiden
apply
-metodit palauttavat parametrin määrämälle indeksille tallennetun tiedon.
Joskus on tarpeen kuvata sellaisia ominaisuuksia tai toimintoja, jotka eivät liity yksittäisiin luokan ilmentymiin (olioihin) vaan luokkaan itseensä. Tähän tarkoitukseen voi Scalassa määritellä luokalle kumppaniolion.
Kumppaniolio on hyvä paikka esimerkiksi luokkaan liittyville vakioille.
Luokka ja sen kumppaniolio pääsevät käsiksi toistensa yksityisiinkin osiin.
Lukuun liittyviä termejä sanastosivulla:
apply
; kumppaniolio.
Palaute
Huomaathan, että tämä on henkilökohtainen osio! Vaikka olisit tehnyt lukuun liittyvät tehtävät parin kanssa, täytä palautelomake itse.
Tekijät
Tämän oppimateriaalin kehitystyössä on käytetty apuna tuhansilta opiskelijoilta kerättyä palautetta. Kiitos!
Materiaalin luvut tehtävineen ja viikkokoosteineen on laatinut Juha Sorva.
Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.
Tehtävien automaattisen arvioinnin ovat toteuttaneet: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Joel Toppinen, Anna Valldeoriola Cardó ja Aleksi Vartiainen.
Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.
Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista suunnittelivat Juha Sorva ja Teemu Sirkiä. Teemu Sirkiä ja Riku Autio toteuttivat ne apunaan Teemun aiemmin rakentamat työkalut Jsvee ja Kelmu.
Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset laati Juha Sorva.
O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen, Juha Sorva ja Jaakko Nakaza. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.
Tapa, jolla käytämme O1Libraryn työkaluja (kuten Pic
) yksinkertaiseen graafiseen
ohjelmointiin, on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi
oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.
Oppimisalusta A+ luotiin alun perin Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Nykyään tätä avoimen lähdekoodin projektia kehittää Tietotekniikan laitoksen opetusteknologiatiimi ja tarjoaa palveluna laitoksen IT-tuki; sitä ovat kehittäneet kymmenet Aallon opiskelijat ja muut.
A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen suunnitteluun ja toteutukseen on osallistunut useita opiskelijoita yhteistyössä O1-kurssin opettajien kanssa.
Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.
Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.
Määritellään luokan
Asiakas
lisäksi luokan kumppaniolio (companion object). Kumppaniolio on yksittäisolio, jolle annetaan prikulleen sama nimi kuin luokalle itselleen ja jonka määrittely kirjoitetaan samaan tiedostoon.