Helsingin yliopisto / tietojenkäsittelytieteen laitos / Ohjelmointitekniikka (Scala) / © Arto Wikla 2016

7 Ohjausrakenteita

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.

if

Ehdollisuus on tuttua. Uutuutena if-rakenne ilmaisee myös ehdollisen lausekkeen. Esimerkiksi "a:lle ei-pienempi arvoista b ja c" voidaan ohjelmoida moneen tapaan:
//-- "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

while ja do-while

Ja tuttuus jatkuu, alkuehtoisessa toistossa ei ole mitään kummallista:
  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

Jos olivatkin tuttuja ja turvallisia nuo edelliset, for-lauseen kanssa sitten joudutaankin tosi toimiin...

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)

Suodattaminen

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)

Sisäkkäiset generaattorit

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 4
Tehdää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")

Arvojonon generointi – yield

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)

try-catch

Poikkeusten käsittely Scalalla muistuttaa paljon Javan tyyliä. Scalassa ei kuitenkaan ole "tarkistettuja" (checked) poikkeuksia, joihin Javassa on pakko varautua tavalla tai toisella. Kaikki poikkeukset ovat Java-jargonilla ilmaistuna "tarkistamattomia" (unchecked). Tällaiset voi ja saa käsitellä, jos niin hyväksi katsoo. Poikkeuksia heitetään throw-ilmauksella ja niitä siepataan try-catch-rakenteella. Myös finally on haluttaessa käytettävissä.

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.

match

Scalan match-rakenne on kehittynyt versio eräiden alkeellisten kielten switch-lauseesta. Valittava asia (ei siis vain lause) valitaan try-catch-rakenteen tapaan hahmontunnistusilmauksilla "case ... => ...".

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:

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 _ : = => <- <: <% >: # @
]

Lohkot ja näkyvyys

Scala ei C-perheen kielten tapaan ole litteä. Scalassa mm. lohkot ja funktiot voivat olla sisäkkäisiä seuraavaan tapaan: Esimerkki lohkoista lohkoissa ja muuttujien peittämisestä:


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 apurutiiniB
Luennolla 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