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)) // 7
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 = 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/3
Esimerkki 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 // |
paarutiini
Tulostus:
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 hei
Miksi 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