Muutettu viimeksi 24.3.2010 / Sivu luotu 19.3.2010 / [oppikirjan esimerkit] / [Scala]
Sivun sisältöä:
Oppikirjan hengessä aletaan rakennella immutaabelia rationaalilukuluokkaa Rational. Välillä katsotaan muitakin esimerkkejä periaatteista ja tekniikoista.
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".)
Luokkaparametreista synytetään jokaiselle luotavalle oliolle omat salaiset vakiokentät. Luokkamäärittelyn otsake myös määrittelee pääkonstruktorin:
val x = new Rational(1,2) println(x); // Main$Rational$1@b2002fAaltosulkeitakaan ei tarvita, ellei luokkamäärittelyltä muuta haluta kuin keino luoda oliota salaisin vakiokentin. Tyhjän { } saa toki kirjoittaa. Olion tulostustaitokin näkyy luokkaan Javan tapaan periytyvän.
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
Luokassa voidaan siis määritellä kenttiä:
class SuperSalainen { private val x = 3 private[this] val y = 3 def toimii (kaveri: SuperSalainen): Int = kaveri.x def eiToimi(kaveri: SuperSalainen): Int = kaveri.y }[Ja hommeli on itse asiassa vieläkin monipuolisempi: vastaavalla syntaksilla voidaan säädellä ominaisuuksien näkyvyyttä myös esim. pakkauksiin. Lisää sepustusta asiasta löytyy sivulta The busy Java developer's guide to Scala: Packages and access modifiers! Kiitokset näistä tiedoista ja hyvästä esimerkistä Martin Pärtelille 23.3.2010!]
Ainokaista käyttäen on helppo ohjelmoida "sarjanumeron generointi", mikä Javalla tehdään staticeilla:
class Rational(os: Int, nim: Int) { Rational.lkm += 1 println("Hip, hei, syntyi " + Rational.lkm +". luku: "+ os +"/"+ nim) } object Rational { 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äkyy näkyvän myös oliokumppanin yksityiset kentät. Koska toisaalta oliokumppanin sisältä ei näe luokkakumppanin kenttiä, mieleen tulee elävästi Javan sääntö: "staticista ei näe ei-staticciin, mutta ei-staticista näkee staticciin".
Oliota luotaessa siis määritellään kenttiä ja metodeita ja suoritetaan luokkamäärittelyn mahdollisesti sisältämät lauseet (tms.).
Korvataan peritty toString:
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
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 ... ...
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 a'la Java-tyyli, mutta miksei noita rationaaliluvun muuttumattomia osia voisi näyttää ulkomaailmallekin? Saadaan ikäänkuin ilmaiset "getterit".
Ohjelmoidaan rationaalilukujen yhteen- ja kertolasku:
class Rational(os: Int, nim: Int) { require(nim != 0) val osoittaja = os val nimittaja = nim 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(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 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 } val a = new Rational(5, 6) val kolmonen = new Rational(3) println(a * kolmonen) // 15/6Lisäkonstruktorit voivat pääkonstruktorin sijasta kutsua myös toisia lisäkonstruktoreita. Mutta vain luokan kirjoitusasussa aiemmin määriteltyjä konstruktoreita on luvallista kutsua.
Huom: Myös lisäkonstruktoreissa voi olla suoritettava lohko. Mutta toisen konstruktorin kutsun on oltava 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än 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! } val a = new Rational(2, 3) val b = new Rational(4, 5) println(a + a * b + b) // 2/1
No, kuormitetaan:
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/5Jo siis voi rationaaliin 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 (Double)Double
println(1 + a) ^ one error found !!!Jotenkin olisi kivaa jos yhteenlasku ja kertolasku kuitenkin 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 implicit def intToRational(x: Int) = new Rational(x) println(1 + a) // 5/3 println(2 * b) // 8/5Johan alkoi Lyyti kirjoittaa...
Tuota "implisiittimääritelmää" ei voi kirjoittaa tietenkään Rational-luokan sisälle, koska silloin se ei näkyisi muualle. Miten ja mistä kääntäjä tuon määrittelyn osaa löytää, selviää, jos kurssilla ehditään kirjan lukuun 21...