Muutettu viimeksi 30.3.2010 / Sivu luotu 24.3.2010 / [oppikirjan esimerkit] / [Scala]
Sivun sisältöä:
Scalan sisäänrakennetut ohjausrakenteet ovat if, while, do-while, for, try-catch ja match. Ehdollisuus, ehdolliset toistot ja poikkeusten käsittely ovat paljolti "Javasta tuttuja". Nuo pari muuta sitten ovatkin oma lukunsa.
Ja koska "melkein kaikella" Scalassa on arvo, uutuuksia löytyy.
//--"klassisesti": if (b < c) a = c else a = b //--ehdollinen lauseke: a = if (b < c) c else bKoska Scala-maailma arvostaa val-muuttujia enemmän kuin var-muuttujia, seuraavat muunnelmat äskeisestä perustelevat "uuden tyylin" paremmuutta:
var a1 = b // oletus if (a1 < c) // korjaus? a1 = c //------------ val a2 = if (b < c) c else b
def gcdLoop(x: Long, y: Long): Long = { var a = x var b = y while (a != 0) { val temp = a a = b % a b = temp } b } println(gcdLoop(49,56)) // 7Yksi opettavainen yksityiskohta, joka ei sinänsä liity toistorakenteeseen: Vaikka val temp siis onkin "vakio" omassa lohkossaan, se lohkon eri suorituskerroilla on "eri vakio"!
Myös loppuehtoinen toisto on tuttu:
var otus = "" do { println("Syötä kissa!") otus = readLine } while (otus != "kissa") println("Vihdoinkin ymmärsit.")
Huom: Myös Scalassa while- ja do-while-rakenteet ovat perinteisen puheenparren mukaisia lauseita (statement), eivät lausekkeita (expression), koska "ne vain tekevät jotakin" ja "niillä ei ole arvoa". Scalassa näillä kuitenkin itse asiassa on arvo, mutta arvoksi vähän erikoinen: Unit eli ().
Huom: Myös uudelleensijoitus var-muuttujaan on Scalassa Unit! Tällä on merkitystä Java/C++-idiomiin tottuneelle: sijoituslausetta ei (onneksi!) voikaan käyttää arvon ilmauksena. Seuraavan tyylinen logiikka ei onnistu:
var rivi = "" while ((rivi = readLine()) != "") println(rivi)Ilmaus rivi = readLine() on siis tyypiltään Unit, ei Javan ja C-kielten tapaan String! Vaikka erisuuruusvertailu on sallittu Unitin ja Stringin kesken, siitä ei ole paljonkaan iloa, koska se on aina true. Kääntäjä onneksi varoittaa asiasta: warning: comparing values of types Unit and java.lang.String using `!=' will always yield true. Homman voi hoitaa "perinneohjelmointityyliin" vaikkapa seuraavasti:
var rivi = readLine() while (rivi != "") { println(rivi) rivi = readLine() }
for-rakenteella tehdään vaikka millasta iterointia erilaisten arvoalueiden ja kokoelmien yli, suodatetaan kokoelmista osakokelmia ja voidaan tuottaa uusia kokoelmia yms...
("Iterare" (it.) = (engl.) double, duplicate, recur, reduplicate, repeat, replicate, go over, ingeminate, iterate, reiterate, repeat, restate, retell, run over, say after, say again)
Aleteaan perehtyä tähän monipuoliseen työkaluun oppikirjan esimerkien avulla:
Käydään läpi kokoelman alkiot:
val filesHere = (new java.io.File(".")).listFiles for (file <- filesHere) println(file)Luodaan nykyhakemistosta Javan File-olio, josta listFiles operaatio luo taulukon (Array[File]) hakemiston sisältämistä tiedostoista ja hakemistoista. For-lauseen otsikossa oleva generaattori file <- filesHere antaa taulukon alkiot yksi kerrallaan val-muuttujan file arvoksi. Ja tästä muuttujasta toistettavan alialgoritmi sitten saa File-oliot käyttöönsä.
Kokonaisluvuille on käytettävissä pari operaatiota, jotka tuottavat Range-olion. Sekin kelpaa iteroitavaksi:
for (i <- 1 until 4) print(i) // 123 for (i <- 1 to 4) print(i) // 1234
Generoitavaa arvojonoa voidaan myös suodattaa; Tuotetaan esimerkkinä vaikkapa kerrosnumeroita amerikkalaiseen hotelliin:
for (i <- 1 to 20 if i != 13) print(i + " ") // 1 2 3 4 5 6 7 8 9 10 11 12 14 15 16 17 18 19 20
Tai listataan .scala-tiedostot:
val filesHere = (new java.io.File(".")).listFiles for (file <- filesHere if file.getName.endsWith(".scala")) println(file)Saman voisi tehdä myös perinneohjelmoiden:
for (file <- filesHere) if (file.getName.endsWith(".scala")) println(file)Mutta näillä versioilla on yksi mielenkiintoinen ero joka paljastuu pikapuolin...
Suodatinehtoja voi olla useampiakin, erottimena on oltava puolipiste. Tulostetaan parittomat luvun paitsi 13 väliltä 1-20:
for (i <- 1 to 20 if i%2 != 0; if i != 13) print(i + " ") // 1 3 5 7 9 11 15 17 19Huom: Puolipiste on nimenomaan erotin. Viimeistä ehtoa ei saa seurata puolipiste! Miksiköhän?
Hienompi esimerkki: Luetellaan nykyhakemiston kaikki sellaiset tiedostot, jotka eivät ole hakemistoja ja joiden nimi päättyy merkkijonoon html:
for ( file <- filesHere if file.isFile; if file.getName.endsWith("html") ) println(file)
For-ilmauksen otsikossa voi olla myös useampia generaattoreita. Ne tulkitaan sisäkkäisiksi:
for (i <- 1 to 4; j <- i+1 to 4) println(i + " " + j) Tulostus: 1 2 1 3 1 4 2 3 2 4 3 4Tehdäänpä sitten ihan todellinen ohjelma: Tämä ohjelma selvittää annetun välin lukuparit, joiden suurin yhteinen tekijä on yksi:
def syt(a: Int, b: Int): Int = if (b == 0) a else syt(b, a % b) println("Lukuparit joilla ei ole muita yhteisiä tekijöitä kuin 1:") print("Välin alku? ") val alku = readInt print("Välin loppu? ") val loppu = readInt for (i <- alku to loppu; j <- i+1 to loppu if syt(i, j) == 1 ) println(i + " " + j)[Tässä välissä tuli kiinnostava elämys: ohjelmointi on (taas pitkästä aikaa) hauskaa!]
Sisäkkäisiin generaattoreihin voi liittyä kuhunkin omia ehtojaan. Seuraava ohjelma etsii ja tulostaa suoritushakemistostaan löytyvien ".txt"-tiedostojen kaikki sellaiset rivit, joilta löytyy "kissa":
val filesHere = (new java.io.File(".")).listFiles def fileLines(file: java.io.File) = // tiedostot taas listaksi $ scala.io.Source.fromFile(file).getLines.toList // hmm... ja hmm...!! def grep(pattern: String) = for ( file <- filesHere if file.getName.endsWith(".txt"); line <- fileLines(file) if line contains pattern ) println(file +":\n"+ line) grep("kissa")
Esimerkeissä nähty for-ilmauksen otsikon rakenne
for(generoi jokin arvojen jono; suodata saadusta jonosta mukaan jokin osa; generoi jokaista yltä saatua arvoa kohden jokin arvojen jono; suodata saadusta jonosta mukaan jokin osa; generoi jokaista yltä saatua arvoa kohden jokin arvojen jono; suodata saadusta jonosta mukaan jokin osa ... )tuo mieleen jo aika lailla "funktionaalisia" ajatuksia. (Ainakin minun mieleeni!)
Nähdyissä esimerkeissä for-ilmausta käytetään lauseena. Sen tyyppi noissa esimerkeissä on Unit eli (). Mutta for-ilmauksella voi todellakin ilmaista myös arvoja! Kirjoittamalla for-otsakkeen jälkeen ilmaus yield arvo, syntyy jono arvoja, joita voi sitten edelleen käyttää muissa operaatioissa:
scala> val lause = for (i <- 1 to 3) print(i); println 123 lause: Unit = () scala> val lauseke = for (i <- 1 to 3) yield i lauseke: RandomAccessSeq.Projection[Int] = RangeM(1, 2, 3) scala> println(lause) () scala> println(lauseke) RangeM(1, 2, 3) scala> for (a <- lauseke) print(a) 123
Seuraavassa esimerkissä ensin käydään läpi nykyhakemiston kaikkien .txt-tiedostojen kaikki rivit, joista valitaan kissan sisältävät. Näiden rivien pituuksista tuotetaan (yield) Int-taulukko, jonka alkiot lopuksi tulostetaan:
val filesHere = (new java.io.File(".")).listFiles def fileLines(file: java.io.File) = scala.io.Source.fromFile(file).getLines.toList val kissaRivienPituudet = for { file <- filesHere if file.getName.endsWith(".txt") line <- fileLines(file) if line contains "kissa" } yield line.length for (pituus <- kissaRivienPituudet) println(pituus)
Aika jännittäviäkin asioita voidaan "yieldata":
var y = 100 val x = for (i <- 1 to 3) yield {print("töitä tehdään vaiheessa " + i) println(" ja y = " +y) y += 1 } println(x) println(x) y = 1200 println(x)Muuttujan y sitominen arvoonsa on kiinnostavaa; ohjelma tulostaa:
töitä tehdään vaiheessa 1 ja y = 100 töitä tehdään vaiheessa 2 ja y = 101 töitä tehdään vaiheessa 3 ja y = 102 RangeM((), (), ()) töitä tehdään vaiheessa 1 ja y = 103 töitä tehdään vaiheessa 2 ja y = 104 töitä tehdään vaiheessa 3 ja y = 105 RangeM((), (), ()) töitä tehdään vaiheessa 1 ja y = 1200 töitä tehdään vaiheessa 2 ja y = 1201 töitä tehdään vaiheessa 3 ja y = 1202 RangeM((), (), ())Mutta jätetään näin jännät jutut toistaiseksi. Jäävät ehkä syventäviin opintoihin... ;-)
Ohjelmoidaan esimerkkinä kokonaisjakolaskupalvelu:
println("Anna jaettava ja jakaja!") try { val osoittaja = readInt val nimittaja = readInt println(osoittaja +"/"+ nimittaja + " = " + osoittaja/nimittaja) } catch { case e: NumberFormatException => println("Ei kelpaa: " + e) case e: ArithmeticException => println("Jakaja on nolla: " + e) } finally println("Kiitos käynnistä, kävi miten kävi.")"Kuten kuvasta näkyy" case-osan rakenteessa on Java-ohjelmoijalle jotain uutta: siepattavat poikkeukset ilmaistaan Scalan hahmontunnistusilmauksilla (engl. pattern matching)
case ... => ...Näille opitaan aikanaan muutakin käyttöä. Ja opitaan myös ymmärtämään, mitä ne oikeastaan sanovat.
Scala-knoppologiaa:
Kuten toistuvasti nähdään, Scalassa (melkein) kaikella on arvo. Niin myös throw-ilmauksella:
val n = readInt val half = if (n % 2 == 0) n / 2 else throw new RuntimeException("n must be even")Mitään arvoahan ei muuttujalle half aseteta, jos n on pariton. Vahvasti tyypitetyssä kielessä "arvoilla" pitää kuitenkin aina olla tyyppi. Teknisesti throw-lause on tyypiltään Nothing, joka on kaikkien Scalan tyyppien alityyppi. Tässä esimerkissä se tarkoittaa, että Nothing kelpaa esim. juuri Int-tyypin arvoksi!
Myös try-catch-finally-rakenteella on arvo, joka määräytyy siitä, minne suorituksessa päädytään. finally-osa suoritetaan aina ja siihen liittyy oma return-knoppologiansa:
def f: Int = try { return 1 } finally { return 2 } def g: Int = try { 1 } finally { 2 } println(f) // 2 println(g) // 1Eli ilman returnia finally-osan arvoa ei oteta koko rakenteen arvoksi, returnin kanssa finally-osan arvo on koko rakenteen arvo. Tästä voisi sanoa englantilaisen snobin tyyliin "couldn't care less"... ;-)
Esimerkkinä paperi-kivi-sakset-peli:
println("Paperi-kivi-sakset-peli") val valinta = readLine("Minkä valitset? ").trim.toLowerCase valinta match { case "paperi" => println("Valitsen sakset. Voitin!") case "kivi" => println("Valitsen paperin. Voitin!") case "sakset" => println("Valitsen kiven. Voitin!") case _ => println("Älä huijjaa!") }Huomioita:
valinta match { case "paperi" => println("Valitsen sakset. Voitin!") println("Tietenkin...") println("Et kai muuta kuvitellutkaan?") ... case _ => println("Älä huijjaa!") }
Ja kun Scalasta on kyse, arvoja suositaan algoritmien kustannuksella.
println("Paperi-kivi-sakset-peli") val valinta = readLine("Minkä valitset? ").trim.toLowerCase val voittostrategia = valinta match { case "paperi" => "Valitsen sakset. Voitin!" case "kivi" => "Valitsen paperin. Voitin!" case "sakset" => "Valitsen kiven. Voitin!" case _ => "Älä huijjaa!" } println(voittostrategia)
val a=1; val b=2; val c=3; // | // | { val b=20; val c=30; // || // || { val c=300; // ||| println(a +"/"+ b +"/"+ c); // ||| 1/20/300 } // ||| println(a +"/"+ b +"/"+ c); // || 1/20/30 } // || // || println(a +"/"+ b +"/"+ c) // | 1/2/3Esimerkki sisäkkäisistä aliohjelmista ja niiden kutsuista:
def päärutiini { // | // | def apurutiiniA { // || def apuapurutiiniAA { // ||| println("Olen apuapurutiiniAA") // ||| } // end apuapurutiiniAA // ||| println("Olen apurutiiniA") // || apuapurutiiniAA // || } // end apurutiiniA // || def apurutiiniB { // || println("Olen apurutiiniB") // || } // end apurutiiniB // || println("Olen päärutiini") // | apurutiiniA // | apurutiiniB // | } // end päärutiini // | päärutiiniTulostus:
Olen päärutiini Olen apurutiiniA Olen apuapurutiiniAA Olen apurutiiniBLuennolla piirretään kuva tämän ohjelman määrittely- ja kutsurakenteesta!
Ei-litteyden ansiosta Scalalla on vaivatonta ohjelmoida esim. metodin sisäisiä ja siis paikallisia pikku apumetodeita (tai funktiolle apufunktioita) tyyliin (luvun 1 Quicksort):
def sort(xs: Array[Int]) { def swap(i: Int, j: Int) { val t = xs(i); xs(i) = xs(j); xs(j) = t } def sort1(l: Int, r: Int) { ... swap(i, j) ... if (l < j) sort1(l, j) ... sort1(0, xs.length - 1) }Tässä siis julkisen järjestämismetodin sort(xs: Array[Int]) sisällä on määritelty kaksi paikallista apumetodia swap(i: Int, j: Int) ja sort1(l: Int, r: Int).
"Klassinen perinneohjelmointi" noudatti usein seuraavan näköistä idiomia (Pascal, Algol, ...) (tässä scalattuna):
object Klassinen extends Application { var a=1; var b=2 // "globaalit muuttujat" def ali1(x: Int) { var e=40; var f=50 // ali1:n paikalliset muuttujat println(x + e + a) // parametri, paikallinen, globaali b = -1000 // muutos globaaliin } def ali2(y: Int): Int = { var g=600; var h=700 // ali2:n paikalliset muuttujat return y + g + a // parametri, paikallinen, globaali } // pääohjelma: ali1(b) a = ali2(b+7) println(a) }
Huom: Scala-komentorivitulkki sallii tunnusten uudelleenmäärittelyn tyyliin "monikäyttöinen a":
scala> val a=1 a: Int = 1 scala> var a=67 a: Int = 67 scala> println(a) 67 scala> def a {println("hip hei")} a: Unit scala> a hip heiMiksi tuo on mahdollista? No, lohkoilla se saadaan aikaan. Tulkki tulkitsee jokaisen komentorivin aloittavan uuden sisemmän lohkon eli sen "ajatus juoksee" seuraavaan tapaan:
{ val a=1; { var a=67; { println(a); // 67 { def a {println("hip hei")} { a // hip hei } } } } }Katsotaan vielä kirjan kaunis kertotauluesimerkki
// Returns a row as a sequence def makeRowSeq(row: Int) = for (col <- 1 to 10) yield { // JOKIN scala.Seq-JONO Stringejä val prod = (row * col).toString val padding = " " * (4 - prod.length) padding + prod } // Returns a row as a string def makeRow(row: Int) = makeRowSeq(row).mkString // JONOSTA Stringejä YKSI MERKKIJONO // Returns table as a string with one row per line def multiTable() = { val tableSeq = // a sequence of row strings for (row <- 1 to 10) yield makeRow(row) // JOKIN scala.Seq-JONO SARAKE-Stringejä tableSeq.mkString("\n") // JONOSTA Stringejä YKSI MERKKIJONO } println(multiTable)Tulostus:
1 2 3 4 5 6 7 8 9 10 2 4 6 8 10 12 14 16 18 20 3 6 9 12 15 18 21 24 27 30 4 8 12 16 20 24 28 32 36 40 5 10 15 20 25 30 35 40 45 50 6 12 18 24 30 36 42 48 54 60 7 14 21 28 35 42 49 56 63 70 8 16 24 32 40 48 56 64 72 80 9 18 27 36 45 54 63 72 81 90 10 20 30 40 50 60 70 80 90 100