Muutettu viimeksi 19.4.2010 / Sivu luotu 14.4.2010 / [oppikirjan esimerkit] / [Scala]
Sivun sisältöä:
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"?
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!
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!
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
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.)
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()) // 30Ohjelmoidaan 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()) // 60Ohjelmoidaan 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()) // 31Muuntavien 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 */
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"!
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
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, AnyPalataan 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 foundOnko kyseessä siis käyttämämme Scala-toteutuksen ongelma? Lymyävätkö pohjalla olevan Javan rajoitukset taustalla?
// 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)
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 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