Oppimateriaalin copyright © 2009 Arto Wikla. Tämän oppimateriaalin käyttö on sallittu vain yksityishenkilöille opiskelutarkoituksissa. Materiaalin käyttö muihin tarkoituksiin, kuten kaupallisilla tai muilla kursseilla, on kielletty.
(Muutettu viimeksi 2.4.2009)

Periytymistä ja "traittien" "mixaamista" Scalalla

Peruskuviot

Sovelletaan 1. vuoden ohjelmointikurssin esimerkkiä:
// --- 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

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

Joustavaa(?) perinneohjelmointia

"Traitit" ovat aika joustavia! Niistä voi rakennella vaikka jopa perinnetietueita:

class Ajoneuvo {var nopeus=0}

trait Ohjauspyörä {var läpimitta=30}
trait Moottori {var hevosvoimia=100}

class Auto extends Ajoneuvo with Ohjauspyörä with Moottori {var merkki="perusauto"}

val biili = new Auto

biili.nopeus = 120
biili.läpimitta = 25
biili.hevosvoimia = 200
biili.merkki = "Maserati"

println(biili.nopeus)       // 120
println(biili.läpimitta)    // 25
println(biili.hevosvoimia)  // 200
println(biili.merkki)       // Maserati

Tällainen taitaa kuitenkin olla hienon kielen väärinkäyttöä...

Scalan timantti

Scalassa ei ole luokkien moniperintää. Tehdään aluksi versio, jossa timantti syntyy luokasta ja traitista:
// Timantteja: aliluokka ja "traitti"
class Henkilö {
  val nimi="Henkilö"
  def syö=println("Henkilö syö")
}
class Opettaja extends Henkilö {
  override def syö=println("Opettaja syö")
}
trait Opiskelija extends Henkilö {  // 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. Huom: Kun "traitti extendeeraa" luokkaa A, vain luokan A jälkeläisiin on luvallista miksata tuo traitti!

Luontevampaa ehkä olisi ohjelmoida symmetrisesti sekä Opettaja että Opiskelija traittina:

// Timantteja: molemmat traitteja
class Henkilö {
  val nimi="Henkilö"
  def syö=println("Henkilö syö")
}
trait Opettaja extends Henkilö {
  override def syö=println("Opettaja syö")
}
trait Opiskelija extends Henkilö {
  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 miksataan jono traitteja, joilla on itsellään yliluokkia tai "ylitraitteja"! Valintajärjestys on täsmällisesti (ja luontevasti) määritelty ns. periytymishierarkian linearisoinnilla (Odersky et al. s. 228-230):
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

Kokeillaan sitten korvata 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 onnistukaan! Saadaan yllättävä ilmoitus:
(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? Sallisiko kielen määrittely tilanteen?
Jos kenttä korvataan aliluokassa ja traitissa, ei saada virheilmoitusta:
// 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ö

Ohjelmointitekniikoita "traiteilla"

(Odersky et al.)

Laihoista rajapinnoista halvalla rikkaita

Tekniikka muistuttaa Javan abstraktien luokkien yhtä käyttötapaa: konkreettinen aliluokka toteuttaa jotkin abstrakteina perimänsä yksinkertaiset perusoperaatiot ja perii abstraktilta luokalta joukon ei-abstrakteja operaatioita, jotka on ohjelmoitu abstrakteja metodeita kutsumalla.

Esimerkki (Odersky et al. s. 217-219):

trait Rectangular {
  def topLeft: Point       // Vain nämä kaksi ovat abstrakteja, näiden
  def bottomRight: Point   // avulla toteutetaan kaikki muut.
  def left = topLeft.x
  def right = bottomRight.x
  def width = right - left
  // and many more geometric methods...
}
Nyt voidaan ohjelmoida tyyliin:
abstract class Component extends Rectangular {  // saadaan käyttöön läjäpäin
                                                // valmiiksi toteutettuja metodeita
  // other methods...
}
...
class Rectangle(val topLeft: Point, val bottomRight: Point)
  extends Rectangular {
// other methods...
}
...
Ja käytetään:
scala> val rect = new Rectangle(new Point(1, 1),
           new Point(10, 10))
rect: Rectangle = Rectangle@3536fd

scala> rect.left
res2: Int = 1

scala> rect.right
res3: Int = 10

scala> rect.width
res4: Int = 9

...

trait Ordered - Javan Comparable-interfacen vastine

Esimerkki laihan rajapinnan lähes ilmaisesta rikastamisesta: Annetaan toteutus Ordered-traitin nimeämälle abstraktille compare-metodille ja saadaan sen avulla toteutettuna muita vertailuoperaatioita. Ks. Scala-API.

Traitteja pinoon

(Odersky et al. s. 222-226) Viitaukset traitissa yliluokan piirteisiin (super) tarjoavat tyylikkään/kauhean/selkeän/sekavan (valitse omasi) tavan ketjuttaa ("pinota") "yliluokkien superien" kutsumista.

Abstrakti jono:

abstract class IntQueue {
  def get(): Int
  def put(x: Int)
}
Sen toteutus:
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 }
}
Käyttöä:
scala> val queue = new BasicIntQueue
queue: BasicIntQueue = BasicIntQueue@24655f

scala> queue.put(10)

scala> queue.put(20)

scala> queue.get()
res9: Int = 10

scala> queue.get()
res10: Int = 20

Tuplaustraitti:
trait Doubling extends IntQueue {
  abstract override def put(x: Int) { super.put(2 * x) }
}
Käyttöä:
scala> class MyQueue extends BasicIntQueue with Doubling
defined class MyQueue

scala> val queue = new MyQueue
queue: MyQueue = MyQueue@91f017

scala> queue.put(10)

scala> queue.get()
res12: Int = 20

Pari lisätraittia:
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)
  }
}

Käyttöä:
scala> val queue = (new BasicIntQueue
           with Incrementing with Filtering)
queue: BasicIntQueue with Incrementing with Filtering...

scala> queue.put(-1); queue.put(0); queue.put(1)

scala> queue.get()
res15: Int = 1

scala> queue.get()
res16: Int = 2

Mutta jos vaihdetaan traittien järjestys:
scala> val queue = (new BasicIntQueue
           with Filtering with Incrementing)
queue: BasicIntQueue with Filtering with Incrementing...

scala> queue.put(-1); queue.put(0); queue.put(1)

scala> queue.get()
res17: Int = 0

scala> queue.get()
res18: Int = 1

scala> queue.get()
res19: Int = 2


Takaisin sisältösivulle.