Tämä kurssi on jo päättynyt.

Kurssin viimeisimmän version löydät täältä: O1: 2024

Luku 5.3: Oliot funktioina, luokat olioina

Tästä sivusta:

Pääkysymyksiä: Ovatko luokat olioita? Oliot luokkia? Funktiot olioita vai oliot funktioita? Pysyykö pääni kasassa?

Mitä käsitellään? apply-metodit. Luokkien kumppanioliot. Tehdasmetodit.

Mitä tehdään? Lähinnä luetaan.

Suuntaa antava työläysarvio:? Menisikö puoleen tuntiin?

Pistearvo: B5.

Oheisprojektit: Ei ole.

../_images/person03.png

Johdanto

Tässä suhteellisen lyhyessä luvussa jatkamme sen setvimistä, miten laaja merkitys olioilla Scala-ohjelmissa onkaan.

Tässä aluksi pieni vektoriesimerkki:

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

Nostetaan esille muutama kysymys.

  1. Vektorin yksittäisen alkion voi katsoa ilmaisulla vektori(indeksi) tai pidemmällä ilmaisulla vektori.apply(indeksi). Miksi nämä kaksi erinäköistä mutta pohjimmiltaan samanlaista tapaa tehdä sama asia?
  2. Olioitahan kuuluu komentaa metodikutsuilla, joten vektori.apply(indeksi) näyttää järkevältä. Mutta eikö ilmaisu vektori(indeksi) ole vähän outo? Siinähän on viittaus vektori-muuttujan osoittamaan olioon, jolle sitten jotenkin annetaan parametriksi indeksi-lausekkeen arvo. Käskyllä on funktiokutsun muoto mutta se on pikemminkin "oliokutsu". Ei kai oliota voi "suorittaa"?
  3. Entä vektoriolion luova käsky? Luomme yleensä olioita new-sanalla; miksi se jätetään tässä pois?

Vastaukset liittyvät toisiinsa ja ovat monelta osin kytköksissä nimenomaan Scala-kieleen. Aloitetaan vyyhden purkaminen kahdesta ensimmäisestä kysymyksestä.

Erikoismetodi apply

"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 sulkuihin, 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". Kuitenkin on 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. Esimerkki, jonka jo näit, on vektorien, merkkijonojen ja muiden kokoelmien apply, joka on määritelty Scala-peruskirjastossa. On katsottu käteväksi, että ilmaisulla kokoelma(indeksi) voi katsoa tietyn alkion kokoelmasta. Niinpä kyseisille metodeille 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.

Olkoon x Vector[String]-tyyppinen muuttuja, joka viittaa johonkin vektoriolioon, jossa on vähintään yksi alkio. Arvioi pohtimalla ja REPLissä kokeilemalla, mitkä seuraavista väittämistä pitävät paikkansa.

Kumppanioliot eli "luokat olioina"

Siirrytään toiseen aiheeseen. Senkin yhteydessä tosin apply nousee vielä esiin.

Johdanto: asiakasluokka

Tarkastellaan pientä esimerkkiä. Olemme laatimassa luokkaa — vaikkapa Asiakas — ja tavoitteenamme on numeroida kaikki tästä luokasta luodut ilmentymät eri 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

}

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

}

Kuitenkin kun tätä luokkaa käyttää, ei homma toimikaan niin kuin piti. Mieti koodia ja seuraavaa animaatiota sen suorituksesta ja selvitä, missä vika on.

Mitkä seuraavista yllä olevaa esimerkkiä koskevista väitteistä pitävät paikkansa? Oletetaan, että numero- ja montakoLuotu-muuttujien tarkoitus on sama kuin edellä.

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 halutaan liittää luokan kuvaamaan käsitteeseen yleisesti tiettyjä ominaisuuksia tai toimintoja. Siis halutaan, 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. Kuitenkin 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
}

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

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

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

Kumppaniolio Asiakas ei ole asiakasluokan ilmentymä eli sellainen olio, joka luodaan käskyllä new 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:

  1. kumppaniolioihin, joilla on sama nimi jonkin samassa tiedostossa määritellyn luokan kanssa, ja
  2. 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ä, käynnistysoliot mukaan lukien.

Yksi lisäesimerkki kumppanioliosta löytyy Stars-projektin 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.")
  }

}

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

new Asiakas("Maija Mikälienen")res3: Asiakas = #1 Maija Mikälienen
new 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?

Aiemmin muilla kielillä ohjelmoineille tiedoksi: Scalan kumppaniolioilla (kuten joillakin muillakin yksittäisolioilla) on Scala-ohjelmissa samansuuntainen tehtävä kuin esimerkiksi Java-ohjelmissa on sellaisilla muuttujilla ja metodeilla, jotka on määritelty sanaa static käyttäen. Scalassa ei tarvita (eikä ole) static-määrettä, vaan kaikki hoidetaan olioilla.

Kumppaniolioiden käyttötarkoituksia

Luokan ilmentymälaskuri on klassinen johdantoesimerkki, joka selventää käsitteitä, mutta yleisemmin käyttötarkoituksia kumppaniolioille ovat:

  1. Vakiot: Kumppaniolio voi olla hyvä sijoituspaikka vakioille (luku 2.6). Monet vakiot liittyvät tiettyyn luokkaan yleisesti eivätkä ole ilmentymäkohtaisia. Esimerkiksi Stars-projektin StarCoords-luokan kumppanioliossa on määritelty vakiot MinValue ja MaxValue, joihin on tallennettu koordinaattien raja-arvot (-1.0 ja +1.0).
  2. Apufunktiot: Jos on tarvetta kyseiseen luokkaan liittyville apufunktioille, jotka eivät ole ilmentymäkohtaisia, niin kumppanioliota voi käyttää pakkausolion tapaan sijoituspaikkana näille funktiolle. Tällaiset apufunktiot voivat olla yksityisiä tai julkisia.
  3. Tehdasmetodit:

Tehdasmetodeista

Laaditaan Asiakas-kumppaniolioon vielä yksi metodi nimeltä luoUusi:

object Asiakas {

  private var montakoLuotu = 0

  def tulostaMaara() = {
    println("Tähän mennessä on luotu " + this.montakoLuotu + " asiakasoliota.")
  }

  def luoUusi(nimi: String) = new Asiakas(nimi)

}

Kutsulla Asiakas.luoUusi("Sasha") saadaan nyt siis aikaan täsmälleen sama kuin suoralla luontikäskyllä new Asiakas("Sasha").

Tällaista metodia sanotaan tehdasmetodiksi (factory method). Sen tehtävänä on ainoastaan tuottaa ja palauttaa uusi olio.

Huomaa, että laatimamme tehdasmetodi on nimenomaan Asiakas-kumppaniolion metodi eikä Asiakas-luokan, koska uuden asiakkaan luominenhan ei ole olemassa olevaan asiakasluokan ilmentymään liittyvä toiminto.

Muokataan tehdasmetodiamme vielä hieman: vaihdetaan sen nimeksi apply.

object Asiakas {

  private var montakoLuotu = 0

  def tulostaMaara() = {
    println("Tähän mennessä on luotu " + this.montakoLuotu + " asiakasoliota.")
  }

  def apply(nimi: String) = new Asiakas(nimi)

}

apply-metodin erikoisluonteen tuntien voimmekin nyt luoda uusia asiakasolioita näin: Asiakas("Sasha") ilman new-sanaa. Tämä käskyhän laventuu muotoon Asiakas.apply("Sasha").

Monet Scala-ohjelmien tehdasmetodit ovat nimenomaan apply-nimisiä, jolloin olioita voi luoda mahdollisimman yksinkertaisesti.

Tuttuja tehtaita

Scalalla ohjelmoidessa tehdasmetodien käyttämiseltä on vaikea välttyä, vaikka niitä ei itse laatisikaan. Olet itse asiassa jo itsekin käyttänyt niitä, ja on hyvä tiedostaa tämä.

Olet esimerkiksi luonut Vector-olioita käskyillä kuten Vector(4, 10, 2) ilman new-sanaa. Tuo käsky on lyhyempi versio käskystä Vector.apply(4, 10, 2), joka on Vector-luokan kumppaniolion apply-metodin kutsu. Kyseinen apply-metodi on tehdasmetodi, joka luo ja palauttaa vektoriolion. Vektorin luominen käskyllä new Vector(4, 10, 2) ei edes toimi, koska kyseiselle luokalle ei ole tuollaista luontitapaa määritelty.

Muitakin tehdasmetodeja olet jo tavannut:

  • Olet käyttänyt ilmaisuja kuten Some(newFave). Tuolloinkaan et käyttänyt new-sanaa vaan tehdasmetodia. new-sanan käyttö tässä tapauksessa olisi ollut sallittua mutta turhaa.
  • Olet luonut kuvia esimerkiksi circle- ja rectangle-funktioilla, jotka ovat pakkausolion o1-metodeita. Niitäkin voi sanoa Pic-olioita luoviksi tehdasmetodeiksi.
  • Samoin esimerkiksi Pic("face.png") kutsuu tehdasmetodia: se on lyhennysmerkintä Pic-luokan kumppaniolion apply-kutsulle.

Se, käytetäänkö tietyn luokan yhteydessä tehdasmetodia vai new-sanaa, riippuu siitä, miten kyseinen luokka on määritelty ja millaisia tehdasmetodeja mahdollisesti on tarjolla. Tällä kurssilla sinun ei tarvitse osata itse suunnitella, millaisissa tilanteissa kannattaisi määritellä tehdasmetodi; sikäli, kun tehdasmetodeita käytämme, ne esitellään kurssimateriaalissa kuten tähänkin asti.

Miksi tehdasmetodeita?

Voi herätä ajatus, että ihan kiva, mutta mikä näissä tehdasmetodeissa nyt on niin houkuttelevaa. Eikö tavallinen olioiden luominen new-sanalla riittäisi?

Asiakasluokkamme tapauksessa ehkä riittäisikin. Toki sitä, että asiakasolion saa nyt luotua ilman new-sanaa voi joku pitää riittävänä valopuolena.

Miksi tehdasmetodit sitten ovat esimerkiksi Scala API -kirjastossa niin yleisiä?

Onnetonta kyllä, syitä tehdasmetodien käytölle on vaikeaa käsitellä syvällisesti kurssilla opitun asian pohjalta, mutta perustavoitteen voimme todeta. Se on joustavuus. Vertaa:

  • Olion luominen new-sanalla, luokan nimellä ja konstruktoriparametreilla kietoo erottamattomasti yhteen kaksi asiaa:
    1. mitä parametritietoja olion luomiseksi annetaan, ja
    2. minkä nimenomaisen luokan ilmentymä syntyy.
  • Tehdasmetodilla nuo kaksi asiaa voi erottaa toisistaan. Niinpä voidaan:
    1. Laatia useita eri metodeita, jotka luovat ja palauttavat keskenään samantyyppisiä arvoja erilaisilla perusteilla. Esimerkiksi tehdasmetodit Pic, circle ja rectangle kaikki tuottavat Pic-olioita erilaisista parametreista.
    2. Laatia tehdasmetodi, joka ei aina palauta saman luokan ilmentymää vaan valitsee vaikkapa parametriensa perusteella, minkälainen olio luodaan ja palautetaan.

Tehdasmetodien mielekkyydestä lisää Ohjelmointistudio 2 -kurssilla keväällä.

Yhteenvetoa

  • Jos oliolla on apply-niminen metodi, se toimii ikään kuin "oletusmetodina": kutsu olio(parametrit) laventuu automaattisesti muotoon olio.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.
  • Tehdasmetodi on metodi, jonka tehtävä on luoda ja palauttaa uusi olio.
    • Niitä käytetään useiden valmiiden Scala-luokkien yhteydessä suoran new-käskyllä instantioinnin sijaan.
    • Scalassa tehdasmetodit ovat usein kumppaniolioiden apply-metodeita, jolloin olioita voi luoda LuokanNimi(parametrit) -muotoisilla käskyillä, esim. Vector(432, 32, 223).
  • Lukuun liittyviä termejä sanastosivulla: apply; kumppaniolio; tehdasmetodi.

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!

Kierrokset 1–13 ja niihin liittyvät tehtävät ja viikkokoosteet on laatinut Juha Sorva.

Kierrokset 14–20 on laatinut Otto Seppälä. Ne eivät ole julki syksyllä, mutta julkaistaan ennen kuin määräajat lähestyvät.

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

Tehtävien automaattisen arvioinnin ovat toteuttaneet: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä ja Aleksi Vartiainen.

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

Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista ovat suunnitelleet Juha Sorva ja Teemu Sirkiä. Niiden teknisen toteutuksen ovat tehneet Teemu Sirkiä ja Riku Autio käyttäen Teemun toteuttamia Jsvee- ja Kelmu-työkaluja.

Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset on laatinut Juha Sorva.

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

Opetustapa, jossa 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+ on luotu Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Pääkehittäjänä toimii tällä hetkellä Jaakko Kantojärvi, jonka lisäksi järjestelmää kehittävät useat tietotekniikan ja informaatioverkostojen opiskelijat.

Kurssin tämänhetkinen henkilökunta on kerrottu luvussa 1.1.

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

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