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

7 Ohjausrakenteita

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.

if

Ehdollisuus on tuttua. Uutuutena if-rakenne ilmaisee myös ehdollisen lausekkeen. Esimerkiksi "a:lle ei-pienempi arvoista b ja c" voidaan ohjelmoida kahdella tyylillä
//--"klassisesti":
  if (b < c)
     a = c
  else
     a = b
//--ehdollinen lauseke:
  a =  if (b < c) c else b
Koska 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

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
Yksi 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

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)

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   

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 .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 19
Huom: 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)

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

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ä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... ;-)

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ä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)  // 1
Eli 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"... ;-)

match

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

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:

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)

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 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äärutiini
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).

"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 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 {      // 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