Muutettu viimeksi 5.4.2016 / Sivu luotu 19.3.2010 / [oppikirjan esimerkit] / [Scala]
Sivun sisältöä:
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".)
Tällä tavoin kirjoitetusta luokkaparametreista synytetään jokaiselle luotavalle oliolle omat salaiset vakiokentät. Luokkamäärittelyn otsake siis määrittelee pääkonstruktorin:
val x = new Rational(1,2) println(x); // Main$$anon$1$Rational@7ba4f24fAaltosulkeitakaan ei tarvita, ellei luokkamäärittelyltä muuta haluta kuin keino luoda oliota luokkaparametrikentin. Tyhjän määrittelylohkon { } saa toki kirjoittaa, jos jostain syystä haluaa. Olion oletusarvoinen tulostustaitokin näkyy luokkaan Javan tapaan periytyvän "jostain ylempää".
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
Oliota luotaessa siis määritellään kenttiä ja metodeita ja suoritetaan luokkamäärittelyn mahdollisesti sisältämät lauseet (tms.).
Korvataan peritty toString-metodi omalla:
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
Luokkaan voidaan määritellä kenttiä erilaisin tavoin:
class SuperSalainen (x: Int) { private val y = 3 private[this] val z = 3 def eiToimi1(toinen: SuperSalainen): Int = toinen.x def toimii (toinen: SuperSalainen): Int = toinen.y def eiToimi2(toinen: SuperSalainen): Int = toinen.z }
class Rational(os: Int, nim: Int) { Rational.lkm += 1 println("Hip, hei, syntyi " + Rational.lkm +". luku: "+ os +"/"+ nim) } object Rational { // oliokumppani kätkee lukumäärälaskurin 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 thisLaskuri saatiin private-määreellä turvaan ulkopuolisilta! Ja luokaan siis näkyvät oliokumppanin yksityiset kentät. Koska toisaalta oliokumppanin sisältä ei näe luokkakumppanin kenttiä ilman luokan ilmentymää, mieleen tulee elävästi Javan sääntö "staticista ei näe ei-staticciin, mutta ei-staticista näkee staticciin".
Hyvä peruste on tietenkin se, että luokasta Rational voi olla useita ilmentymiä ja siten myös kentistä useita versioita. Rational-ainokainen sen sijaan on ainoa lajissaan ja sen kentät ovat aina yksikäsitteiset.
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 ... ... ja rivikaupalla muita virheilmoituksia ... ...
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, mutta mikseipä noita rationaaliluvun muuttumattomia osia voi näyttää ulkomaailmallekin? Saadaan ikäänkuin ilmaiset "getterit". Muuttamaanhan val-kenttiä ei kukaan pääse.
Ohjelmoidaan rationaalilukujen yhteen- ja kertolasku:
class Rational(os: Int, nim: Int) { require(nim != 0) val osoittaja = os val nimittaja = nim def +(that: Rational): Rational = new Rational( 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
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 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(5, 6) val kolmonen = new Rational(3) println(kolmonen) // 3/1 println(a * kolmonen) // 15/6Lisäkonstruktorit voivat pääkonstruktorin sijasta toki kutsua myös toisia lisäkonstruktoreita. Mutta vain luokan kirjoitusasussa aiemmin määriteltyjä konstruktoreita on luvallista kutsua.
Huom: Myös lisäkonstruktorissa voi olla suoritettava lohko. Mutta tämän konstruktorin kutsuman toisen konstruktorin kutsun on oltava tuon 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änkin ongelmaan löytyy pian apu!
val a = new Rational(2, 3) val b = new Rational(4, 5) println(a + a * b + b) // 450/225Voidaan 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! } println(new Rational(7,49)) // 1/7 val a = new Rational(2, 3) val b = new Rational(4, 5) println(a + a * b + b) // 2/1
Tyylikästä vai mitä!
No, ei tässä mitään hätää, kuormitetaan yhteenlasku ja kertolasku:
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/5Nyt voi siis rationaalilukuun 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: (x: Double)Double(x: Float)Float (x: Long)Long (x: Int)Int (x: Char)Int (x: Short)Int (x: Byte)Int (x: String)String cannot be applied to (this.Rational) println(1 + a) ^ one error found
Jotenkin olisi kuitenkin kivaa, että yhteenlasku ja kertolasku 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 import scala.language.implicitConversions // ilman tätä tulee varoitus 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. Ainakin toistaiseksi tyydymme siihen, että määritelmä löytyy samasta käännösyksiköstä, jossa muunnosta tarvitaan.