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? Tai ehkä olio onkin kuin pakkaus? Pysyykö pääni kasassa?

Mitä käsitellään? apply-metodit. Luokkien kumppanioliot. Muutama vapaaehtoinen lisäteema.

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

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

Pistearvo: B5.

Oheismoduulit: Ei ole.

../_images/person03.png

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

  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"?

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 applyn, 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.

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

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

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

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:

  1. 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 vakiot MinValue ja MaxValue, joihin on tallennettu koordinaattien raja-arvot (-1.0 ja +1.0).

  2. 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 kolmenSummaa 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 importaten.

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

importin 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ä importaaminen

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ä importaaminen 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 defin 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": 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.

    • 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, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, 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 ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.

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

Oppimisalusta A+ luotiin alun perin Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Nykyään tätä avoimen lähdekoodin projektia kehittää Tietotekniikan laitoksen opetusteknologiatiimi ja tarjoaa palveluna laitoksen IT-tuki. Pääkehittäjänä on nyt Markku Riekkinen, jonka lisäksi A+:aa ovat kehittäneet kymmenet Aallon opiskelijat ja muut.

A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen 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...