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

12 Piirretyypit eli "traitit"

Muutettu viimeksi 30.3.2016 / Sivu luotu 14.4.2010 / [oppikirjan esimerkit] / [Scala]

Sivun sisältöä:

Terminologiasta

Scalan trait-otusta kutsutaan suomeksi piirretyypiksi ja "traitin miksaamista" piirretyypin liittämiseksi.

Perusteluja nimelle: Pelkkä "piirre" on merkitykseltään kovin laaja. Ei esimerkiksi olisi kovin selkeää puhua tyyliin: "Millaisia piirteitä tuolla uudella piirteelläsi on? Se taitaa olla kovin suurpiirteisesti laadittu. Voisitko luonnehtia lyhyesti piirteesi piirteet?"

Aluksi käytin termiä "piirreluokka", mutta koska piirretyypistä ei voi luoda ilmentymiä ja koska sitä nimenomaan käytetään muuttujien, parametrien yms. tyyppinä, "piirretyyppi" on mielestäni perustellumpi nimi. Ihan vastaavin perustein Javan interface-otusta voisi kutsua nimellä "rajapintatyyppi" eikä "rajapintaluokka" tai pelkkä "rajapinta".

Mitä, mitä?

Javan rajapintaluokka voi sisältää pelkästään abstrakteja metodeja ja vakiokenttiä. Sen käyttötarkoitus on olla yhteinen tyyppi kaikille olioille, joiden luokat toteuttavat rajapintaluokan luettelemat metodit. Kääntäjä suostuu kääntämään rajapintaluokan tyyppiselle muuttujalle (tms.) suoritettaviksi kaikkien tyypin sisältämien metodien kutsut tietämättä lainkaan, mitä konkreettisia metodeita suoritusaikana todellisuudessa tullaan kutsumaan. Ja sitten suoritusaikana (taas kerran) polymorfismi rulaa!

On syytä muistaa myös, että kun olio on rajapintaluokan tyyppisessä muuttujassa, oliolle ei voi soveltaa muita metodeita kuin rajapintaluokan luettelemia! Toinen muistamisen arvoinen juttu on, että Javan luokalla voi olla vain yksi yliluokka, mutta se voi toteuttaa useita rajapintaluokkia.

Scalan piirretyyppi on Javan rajapintaluokan kaltainen, mutta se voi abstraktien jäsenten lisäksi sisältää myös metodien toteutuksia, kenttiä ja yleensäkin (melkein) mitä vain, mitä tavallinenkin luokka. Konstruktoria piirretyyppi ei voi sisältää, koska piirretyypistä ei voi luoda ilmentymiä. Mutta oliokumppani sillä voi olla ja siten se halutessaan voi myös luoda piirretyypin tyyppisiä olioita, jos käytettävissä on sopivia toteutusluokkia... Hyviä esimerkkejä tästä ovat piirretyypit Map ja Set. Muistele kuvia luvussa 3:

Jos jonkin olion luokkaan on liitetty piirretyyppi, tuo olio on (mm.) kyseisen piirretyypin tyyppiä! Kuten Javassa myös Scalassa luokalla voi olla vain yksi yliluokka, mutta luokkaan voidaan liittää useita piirretyyppejä. Ja kuten Javan rapintaluokan ollessa muuttujan tyyppinä, myös Scalan piirretyyppi määrää muuttujalle käytettävissä olevat operaatiot.

Esimerkki

Aloitetaan johdattelevalla esimerkillä:

  trait Philosophical {
    def philosophize() {
      println("I consume memory, therefore I am!")
    }
  }
Tässä Philosophical määrittelee vain yhden ei-abstraktin metodin. Piirretyypistä ei voi luoda ilmentymää, mutta se voidaan liittää (mix in) vaikkapa sammakkoon:
  class Frog extends Philosophical {
    override def toString = "green"
    def aantele {println("kurr")}
  }

  val kermit = new Frog
  kermit.philosophize()  // I consume memory, therefore I am!

  val saku: Philosophical = kermit   // huom: tyyppi
  println(kermit==saku)              // huom: true

  saku.philosophize()    // I consume memory, therefore I am!

  kermit.aantele  // kurr

//saku.aantele  on KIELLETTY koska sakun tyyppi on Philosophical!
             // value aantele is not a member of this.Philosophical
Luokalla voi olla korkeintaan yksi yliluokka, mutta siihen voidaan lisäksi liittää haluttu määrä piirretyyppejä. Seuraavalla luokalla on nimetty yliluokka ja siihen liitetään yksi piirretyyppi:
  class Animal {/*...*/}

  class Frog extends Animal with Philosophical {
    override def toString = "green"
  }
Tai useampia:
  class Animal {/*...*/}
  trait HasLegs {/*...*/}

  class Frog extends Animal with Philosophical with HasLegs {
    override def toString = "green"
  }
Piirretyypistä perittyjä luokan jäseniä voi korvata samaan tapaan kuin yliluokasta perittyjä:
  class Animal {/*...*/}

  trait Philosophical {
    def philosophize() {
      println("I consume memory, therefore I am!")
    }
  }

  class Frog extends Animal with Philosophical {
    override def toString = "green"   // ylijuokasta peritty
    override def philosophize() {     // piirretyypistä peritty
      println("It ain't easy being "+ toString +"!")
    }
  }

  val kermit = new Frog
  kermit.philosophize()  // It ain't easy being green!

  val saku: Philosophical = kermit  // Silti tämäkin käy!
  saku.philosophize()    // It ain't easy being green!

//saku.aantele  on KIELLETTY koska sakun tyyppi on Philosophical,
             // VAIKKA sakun arvona olevalla kermitillä on aantele-metodi!
             // Muuttujan tyyppi siis määrää operaatiot!

Ohuet ja rikkaat rajapinnat

Luokka tai piirretyyppi (tai melkeinpä mikä vain kirjastollinen operaatioita) voi olla ohut (thin) tai rikas (rich). Tämä tarkoittaa yksinkertaisesti vain sitä, tarjotaanko paljon vai vähän operaatioita. Ja ilmiselvästi on kyse siitä, kumpi tekee paljon töitä ja kumpi pääsee vähämmällä, luokan vai sovelluksen ohjelmoija.

Piirretyypin avulla on mahdollista laatia yleiskäyttöisiä välineitä, joihin on ohjelmoitu rikas rajapinta, jonka saa käyttöönsä, kunhan vain toteuttaa muutamia perittyjä, abstrakteja yksinkertaisia operaatioita. Kyse on itse asiassa ns. "koukkumetodien" toteuttamisesta. Tekniikka on käytössä myös mm. Javan joidenkin kokoelmaluokkien abstrakteissa "adapteriluokissa".

Esimerkki Javan ohuesta rajapinnasta "skalannettuna":

  trait CharSequence {
    def charAt(index: Int): Char
    def length: Int
    def subSequence(start: Int, end: Int): CharSequence
    def toString(): String
  }
Vain muutama metodi tarjolla, vaikka melkein kaikki, mikä on String-olioille mahdollista, voisi olla mahdollista myös CharSequence-tyyppiselle oliolle. Vaan ei ole...

Mutta rajapintaan saadaankin Scalan piirretyypissä ohjelmoitua myös ei-abstrakteja metodeita: Tehdään ohut abstrakteista metodeista koostuva rajapinta ja halutun rikas kokoelma ei-abstrakteja metodeita, jotka kutsuvat – käyttävät hyväkseen! – niitä muutamaa abstraktia metodia.

Ja kunhan vain luokka, johon tuo piirretyyppi liitetään, toteuttaa muutamat harvat abstraktit metodit, se saa "ilmaiseksi" käyttöönsä ne kaikki muut! Seuraava esimerkki valaissee asiaa.

Nelikulmioesimerkki

Otetaan esimerkki grafiikan maailmasta.Tason koordinaatit esitettäköön Point-olioina:
  class Point(val x: Int, val y: Int)
Nelikulmio voitaisiin ohjelmoida vaikka seuraavaan tapaan kirjoittamalla suuri/tarpeellinen määrä konkreettisia metodeja:
  class Rectangle(val topLeft: Point, val bottomRight: Point) {
    def left = topLeft.x
    def right = bottomRight.x
    def width = right - left
    // and many more geometric methods...
  }
Samaan tapaan ohjelmoitaisiin jokin abstrakti vitkutin, jossa on kenttiä ja myös ei-abstrakteja metodeita, joista ainakin osa on ihan samanlaisia kuin edellä:
  abstract class Component {
    def topLeft: Point
    def bottomRight: Point

    def left = topLeft.x
    def right = bottomRight.x
    def width = right - left
    // and many more geometric methods...
  }
Kun tähän tyyliin joutuisi toistelemaan samoja koodipätkiä ties mihin nelikulmaisia graafisia hahmoja esittäviin luokkiin, alkaisi mietityttää, eikö jotenkin voisi päästä helpommalla.

No voi toki! (Osta hyvä Scala! ;-)

Laaditaan "rikastuspiirretyyppi" (enrichment trait). Siinä on vain kaksi abstraktia metodia, joiden avulla sitten toteutaan ei-abstrakteina metodeina rikas rajapinta:

  trait Rectangular {
    def topLeft: Point       // Vain nurkat ovat abstrakteja!
    def bottomRight: Point   // Nähin "koukkuihin" ripustetaan sitten
                             // ne konkreettiset toteutukset.
    def left = topLeft.x
    def right = bottomRight.x       // Kaikki muu ohjelmoidaan valmiiksi kutsumalla
    def width = right - left        // tarvittaessa noita abstrakteja metodeita!

    // and many more geometric methods...
  }
Jos nyt halutaan tuo edellä nähty abstrakti luokka se saadaan käytännössä ihan ilmaiseksi:
  abstract class Component extends Rectangular {
    // other methods...
  }
Eikä Rectanglen toteuttamisessa paljon vaivaa tarvitse nähdä. Riittää toteuttaa nuo kaksi Point-nurkkaa:
  class Rectangle(val topLeft: Point, val bottomRight: Point) extends Rectangular {
                  // (Tämä on esimerkki myös siitä, miten peritty funktio korvataan kentällä!)
    // other methods...
  }

  val x = new  Rectangle(new Point(1,2),new Point(5,6))
  println(x.left)  // 1
  println(x.right) // 5
  println(x.width) // 4

Ordered-piirretyyppi

Olioarvojen järjestysrelaatio, suuremmuus, pienemmyys, samuus, on klassinen esimerkki tilanteesta, jossa rajapinnan rikastaminen on näppärä tekniikka.

Vaikkapa Rational-olioita olisi luontevaa voida vertailla operaatioin "<", ">", "<="ja ">=". Toki noiden ohjelmointi onnistuu. Ja kun yhden saa tehtyä, sen avulla muut on vaivatonta toteuttaa:

  class Rational(n: Int, d: Int) {
    // ...
    def < (that: Rational) = 
      this.numer * that.denom > that.numer * this.denom
    def > (that: Rational) = that < this

    def <= (that: Rational) = (this < that) || (this == that)
    def >= (that: Rational) = (this > that) || (this == that)
  }
Samaan tyyliin voisi toteuttaa ties mille luokille nuo operaatiot. Yhä uudelleen ja uudelleen vaikka logiikka pysyy samana.

Siksi Scalaan onkin laadittu valmis piirretyyppi Ordered[OmaTyyppi] erisuuruusvertailujen liittämiseen omiin luokkiin. Tyypin ainoa abstrakti metodi on compare(that: OmaTyyppi): Int. Kunhan vain toteuttaa sen omalle luokalle, saa ilmaiseksi nuo muut vertailuoperaatiot. Metodin pitää palauttaa negatiivinen arvo, jos this-olio on pienempi kuin that-olio, nolla, jos ne ovat samat ja positiivinen, jos this-olio on suurempi kuin that-olio.

Huom: Vertaa tätä Javan Comparable-rajapintaluokkaan, joka ei toteuta yhtään mitään, antaa vain Comparable-tyypin oman luokan ilmentymille. Kovin on köyhää... ;-)

Huom: Ordered-piirretyyppi vaatii siis vertailtavan olion tyypin tyyppiparametrina.

Ordered-piirretyyppiä käyttäen on helppo liittää vertailtavuus omaan luokkaan. Toteutetaan vain ja ainoastaan se yksi vaadittu metodi:

  class Rational(n: Int, d: Int) extends Ordered[Rational] {
    // ...
    def compare(that: Rational) =
      (this.numer * that.denom) - (that.numer * this.denom)
  }
[ Valmiin Ordered-piirretyypinn toteutus ei ole kovinkaan monimutkainen:
  trait Ordered[T] {
    def compare(that: T): Int  // ainoa abstrakti metodi!
    def <(that: T): Boolean = (this compare that) < 0
    def >(that: T): Boolean = (this compare that) > 0
    def <=(that: T): Boolean = (this compare that) <= 0
    def >=(that: T): Boolean = (this compare that) >= 0
  }
Huomaa tässä myös tyyppiparametrin käyttö piirretyypin määrittelyssä! Asiaa esitellään lyhyesti materiaalin luvussa 19, laajasti kirjan vastaavassa luvussa. ]

Huom: Ordered ei määrittele equals-metodia ja siten ei myöskään ==- ja !=-operaatioita! Ne saa käyttöönsä korvaamalla omassa luokassa Any-luokasta peritty equals-metodi. (Syyksi mainitaan Java-toteutuksen "type erasure"; geneerisiä tyyppejä ei tiedetä ajoaikana.)

Pinoutuvat muunnokset

Piirretyyppejä voidaan käyttää myös luokan metodien muuntamiseen ja tällaisia muunnoksia voidaan pinota suoritettavaksi toinen toisensa jälkeen.

Menettely perustuu siihen, että piirretyypissäkin saa käyttää super-viittausta, joka kuitenkin sidotaan milloin mihinkin yliluokkaan tai piirretyyppiin "dynaamisesti", siis toisin kuin tavallisen luokan tapauksessa, jossa yliluokka on yksikäsitteinen. Piirretyypin super saa merkityksensä, kun piirretyyppi ohjelman käännösaikana liitetään johonkin luokkaan tai piirretyyppiin. (Varoitus: yleensä ohjelmointikielten yhteydessä "dynaaminen" tarkoittaa suoritusaikaista!)

Ohjelmoidaan kokonaislukujono ja sille joitakin muunnoksia. Ensin abstrakti jono:

  abstract class IntQueue {
    def get(): Int
    def put(x: Int)
  }
Jonoon siis viedään lukuja ja sieltä saadaan ulos yksitellen aina kaikkein vanhin luku, jonossa pisimpään jonottanut. Jono voidaan toteuttaa esimerkiksi ArrayBuffer-oliona:
  import scala.collection.mutable.ArrayBuffer

  class BasicIntQueue extends IntQueue {
    private val buf = new ArrayBuffer[Int]
    def get() = buf.remove(0)
    def put(x: Int) { buf += x }
  }

  val jono = new BasicIntQueue

  jono.put(10)
  jono.put(20)
  jono.put(30)

  println(jono.get()) // 10
  println(jono.get()) // 20
  println(jono.get()) // 30
Ohjelmoidaan sitten jonoa muuntava piirretyyppi Doubling, joka nimensä mukaisesti tuplaa jonoon vietävän luvun:
  trait Doubling extends IntQueue {
    abstract override def put(x: Int) { super.put(2 * x) }
  }
Abstrakti IntQueue on siis Doubling-piirretyypin yliluokka eli samalla sen ylityyppi! Tämä johtaa siihen, että Doubling voidaan liittää vain johonkin IntQuen aliluokkaan!

Viittaus super on myös kiinnostava! Se tulee luokkaan liittämisen jälkeen tarkoittamaan juuri nyt kyseessä olevan luokan put-metodia! Ja tällainenhan varmasti on olemassa, koska se on abstraktina vaatimuksena luokassa IntQue! Tällaisesta käännösaikaisesta "dynaamisuudesta" yllä puhuttiin.

Kun piirretyypissä (sallittu vain siellä!) määritellään korvaaminen abstraktiksi, tyyppi on luvallista liittää vain sellaiseen luokkaan, jonka ilmentymä antaa konkreettisen toteutuksen tuolle metodille. Muista: konkreettisen aliluokan ilmentymä on myös abstraktin yliluokan ilmentymä!

Kokeillaan:

// IntQueuen ja  BasicIntQueuen määrittelyt

  trait Doubling extends IntQueue {
    abstract override def put(x: Int) { super.put(2 * x) }
  }

  class MyQueue extends BasicIntQueue with Doubling // !!

  val jono = new  MyQueue

  jono.put(10)
  jono.put(20)
  jono.put(30)

  println(jono.get()) // 20
  println(jono.get()) // 40
  println(jono.get()) // 60

Ohjelmoidaan sitten pari muuta muunnosta:
// IntQueuen ja  BasicIntQueuen määrittelyt

  trait Incrementing extends IntQueue {
    abstract override def put(x: Int) { super.put(x + 1) }
  }
  trait Filtering extends IntQueue {
    abstract override def put(x: Int) {
      if (x >= 0) super.put(x)
    }
  }

  val jono = new BasicIntQueue with Incrementing with Filtering // !!
  jono.put(10)
  jono.put(20)
  jono.put(-50)
  jono.put(30)

  println(jono.get()) // 11
  println(jono.get()) // 21
  println(jono.get()) // 31
Muuntavien piirretyyppien liittämisjärjestys määrää muunnosten suoritusjärjestyksen. Yksinkertaistettuna ne suoritetaan "oikealta vasemmalle". Asia on todellisuudessa monimutkaisempi, koska luokkien ja piirretyyppien välillä voi olla verkkomaisia yli-ali-suhteita. Ns. linearisointia esitellään alempana.

Tehdään versio "aputulostuksin":

  abstract class IntQueue {
    def get(): Int
    def put(x: Int)
  }

  import scala.collection.mutable.ArrayBuffer

  class BasicIntQueue extends IntQueue {
    private val buf = new ArrayBuffer[Int]
    def get() = buf.remove(0)
    def put(x: Int) {
      print("vien ")
      buf += x
    }
  }
  trait Doubling extends IntQueue {
    abstract override def put(x: Int) {
      print("tuplaan ")
      super.put(2 * x)
    }
  }
  trait Incrementing extends IntQueue {
    abstract override def put(x: Int) {
      print("kasvatan ")
      super.put(x + 1)
    }
  }
  trait Filtering extends IntQueue {
    abstract override def put(x: Int) {
      print("suodatan ")
      if (x >= 0) super.put(x)
    }
  }

  val jono1 = new BasicIntQueue with Doubling with Incrementing with Filtering
  jono1.put(10)
  println(jono1.get()) // suodatan kasvatan tuplaan vien 22

  val jono2 = new BasicIntQueue with Filtering with Incrementing with Doubling

  jono2.put(10)
  println(jono2.get()) // tuplaan kasvatan suodatan vien 21

Moniperinnästä ja timanteista

Vaikka piirretyyppien perimistä voidaan pitää "rajoitettuna moniperintänä", sitä voidaan pitää toisaalta myös tavallista moniperintää voimakkaampana, koska liittettyjen piirretyyppien korvaavia metodeita voidaan suorittaa yllä nähtyyn tapaan pinoutuvina muunnoksina.

Jos kielessä olisi "tavallinen moniperintä", seuraava koodipätkä toimisi toisin:

  val q = new BasicIntQueue with Incrementing with Doubling
  q.put(42)
Tässä jouduttaisiin valitsemaan jompi kumpi tulkinta: joko kasvattaminen tai tuplaaminen. Kielessä olisi varmaan jokin julkilausuttu sääntö valintaan. Ja jos oletuksesta halutaan poiketa, se olisi ilmaistava vaikkapa tyyliin:
  trait MyQueue extends BasicIntQueue with Incrementing with Doubling {

    def put(x: Int) {
      Incrementing.super.put(x) // Tämä ei ole Scalaa!
      Doubling.super.put(x)
    }
  }
Pitäisi siis itse määrätä mitä ei-yksikäsitteisiä metodeita kutsutaan ja missä järjestyksessä. Vaan Scalassapa on taas automatisoitu asioita....

Klassinen moniperinnän "timanttiongelma" esitetään klassisesti luokkien Henkilö, Opettaja, Opiskelija ja OpettavaOpiskelija suhteina seuraavaan tapaan:

Tehdään aluksi versio, jossa timantti syntyy luokasta ja traitista:

// Timantti: aliluokka ja "traitti"
class Henkilo {
  val nimi="Henkilö"
  def syo {println("Henkilö syö")}
}
class Opettaja extends Henkilo {
  override def syo {println("Opettaja syö")}
}
trait Opiskelija extends Henkilo {  // vaatii extends Henkilö, ks. selitys alla
  override def syo {println("Opiskelija syö")}
}

class OpettavaOpiskelija extends Opettaja with Opiskelija

val x = new OpettavaOpiskelija

println(x.nimi) // Henkilö
x.syo           // Opiskelija syö
Nimi periytyy vain kerran; ei siis ole mahdollista, että opettajana henkilön nimi olisi "Matti Lahnanen" ja opiskelijana "Masa". Ja kuten näkyy viimeisenä valittu syömisen tapa valitaan.

Ehkä olisi luontevampaa ohjelmoida symmetrisesti sekä Opettaja että Opiskelija piirretyyppinä:

// Timantti: molemmat traitteja
class Henkilo {
  val nimi="Henkilö"
  def syo {println("Henkilö syö")}

}
trait Opettaja extends Henkilo {
  override def syo {println("Opettaja syö")}
}
trait Opiskelija extends Henkilo {
  override def syo {println("Opiskelija syö")}

}

class OpettavaOpiskelija extends Opettaja with Opiskelija
val x = new OpettavaOpiskelija
println(x.nimi) // Henkilö
x.syo           // Opiskelija syö

class OpiskelevaOpettaja extends Opiskelija with Opettaja
val y = new OpiskelevaOpettaja
println(y.nimi) // Henkilö
y.syo           // Opettaja syö

Myös kenttiä voidaan periä useampaa kautta:

// Timantti: korvataan kenttiä
class Henkilö {
  val nimi="Henkilö"
  def syo {println("Henkilö syö")}
}
trait Opettaja extends Henkilö {
  override val nimi="Opettaja"
  override def syo {println("Opettaja syö")}
}
trait Opiskelija extends Henkilö {
  override val nimi="Opiskelija"
  override def syo {println("Opiskelija syö")}
}
class OpettavaOpiskelija extends Opettaja with Opiskelija
val x = new OpettavaOpiskelija
println(x.nimi)  // Opiskelija
x.syo            // Opiskelija syö

class OpiskelevaOpettaja extends Opiskelija with Opettaja
val y = new OpiskelevaOpettaja
println(y.nimi)  // Opettaja
y.syo            // Opettaja syö

Siistiä: viimeisin miksaus siis voittaa! Voittajan valinta on itse asiassa hieman hieman mutkikkaanpaa, kun luokkaan liitetään useita piirretyyppejä, joilla on itsellään yliluokkia tai "ylipiirretyyppejä"!


(Linearisointi syvällisemmin kuin yllä nähtiin ei kuulu koealueeseen! On lähinnä Ohjelmointikielten periaatteet -kurssin asiaa. Siellä timantista ja linearisoinnista puhutaan hieman enemmän.)

Liitettyjen piirretyyppien kenttien ja metodien valintajärjestys on täsmällisesti (ja luontevasti??) määritelty ns. periytymishierarkian linearisoinnilla. Se määrää siis myös ja aivan erityisesti sen, mihin super-viittaukset viittaavat.

class Animal
trait Furry extends Animal
trait HasLegs extends Animal
trait FourLegged extends HasLegs
class Cat extends Animal with Furry with FourLegged
Kissan sukupuu
Type Linearization:
Animal 		Animal, AnyRef, Any
Furry 		Furry, Animal, AnyRef, Any
FourLegged 	FourLegged, HasLegs, Animal, AnyRef, Any
HasLegs 	HasLegs, Animal, AnyRef, Any
Cat 		Cat, FourLegged, HasLegs, Furry, Animal, AnyRef, Any
(Koealueen ulkopuolinen osuus päättyy)

Lähes luokatonta ohjelmointia

Piirretyypeillä voi rakennella luokkia ja olioita miltei tyhjästä, koska piirretyypit voivat sisältää kenttiä käsittelymetodeineen:
trait Nopeus {
  private var nopeus = 0
  def setNopeus(n: Int) {nopeus = n}
  def getNopeus = nopeus
}

trait Moottori {
  private var hevosia = 0
  def setHevosia(h: Int) {hevosia = h}
  def getHevosia = hevosia
}

trait Merkki {
  private var merkki = "hyrysysy"
  def setMerkki(m: String) {merkki = m}
  def getMerkki = merkki
}

trait Automaisuus extends Nopeus with Moottori with Merkki {
  override def toString =
    "("+ getMerkki +": " + getHevosia + " hp, " + getNopeus + " km/h)"
}

class Auto extends Automaisuus
  // Saisi toki sanoa myös class Auto extends AnyRef with Automaisuus

val biili = new Auto

biili.setNopeus(120)
biili.setHevosia(200)
biili.setMerkki("Maserati")

println(biili.getNopeus)   // 120
println(biili.getHevosia)  // 200
println(biili.getMerkki)   // Maserati
println(biili)             // (Maserati: 200 hp, 120 km/h)
Helppoa ja hauskaa?

Eläinten hoitoa

Tarkastellaan vielä ikivanhan Ohjelmoinnin jatkokurssin (ent. Java-ohjelmointi) rajapintaluokan ja abstraktin luokan eläimellisiä esimerkkejä Scalalle sovellettuina. Samalla kerrataan jo opittua.
// --- Eläinten yhteinen yliluokka:

abstract class Elain (nimi:String) {
  def aantele: Unit              // abstrakti metodi
  override def toString = nimi  // korvattaessa "override" pakollinen!
}

// --- Aliluokan määrittely, huomaa parametrivälitys:

class Kissa(nimi:String, naukumistiheys:Int) extends Elain(nimi)  {
  override def aantele {println("Miau")}
  override def toString = super.toString + "-" + naukumistiheys
}
val kissa = new Kissa("Missu", 7)
println(kissa)   // Missu-7
kissa.aantele    // Miau

// --- Pari imettäväisiin kuuluvaa eläinlajia:

class Hevonen(nimi:String) extends Elain(nimi)  {
  override def aantele {println("Ihahaa")}
}
val hevonen = new Hevonen("Polle")
println(hevonen)   // Polle
hevonen.aantele    // Ihahaa

class Nauta(nimi:String) extends Elain(nimi)  {
  override def aantele {println("Ammuu")}
}
val nauta = new Nauta("Julle")
println(nauta)   // Julle
nauta.aantele    // Ammuu

// --- Kokeillaan polymorfismia:

var x:Elain = new Kissa("Töpö", 2) // Huom: muuttujan tyyppi on Elain
println(x)   // Töpö-2
x.aantele    // Miau
x = new Hevonen("Valma")
println(x)   // Valma
x.aantele    // Ihahaa
x = new Nauta("Muurikki")
println(x)   // Muurikki
x.aantele    // Ammuu

// -- Ja sitten tehdään piirretyyppi lypsäville eläimille:

trait Lypsava {
  def lypsa = {0.0}  // tässä oletustoteutus; myös abstrakti metodi "def lypsa:Double" olisi mahdollinen
}

// Ja pari lypsävää, joihin tuo "traitti mixataan":

class Lehma(nimi:String) extends Nauta(nimi) with Lypsava {
  override def lypsa = 3.14 * nimi.length  // maidon määrä riippuu nimen pituudesta...
}
val mansikki = new Lehma("Mansikki")
println(mansikki)   // Mansikki
mansikki.aantele    // Ammuu
println(mansikki.lypsa)   // 25.12

class Tamma(nimi:String) extends Hevonen(nimi) with Lypsava {
  override def lypsa = 1.23 * nimi.length  // vähemmän maitoa kuin lehmältä
}
val tammukka = new Tamma("Tammukka")
println(tammukka)   // Tammukka
tammukka.aantele    // Ihahaa
println(tammukka.lypsa)   // 9.84

// Lopuksi tehdään juustoa mistä tahansa "traitin saaneesta":

def juustoa(tuotantoelain:Lypsava):Double = 42 * tuotantoelain.lypsa

println(juustoa(mansikki))   // 1055.04
println(juustoa(tammukka))   // 413.28

Piirretyyppi voi sisältää sekä toteutettuja metodeita (jotka peritään) sekä abstrakteja metodeita (jotka on pakko toteuttaa, ellei ole ohjelmoimassa abstraktia luokkaa). Peritty toteutettu metodikin toki voidaan korvata:

// --- Eläinten yhteinen yliluokka:

abstract class Elain (nimi:String) {
  def aantele: Unit             // abstrakti metodi
  override def toString = nimi  // korvattaessa "override" pakollinen
}
// --- Pari imettäväisiin kuuluvaa eläinlajia:
class Hevonen(nimi:String) extends Elain(nimi)  {
  override def aantele {println("Ihahaa")}
}
class Nauta(nimi:String) extends Elain(nimi)  {
  override def aantele {println("Ammuu")}
}

// -- Ja sitten tehdään hienompi "traitti" lypsäville eläimille:

trait Lypsava {
  def lypsa(n:String) = 3.14 * n.length // ei-abstrakti metodi
  def potkaise:Unit  // abstrakti metodi eli Javan interfacen kaltainen "vaatimus"
}

// Ja pari Lypsävää, joihin tuo "traitti" "mixataan in":

class Lehma(nimi:String) extends Nauta(nimi) with Lypsava {
   // lypsa(n:String) peritään traitista, so. oletustoteutus
   def potkaise {println("Lehmä potkaisee")}

}
val mansikki = new Lehma("Mansikki")
println(mansikki)   // Mansikki
mansikki.aantele    // Ammuu
println(mansikki.lypsa(mansikki.toString))   // 25.12
mansikki.potkaise   // Lehmä potkaisee

class Tamma(nimi:String) extends Hevonen(nimi) with Lypsava {
  override def lypsa(n:String) = 1.23 * n.length  // korvataan peritty omalla
  def potkaise {println("Tamma potkaisee")}
}
val tammukka = new Tamma("Tammukka")
println(tammukka)   // Tammukka
tammukka.aantele    // Ihahaa
println(tammukka.lypsa(tammukka.toString))   // 9.84
tammukka.potkaise   // Tamma potkaisee

// Lopuksi tehdään juustoa:

def juustoa(tuotantoelain: Lypsava):Double =
             42 * tuotantoelain.lypsa(tuotantoelain.toString)

println(juustoa(mansikki))   // 1055.04
println(juustoa(tammukka))   // 413.28