Helsingin yliopisto / tietojenkäsittelytieteen laitos / Ohjelmointitekniikka (Scala) / © Arto Wikla 2010

6 Funktionaalisia olioita ja vähän muutakin

Muutettu viimeksi 24.3.2010 / Sivu luotu 19.3.2010 / [oppikirjan esimerkit] / [Scala]

Sivun sisältöä:

Pääkonstruktori

Muuttumaton eli immutaabeli olio on kerran synnyttyään aina saman sisältöinen. Sillä (=sen kentillä) on siis aina sama arvo. Muutettavalla eli mutaabelilla oliolla on vastaavasti tila, joka voi ajassa muuttua.

Oppikirjan hengessä aletaan rakennella immutaabelia rationaalilukuluokkaa Rational. Välillä katsotaan muitakin esimerkkejä periaatteista ja tekniikoista.

Muuttumaton rationaaliluku voidaan määritellä lyhyesti

class Rational(os: Int, nim: Int)
Tunnukset os ja nim tarkoittavat luokkaparametreja. (Älä sekoita näitä Javan "luokkamuuttujiin"! Muista ettei Scalassa ole mitään "staticcia".)

Luokkaparametreista synytetään jokaiselle luotavalle oliolle omat salaiset vakiokentät. Luokkamäärittelyn otsake myös määrittelee pääkonstruktorin:

val x = new Rational(1,2)
println(x);                      // Main$Rational$1@b2002f
Aaltosulkeitakaan ei tarvita, ellei luokkamäärittelyltä muuta haluta kuin keino luoda oliota salaisin vakiokentin. Tyhjän { } saa toki kirjoittaa. Olion tulostustaitokin näkyy luokkaan Javan tapaan periytyvän.

Usein luokkaan kuitenkin halutaan muitakin kenttiä ja monenmoisia metodeita. Myös saatetaan haluta suorittaa joitakin toimenpiteitä aina, kun olio luodaan. Tällaiset toimenpiteet ohjelmoidaan sellaisinaan luokkamäärittelyn runkoon:

class Rational(os: Int, nim: Int) {
  println("Hip, hei, syntyi luku: "+ os +"/"+ nim)
}

val x = new Rational(1, 2)       // Hip, hei, syntyi luku: 1/2
val y = new Rational(764, 321);  // Hip, hei, syntyi luku: 764/321


Periaatteita/tekniikkaa:

Luokassa voidaan siis määritellä kenttiä:

Ainokaista käyttäen on helppo ohjelmoida "sarjanumeron generointi", mikä Javalla tehdään staticeilla:

class Rational(os: Int, nim: Int) {
  Rational.lkm += 1
  println("Hip, hei, syntyi " +  Rational.lkm +". luku: "+ os +"/"+ nim)
}
object Rational {
  private var lkm = 0
}

val x = new Rational(1, 2)       // Hip, hei, syntyi 1. luku: 1/2
val y = new Rational(764, 321);  // Hip, hei, syntyi 2. luku: 764/321

// Rational.lkm = -666  **** error: variable lkm cannot be accessed in object this
Laskuri saatiin private-määreellä turvaan ulkopuolisilta! Ja luokaan siis näkyy näkyvän myös oliokumppanin yksityiset kentät. Koska toisaalta oliokumppanin sisältä ei näe luokkakumppanin kenttiä, mieleen tulee elävästi Javan sääntö: "staticista ei näe ei-staticciin, mutta ei-staticista näkee staticciin".


Oliota luotaessa siis määritellään kenttiä ja metodeita ja suoritetaan luokkamäärittelyn mahdollisesti sisältämät lauseet (tms.).

Korvataan peritty toString:

class Rational(os: Int, nim: Int) {
  override def toString = os +"/"+ nim
}

var x = new Rational(1,2)
println(x);                      // 1/2
x =  new Rational(1234,5678)
println(x);                      // 1234/5678

Ehto, vaatimus ja väittämä

Ilmauksella require(väittämä) voidaan ohjelmoida väittämiä, joiden ohjelmoijan mielestä pitäisi olla aina tosia. Voidaan esimerkiksi ajatella, että luokan Rational dokumentaatiossa on sanottu, että käyttäjän on aina itse varmistettava, ettei luotavan olion nimittäjä ole nolla. Ja jos on, huonosti käyköön...

Ohjelmoidaan rationaaliluvun konstruoinnin alkuun ennakkoehtona (pre-condition) nimittäjän ei-nolluuden tarkistus:

class Rational(os: Int, nim: Int) {
  require(nim != 0)
  override def toString = os +"/"+ nim
}

val x = new Rational(666, 0)
println(x);
Ja huonostihan siinä käy:
java.lang.IllegalArgumentException: requirement failed
  ...
  ...

Kenttiä ja metodeja

Hienojahan nuo rationaalilukuoliot ovat, mutta pitäisihän niillä päästä laskemaankin!

Kuten edellä todettiin, määreettömät luokkaparametrit näkyvät vain oliolle itselleen; kyseessä on siis "olioprivaattius". Niinpä luokkaan Rational ei ole mahdollista ohjelmoida metodia, jossa käsiteltäisiin parametrina saadun Rational-olion luokkaparametrien tarkoittamia kenttiä. Ja niihinhän pitää päästä käsiksi, jotta pääsisi aritmetiikkaa harrastamaan.

Ratkaisu löytyy määrittelemällä val-kentät, joihin kopioidaan luokkaparametrien arvot. Ne voitaisiin tehdä toki privaateiksi a'la Java-tyyli, mutta miksei noita rationaaliluvun muuttumattomia osia voisi näyttää ulkomaailmallekin? Saadaan ikäänkuin ilmaiset "getterit".

Ohjelmoidaan rationaalilukujen yhteen- ja kertolasku:

class Rational(os: Int, nim: Int) {
  require(nim != 0)

  val osoittaja = os
  val nimittaja = nim

  def +(that: Rational): Rational =       //   (itseviittauksessa voisi Javan tapaan
   new Rational(                          //    käyttää ilmausta this) 
      osoittaja * that.nimittaja + that.osoittaja * nimittaja,
      nimittaja * that.nimittaja
   )

  def *(that: Rational): Rational =
    new Rational(osoittaja * that.osoittaja, nimittaja * that.nimittaja)

  override def toString = osoittaja +"/"+ nimittaja
}

val a = new Rational(2, 3)
val b = new Rational(4, 5)

val c = a + b
println(c)            // 22/15

println(a*a)          // 4/9

println(b osoittaja)  // 4   ** jos osoittaja ja nimittaja olisivat private,
                      //     ** tällainen viittaus ei olisi luvallinen

Lisäkonstruktorit

Ihmiselle kokonaisluvutkin voivat olla rationaalilukuja: 1/1, 2/1, 3/1, ... Mutta me tapaamme jättää tuon turhan ykkösellä jakamisen pois. Tyhmälle tietokoneelle (= luokka Rational) tuollaisetkin rationaaliluvut pitää kuitenkin antaa muodossa new Rational(3, 1).

Mikä neuvoksi? No tietenkin pääkonstruktoria kuormittava yksiparametrinen lisäkonstruktori:

class Rational(os: Int, nim: Int) {
  require(nim != 0)

  val osoittaja = os
  val nimittaja = nim

  def this(n: Int) = this(n, 1) // lisäkonstruktori kutsuu pääkonstruktoria

  def +(that: Rational): Rational =       //   (itseviittauksessa voisi Javan t$
   new Rational(                          //    käyttää ilmausta this)
      osoittaja * that.nimittaja + that.osoittaja * nimittaja,
      nimittaja * that.nimittaja
   )

  def *(that: Rational): Rational =
    new Rational(osoittaja * that.osoittaja, nimittaja * that.nimittaja)

  override def toString = osoittaja +"/"+ nimittaja
}

val a = new Rational(5, 6)
val kolmonen = new Rational(3)

println(a * kolmonen)  // 15/6
Lisäkonstruktorit voivat pääkonstruktorin sijasta kutsua myös toisia lisäkonstruktoreita. Mutta vain luokan kirjoitusasussa aiemmin määriteltyjä konstruktoreita on luvallista kutsua.

Huom: Myös lisäkonstruktoreissa voi olla suoritettava lohko. Mutta toisen konstruktorin kutsun on oltava lohkon ensimmäinen operaatio!

Huom: Tietokone (= luokka Rational) jää vieläkin niin tyhmäksi, ettei se osaa seuraavaa:

val a = new Rational(5, 6)
println(a * 2)
println(2 * a)  // tästä nyt puhumattakaan!
Tähän löytyy pian apu!

Yksityiset kentät ja metodit

Käytetään vielä tuota edellistä Rational-määrittelyä:
val a = new Rational(2, 3)
val b = new Rational(4, 5)

println(a + a * b + b)      // 450/225
Voidaan huomata sellainen myönteinen seikka, että laskutoimitukset sitovat oikein. Mutta kielteistäkin näkyy: Rational-luokka ei osaa sieventää rationaalilukuja. Opetetaan se sille ohjelmoimalla suurimman yhteisen tekijän (syt) etsintä ja sitten supistaminen sen avulla:
class Rational(os: Int, nim: Int) {
  require(nim != 0)

  private val sytinArvo = syt(os.abs, nim.abs)

  val osoittaja = os  / sytinArvo     // supistetaan aina, kun luodaan
  val nimittaja = nim / sytinArvo     // uusi olio! "normeerataan"

  def this(n: Int) = this(n, 1) // lisäkonstruktori kutsuu pääkonstruktoria

  def +(that: Rational): Rational =       //   (itseviittauksessa voisi Javan t$
   new Rational(                          //    käyttää ilmausta this)
      osoittaja * that.nimittaja + that.osoittaja * nimittaja,
      nimittaja * that.nimittaja
   )

  def *(that: Rational): Rational =
    new Rational(osoittaja * that.osoittaja, nimittaja * that.nimittaja)

  override def toString = osoittaja +"/"+ nimittaja

  private def syt(a: Int, b: Int): Int =    // Rekursiiviselle metodille
    if (b == 0) a else syt(b, a % b)        // on annettava tyyppi!
}


val a = new Rational(2, 3)
val b = new Rational(4, 5)

println(a + a * b + b)      // 2/1

Tunnuksista ja niiden tyylistä

Scalan tyylistä:

Metodien kuormittamisesta ja automaattisista tyyppimuunnoksista

Edellä todettiin eräs puute Rational-luokassa. Kokonaislukujen ja rationaalien sekoittaminen ei onnistu.

No, kuormitetaan:

class Rational(os: Int, nim: Int) {
  require(nim != 0)

  private val sytinArvo = syt(os.abs, nim.abs)

  val osoittaja = os  / sytinArvo     // supistetaan aina, kun luodaan
  val nimittaja = nim / sytinArvo     // uusi olio! "normeerataan"

  def this(n: Int) = this(n, 1) // lisäkonstruktori kutsuu pääkonstruktoria

  def +(that: Rational): Rational =       
   new Rational(                          
      osoittaja * that.nimittaja + that.osoittaja * nimittaja,
      nimittaja * that.nimittaja
   )

  def +(i: Int): Rational =
   new Rational(osoittaja + i * nimittaja, nimittaja)

  def *(that: Rational): Rational =
    new Rational(osoittaja * that.osoittaja, nimittaja * that.nimittaja)

  def *(i: Int): Rational =
    new Rational(osoittaja * i, nimittaja)

  override def toString = osoittaja +"/"+ nimittaja

  private def syt(a: Int, b: Int): Int =    // Rekursiiviselle metodille
    if (b == 0) a else syt(b, a % b)        // on annettava tyyppi!
}

val a = new Rational(2, 3)
val b = new Rational(4, 5)

println(a + 1)  // 5/3
println(b * 2)  // 8/5

Jo siis voi rationaaliin lisätä kokonaisluvun ja sen voi kertoa kokonaisluvulla. Mutta toisin päin ei vielä onnistu:

Yritys println(1 + a) johtaa ilmoitukseen:

error: overloaded method value + with alternatives (Double)Double (Float)Float (Long)Long (Int)Int (Char)Int (Short)Int (Byte)Int (java.lang.String)java.lang.String cannot be applied to (this.Rational)

println(1 + a)
           ^
one error found
!!!
Jotenkin olisi kivaa jos yhteenlasku ja kertolasku kuitenkin olisivat vaihdannaisia...

Kuitenkaan esim. ykköselle ei ole operaatioita 1.+(a), missä a olisi ikiomaa Rational-tyyppiä.

Ratkaisu saadaan implisiittisestä tyyppimuunnoksesta ("implicit conversion"), jolla Int-arvo saadaan muunnettua automaattisesti Rational-tyyppiseksi. Siihen näkyvyysalueeseen, jossa tuota edellä kiellettyä operaatiota suoritetaan, kirjoitetaan määrittely:

implicit def intToRational(x: Int) = new Rational(x)
Esimerkiksi juuri tuohon yllä nähtyyn ohjelmanpätkään:
val a = new Rational(2, 3)
val b = new Rational(4, 5)

println(a + 1)  // 5/3
println(b * 2)  // 8/5

implicit def intToRational(x: Int) = new Rational(x)

println(1 + a)  // 5/3
println(2 * b)  // 8/5
Johan alkoi Lyyti kirjoittaa...

Tuota "implisiittimääritelmää" ei voi kirjoittaa tietenkään Rational-luokan sisälle, koska silloin se ei näkyisi muualle. Miten ja mistä kääntäjä tuon määrittelyn osaa löytää, selviää, jos kurssilla ehditään kirjan lukuun 21...