Muutettu viimeksi 3.3.2016 / 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 //-- Scalan ehdollinen lauseke: a = if (b < c) c else b //-- oletus ja korjaus: var a = b // oletus if (a < c) // korjaus? a = c //-- val on var:ia tyylikkäämpi ja turvallisempi: val a = 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)) // 7Opettavainen (?) 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 = scala.io.StdIn.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 sijoitus var-muuttujaan on Scalassa Unit! Tällä on merkitystä Java/C-idiomiin tottuneelle: sijoituslausetta ei (onneksi!) voi käyttää arvon ilmauksena. Seuraavan tyylinen logiikka ei onnistu:
var rivi = "" while ((rivi = readLine()) != "") // Vrt. Java ja C! 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 while ((rivi = readLine()) != "") ^Homman voi tietenkin 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)
Aletaan 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ä tiedosto- ja hakemistonimistä. For-lauseen otsikossa oleva generaattori file <- filesHere antaa taulukon alkiot yksi kerrallaan val-muuttujan file arvoksi. Tästä muuttujasta toistettava alialgoritmi saa File-oliot käyttöönsä ja tulostaa ne toString-metodin ohjaamalla tavalla tiedostoniminä.
Kokonaisluvuille on käytettävissä pari operaatiota, until ja to, jotka tuottavat Range-olion. Sellainen toki kelpaa iteroitavaksi:
for (i <- 1 until 4) print(i) // 123 for (i <- 1 to 4) print(i) // 1234
For-askel voidaan antaa myös vaikkapa seuraavaan tapaan:
val a = readInt val b = readInt for (i <- (a to b) by (if (a < b) 1 else -1)) println(i)
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 nykyhakemiston .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, jos ehdot ovat samalla rivillä. Tulostetaan parittomat luvut 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 19
Hienompi esimerkki: Luetellaan nykyhakemiston kaikki sellaiset tiedostot, jotka eivät ole hakemistoja ja joiden nimi päättyy merkkijonoon html:
val filesHere = (new java.io.File(".")).listFiles 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ästäpä tuli kiinnostava elämys: ohjelmointihan on ihan 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äytettiin lauseena, komentona tehdä jotakin. Silloin ilmauksen omakin tyyppi on Unit eli ().
scala> val lause = for (i <- 1 to 3) print(i); println 123 lause: Unit = () scala> println(lause) ()Mutta for-ilmauksella voi ilmaista myös arvon, so. kyseessä voi olla lauseke. Kirjoittamalla for-otsakkeen jälkeen ilmaus yield arvo, syntyy jono arvoja (Vector-olio), joita voi sitten käyttää muissa operaatioissa:
scala> val lauseke = for (i <- 1 to 3) yield i lauseke: scala.collection.immutable.IndexedSeq[Int] = Vector(1, 2, 3) scala> println(lauseke) Vector(1, 2, 3) scala> for (a <- lauseke) print(a) 123
Hotellikerroksia:
val hotellikerrokset = for (i <- 1 to 20 if i != 13) yield i println(hotellikerrokset) // Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 16, 17, 18, 19, 20) for (k <- hotellikerrokset) print(k + " ") // 1 2 3 4 5 6 7 8 9 10 11 12 14 15 16 17 18 19 20 // jne.Sisäkkäiset generaattorit:
val parit = for (i <- 1 to 4; j <- i+1 to 4) yield i + " " + j println(parit) // Vector(1 2, 1 3, 1 4, 2 3, 2 4, 3 4)
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 Int-vektori, 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)
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äistä lisää myöhemmin.
Esimerkkinä paperi-kivi-sakset-peli:
import scala.io.StdIn._ 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)
Valintarakenne match on hyvin monipuolinen väline. Seuraavat esimerkit antavat asiasta vain kalpean aavistuksen mutta sisältänevät silti jotakin kiinnostavaa ja mahdollisesti hyödyllistäkin:
// keskenään eri tyyppisiä vakioita: def vastaus(x: Any) = // Any on mitä tahansa... x match { case 2 => "kolme" case true => "Tosi on!" case "moi" => "moro" case 3.14 => "pii" case Nil => "tyhjä" case _ => "Enpä osaa sanoa" } println(vastaus(1+1)) // kolme println(vastaus(1<2)) // Tosi on! println(vastaus(3.15-0.01)) // pii println(vastaus("moi")) // moro println(vastaus(List())) // tyhjä println(vastaus(1)) // Enpä osaa sanoa println(vastaus(new Rational(2))) // Enpä osaa sanoa ===================================================================== // predikaatti nolla ja muut: def nollako(x: Any) = x match { case 0 => true case _ => false } println(nollako(2)) // false println(nollako(3-1-2)) // true println(nollako("kissa")) // false println(nollako(3.14)) // false ===================================================================== // ollaanko sitä nolla nolla: def nollako(x: Any) = x match { case 0 => "Nolla on!" case e => e + " ei ole nolla!" // HUOM: parametri } println(nollako(2)) // 2 ei ole nolla! println(nollako(3-1-2)) // Nolla on! println(nollako("kissa")) // kissa ei ole nolla! println(nollako(3.14)) // 3.14 ei ole nolla! println(nollako({val a=1})) // () ei ole nolla! println(nollako({})) // () ei ole nolla! println(nollako{x:Int=>x+1}) // <function1> ei ole nolla! ===================================================================== // tunnistetaan Int, String ja oma Rational, muut "ufoja": def mikaMika(v: Any) { val mika = v match { case e: Int => "Int: " + e // HUOM: parametrit case e: String => "String: " + e case e: Rational => "Rationalx2 = " + (e + e) case _ => "UFO" } println(mika) } var a: Any = 77 mikaMika(a) // Int: 77 a = "miau" mikaMika(a) // String: miau a = new Rational(1, 4) mikaMika(a) // Rationalx2 = 1/2 a = 3.14 mikaMika(a) // UFO mikaMika("hauva") // String: hauva mikaMika(-321) // Int: -321 mikaMika(new Rational(123, 456 )) // Rationalx2 = 41/76 mikaMika(1<2) // UFO[Scalan varatut sanat:
abstract case catch class def do else extends false final finally for forSome if implicit import lazy match new null object override package private protected return sealed super this throw trait try true type val var while with yield _ : = => <- <: <% >: # @]
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 paarutiini { // | // | 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 // | paarutiiniTulostus:
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). Nämä eivät siis näy sort-funktion ulkopuolelle!
Huom: Scala-komentorivitulkki näennäisesti sallii tunnusten uudelleenmäärittelyn:
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 { // JONO String-arvoja 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) // 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