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

12 Piirreluokat eli "traitit"

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

Sivun sisältöä:

Terminologiasta

Päätin kutsua Scalan trait-otusta nimellä piirreluokka ja "traitin miksaamista" piirreluokan liittämiseksi. Perusteluja: 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?" Ihan vastaavin perustein kutsun Javan interface-otusta nimellä "rajapintaluokka", en pelkästään "rajapinta".

Kahvihuoneessa tosin syntyi keskustelua siitä, että "piirre" on alallamme varattu sana: "Ei se ole virhe, se on piirre". Ehdoteltiin "lisäosaa" (engl. plugin) ehdoteltiin. Hmm: "lisäosaluokka"?

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 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 useampiakin rajapintaluokkia.

Scalan piirreluokka on kuin Javan rajapintaluokka, 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 piirreluokka ei voi sisältää, koska piirreluokasta ei voi luoda ilmentymiä. Mutta oliokumppani sillä voi olla ja sehän sitten halutessaan voi myös luoda piirreluokan tyyppisiä olioita, jos käytettävissä on sopivia toteutusluokkia...

Jos jonkin olion luokkaan on liitetty piirreluokka, tuo olio on mm. kyseisen piirreluokan tyyppiä! Myös Scalassa luokalla voi olla vain yksi yliluokka, mutta siihen voidaan liittää useita piirreluokkia. Ja kuten Javan rapintaluokan ollessa muuttujan tyyppi, myös Scalan piirreluokka määrää muuttujalle käytettävissä olevat operaatiot.

Aloitetaan johdattelevalla esimerkillä:

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

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

  val saku: Philosophical = kermit   // Huom: tyyppi!
  saku.philosophize()    // I consume memory, therefore I am!

Luokalla voi olla korkeintaan yksi yliluokka, mutta siihen voidaan lisäksi liittää tarvittu määrä piirreluokkia. Tässä luokalla on nimetty yliluokka ja siihen liitetään yksi piirreluokka:
  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"
  }
Piirreluokasta perittyjä luokan jäseniä voi korvata samaan tapaan kuin yliluokasta perittyjä:
  class Animal {/*...*/}

  class Frog extends Animal with Philosophical {
    override def toString = "green"   // ylijuokasta peritty
    override def philosophize() {     // piirrluokasta 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!

Ohuet ja rikkaat rajapinnat

Luokka tai piirreluokka (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.

Piirreluokan 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 "koukkumetodien" toteuttamisesta. Tekniikka on käytössä myös Javan joidenkin kokelmaluokkien 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 rajapintaanpa saadaankin Scalan piirreluokassa 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.

Ja siis luokka, johon tuo piirreluokka liitetään, toteuttaa ne muutamat harvat abstraktit metodit ja saavat "ilmaiseksi" käyttöönsä ne kaikki muut!

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 voisi toki! (Osta hyvä Scala! ;-)

Laaditaan "rikastuspiirreluokka" (enrichment trait). Siinä on vain kaksi hyvin yksinkertaista 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 saadaankin sitten valmiina!
    def width = right - left
    // 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 {
    // 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-piirreluokka

Olioarvojen järjestysrelaatio, suuremmuus, pienemmyys, samuus, on klassinen esimerkki tilanteesta, jossa rajapinnan rikastaminen on mukava 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.

Niinpä Scalassa onkin valmis piirreluokka Ordered[OmaTyyppi] erisuuruusvertailujen liittämiseen omiin luokkiin. Ainoa toteutettava metodi on compare(that: OmaTyyppi): Int. Metodi palauttaa negatiivisen, jos this-olio on pienempi kuin that-olio, nollan, jos ne ovat samat ja positiivisen, jos this-olio on suurempi kuin that-olio.

Huom: Vertaa tätä Javan Comparable-rajapintaluokkaan, joka ei toteuta yhtään mitään, antaa vain luvan itse käyttää metodia compareTo! Köyhää... ;-)

Huom: Ordered-piirreluokka vaatii vertailtavan otuksen tyypin tyyppiparametrina. Näihin toivottavasti ehditään kurssin aikana. Muistele toistaiseksi vaikkapa Javan geneerisiä luokkia!

Ordered-piirreluokkaa 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)
  }

[Toteutus ei ole kovin kummoinen:
  trait Ordered[T] {
    def compare(that: T): Int

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

Pinoutuvat muunnokset

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

Menettely perustuu siihen, että piirreluokassakin saa käyttää super-viittausta, joka kuitenkin sidotaan milloin mihinkin yliluokkaan, "dynaamisesti", toisin kuin tavallisen luokan tapauksessa, jossa yliluokka on yksikäsitteinen. Piirreluokan super saa merktyksensä, kun piirreluokka liitetään johonkin luokkaan. (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, jonossa pisimpään jonottanut. Jono voidaan toteuttaa esimerkiksi vaikkapa 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 piirreluokka Doubling, joka nimensä mukaisesti tuplaa jonoon vietävän luvun:
  trait Doubling extends IntQueue {
    abstract override def put(x: Int) { super.put(2 * x) }
  }
Doubling siis on abstraktin IntQueue luokan aliluokka! Tämä tarkoittaa sitä, 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 luokassa IntQue! Tällaisesta "dynaamisuudesta" yllä puhuttiin.

Kun piirreluokassa (sallittu vain siellä!) määritellään korvaaminen abstraktiksi, luokka on luvallista liittää vain sellaiseen luokkaan, joka antaa konkreettisen toteutuksen tuolle metodille.

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 piirreluokkien liittämisjärjestys määrää muunnosten suoritusjärjestyksen. Yksinkertaistettuna ne suoritetaan "oikealta vasemmalle". Asia on todellisuudessa monimutkaisempi, koska luokkien välillä voi olla verkkomaisia yli-ali-suhteita. Ns. linearisointi 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)
    }
  }

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

  println("with Filtering with Incrementing with Doubling")
  val jono2 = new BasicIntQueue
                 with Filtering with Incrementing with Doubling
  jono2.put(10)
  println(jono2.get())
  /* tulostaa:
  with Filtering with Incrementing with Doubling
  tuplaan kasvatan suodatan vien 21
  */

Moniperinnästä ja timanteista

Vaikka piirreluokkien perimistä voidaan pitää "rajoitettuna moniperintänä", se on rajoituksien lisäksi myös sikäli voimakkaampaa, että liittettyjen piirreluokkien korvaavia metodeita voidaan suorittaa yllä nähtyyn tapaan useampia: "pinoutuvia muunnoksia".

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 julki lausuttu 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ä kutsurakenne. Scalassa on taas automatisoitu aisioita....

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

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

// Timantteja: aliluokka ja "traitti"

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

class OpettavaOpiskelija extends Opettaja with Opiskelija {}

val x = new OpettavaOpiskelija

println(x.nimi) // Henkilö
x.syö           // Opiskelija syö
Nimi näkyy periytyvän 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ä oliis luontevampaa ohjelmoida symmetrisesti sekä Opettaja että Opiskelija traittina:

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

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

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

class OpiskelevaOpettaja extends Opiskelija with Opettaja {}
val y = new OpiskelevaOpettaja
println(y.nimi) // Henkilö
y.syö           // Opettaja syö
Siistiä: viimeisin miksaus siis voittaa! Voittajan valinta on itse asiassa hieman hieman mutkikkaanpaa, kun luokkaan liitetään useita piirreluokkia, joilla on itsellään yliluokkia tai "ylipiirreluokkia"!


Huom: Linearisointi ei kuulu koealueeseen! (Tästä viivasta seuraavaan viivaan asia on lähinnä Ohjelmointikielten periaatteet -asiaa)

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

Palataan vielä edellä nähtyyn opiskelijan ja opettajan timanttiin: Kokeillaan korvata myös kenttä kahdessa traitissa:
// Timantteja: korvataan kenttiä
class Henkilö {
  val nimi="Henkilö"
  def syö=println("Henkilö syö")
}
trait Opettaja extends Henkilö {
  override val nimi="Opettaja"
  override def syö=println("Opettaja syö")
}
trait Opiskelija extends Henkilö {
  override val nimi="Opiskelija"  // KENTÄN TUPLAMÄÄRITTELYSTÄ TULEE ONGELMA! 

  override def syö=println("Opiskelija syö")
}
class OpettavaOpiskelija extends Opettaja with Opiskelija {}
val x = new OpettavaOpiskelija
println(x.nimi)
x.syö

class OpiskelevaOpettaja extends Opiskelija with Opettaja {}
val y = new OpiskelevaOpettaja
println(y.nimi)
y.syö
Ei kelpaa kääntäjälle! Saadaan ilmoitus:
java.lang.ClassFormatError: Duplicate method name&signature in class file
  Main$$anon$1$OpettavaOpiskelija
  ...
Jos val-kentät vaihtaa def-metodeihin, linearisointi toimii oletetusti !

Vielä viime keväänä ilmoitus sisälsi seuraavaa

(fragment of tmp.scala):14: error: error overriding value nimi in trait
Opettaja of type java.lang.String;
 value nimi in trait Opiskelija of type java.lang.String cannot override
a value or variable definition in a trait
 (this is an implementation restriction)
class OpettavaOpiskelija extends Opettaja with Opiskelija {}
       ^
(fragment of tmp.scala):19: error: error overriding value nimi in trait
Opiskelija of type java.lang.String;
 value nimi in trait Opettaja of type java.lang.String cannot override a
value or variable definition in a trait
 (this is an implementation restriction)
class OpiskelevaOpettaja extends Opiskelija with Opettaja {}
       ^
two errors found

Onko kyseessä siis käyttämämme Scala-toteutuksen ongelma? Lymyävätkö pohjalla olevan Javan rajoitukset taustalla?
Jos kenttä korvataan aliluokassa ja traitissa, ohjlema toimii:
// Timantteja: korvataan kenttiä aliluokassa ja traitissa
class Henkilö {
  val nimi="Henkilö"
  def syö=println("Henkilö syö")
}
class Opettaja extends Henkilö {
  override val nimi="Opettaja"
  override def syö=println("Opettaja syö")
}
trait Opiskelija extends Henkilö {  // vaatii extends Henkilö, ks. selitys alla
  override val nimi="Opiskelija"

  override def syö=println("Opiskelija syö")
}

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

(Koealueen ulkopuolinen osuus päättyy)



Lähes luokatonta ohjelmointia

Piirreluokilla voi rakennella vaikkapa kapseloituja luokkia ja olioita miltei tyhjästä:
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 ToString extends Nopeus with Moottori with Merkki {
  override def toString =
    "("+ getMerkki +": " + getHevosia + " hp, " + getNopeus + " km/h)"
}

class Auto extends AnyRef with ToString

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

Ohjelmoinnin jatkokurssin (ent. Java-ohjelmointi) rajapintaluokan ja abstraktin luokan eläimellisiä esimerkkejä Scalalle sovellettuina (lainattu Ohjelmointikielten periaatteet -materiaalista):
// --- Eläinten yhteinen yliluokka:

abstract class Eläin (nimi:String) {
  def ääntele: Unit             // abstrakti metodi
  override def toString = nimi  // korvattaessa "override" pakollinen!
}

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

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

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

class Hevonen(nimi:String) extends Eläin(nimi)  {
  override def ääntele = println("Ihahaa")
}
val hevonen = new Hevonen("Polle")
println(hevonen)   // Polle
hevonen.ääntele    // Ihahaa

class Nauta(nimi:String) extends Eläin(nimi)  {
  override def ääntele = println("Ammuu")
}
val nauta = new Nauta("Julle")
println(nauta)   // Julle
nauta.ääntele    // Ammuu

// --- Kokeillaan polymorfismia:

var x:Eläin = new Kissa("Töpö", 2)
println(x)   // Töpö-2
x.ääntele    // Miau
x = new Hevonen("Valma")
println(x)   // Valma
x.ääntele    // Ihahaa
x = new Nauta("Muurikki")
println(x)   // Muurikki
x.ääntele    // Ammuu

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

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

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

class Lehmä(nimi:String) extends Nauta(nimi) with Lypsävä {
  override def lypsä = 3.14 * nimi.length  // maidon määrä riippuu nimen pituudesta...
}
val mansikki = new Lehmä("Mansikki")
println(mansikki)   // Mansikki
mansikki.ääntele    // Ammuu
println(mansikki.lypsä)   // 25.12

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

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

def juustoa(tuotantoeläin:Lypsävä):Double = {42 * tuotantoeläin.lypsä}

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


Piirreluokka 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 Eläin (nimi:String) {
  def ääntele: Unit             // abstrakti metodi
  override def toString = nimi  // korvattaessa "override" pakollinen
}
// --- Pari imettäväisiin kuuluvaa eläinlajia:
class Hevonen(nimi:String) extends Eläin(nimi)  {
  override def ääntele = println("Ihahaa")
}
class Nauta(nimi:String) extends Eläin(nimi)  {
  override def ääntele = println("Ammuu")
}

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

trait Lypsävä {
  def lypsä(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 Lehmä(nimi:String) extends Nauta(nimi) with Lypsävä {
   // lypsä(n:String) peritään traitista, so. oletustoteutus

   def potkaise = println("Lehmä potkaisee")
}
val mansikki = new Lehmä("Mansikki")
println(mansikki)   // Mansikki
mansikki.ääntele    // Ammuu
println(mansikki.lypsä(mansikki.toString))   // 25.12
mansikki.potkaise   // Lehmä potkaisee

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

// Lopuksi tehdään juustoa:

def juustoa(tuotantoeläin: Lypsävä):Double =
     {42 * tuotantoeläin.lypsä(tuotantoeläin.toString)}

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