Vezérlő szerkezetek Scalában

Kategória: Scala.

Vezérlő szerkezetek alatt a különféle feltételkezeléseket és ciklusokat értem. A Scala-ban a klasszikus megoldásokon túl ezt is továbbgondolták.

Feltételkezelés

A feltételkezelés a Scala-ban az if…else struktúrával történik. A következő példában tegyük fel, hogy az i egész. Ekkor nem okoz meglepetést a következő program:

if (i % 2 == 1) {
  println("páratlan")
} else {
  println("páros")
}

Ezek tetszőlegesen egymásba ágyazhatóak, az else-t további if követheti, ahogy szinte minden más nyelvben. Viszont az if-nek is van a Scala-ban visszatérési értéke, mégpedig minden ágon az utoljára kiszámított érték. A visszatérési típust sem kell megadnunk, ugyanis a fordító automatikusan meghatározza: a legközelebbi közös ős lesz a típus. Az előző példát továbbgondolva, meglepő, de működik az alábbi:

println(if (i % 2 == 1) "páratlan" else "páros")

ill. parancssorból

if (i % 2 == 1) "páratlan" else "páros"

Az if…else eredményének típusa String, amit átadunk a println eljárásnak paraméterként. A fenti 5 sort egyetlen sorba sűrítettük úgy, hogy a kód olvashatósága megmaradt.

Mintaillesztés

Ha sokféle feltétel lehetséges, akkor nehezen olvashatóvá válik a kód abban az esetben, ha azt egy hatalmas if … else if … else struktúrába helyezzük. A programozási nyelvek többsége erre találta ki a switch … case … default szerkezetet. Ez a Scala-ban is meg van, de egészen más a szintaxisa, és sokkal rugalmasabb, mint a legtöbb programozási nyelv hasonló struktúrája. Érdemes jól megjegyezni, mivel ez az egyik leggyakrabban használt olyan nyelvi elem a Scala-ban, ami leginkább erre a nyelvre jellemző.

Lássunk egy egyszerű példát: amit vizsgálunk, az egy szám, és kiírjuk a neki megfelelő sorszámú napot.

val nap = 3
nap match {
  case 1 => println("hétfő")
  case 2 => println("kedd")
  case 3 => println("szerda")
  case 4 => println("csütörtök")
  case 5 => println("péntek")
  case 6 => println("szombat")
  case 7 => println("vasárnap")
  case _ => println("hiba")
}

Egyszerű, mint a faék. Felesleges break utasítások nincsenek, mivel úgyis csak egy ágat szeretnénk végrehajtani, ráadásul nincs is break a Scala-ban. A default-ra külön nincs szó, helyette a _ joker karaktert használhatjuk.

Bonyolítsuk kicsit a dolgot! A match-nek is van visszatérési értéke, az if-hez hasonlóan, így valójában elég a println-t egyszer kiírni, ami kiírja az egész mintaillesztés eredményét, így:

val nap = 3
println(nap match {
  case 1 => "hétfő"
  case 2 => "kedd"
  case 3 => "szerda"
  case 4 => "csütörtök"
  case 5 => "péntek"
  case 6 => "szombat"
  case 7 => "vasárnap"
})

A Java-ban megszokott kötöttségeket viszont a Scalaban el is felejthetjük. Emlékeztetőül: a Java-ban igen kevés dologra lehet ráhúzni a switch … case … default szerkezetet, kezdetben csak számokra és felsorolás típusra lehetett, és csak a 7-es Java-ban jelent meg a Stringre illesztés lehetősége. A Scala-ban a lehetősége tárháza szinte korlátlan. Case osztályokra is tudunk szűrni, konkrét értékkel (nem case osztályokra nem), sőt, további feltételeket adhatunk meg. Lássunk erre is egy példát (kissé előreszaladva, ugyanis az osztályokról még nem volt szó):

abstract class Sikidom
case class Teglalap(a: Int, b: Int) extends Sikidom
case class Kor(r: Int) extends Sikidom

val s: Sikidom = new Teglalap(4, 4)
"A síkidom egy " + (s match {
  case Teglalap(1, 1)           => "egység oldalú négyzet"
  case Teglalap(x, y) if x == y => "négyzet"
  case Teglalap(_, _)           => "téglalap"
  case Kor(1)                   => "egységsugarú kör"
  case Kor(_)                   => "kör"
})

A program első 3 sorában osztályokat definiálunk. Erről részletesen még lesz szó a későbbiekben; elöljáróban annyit, hogy a case class olyan, mintha a Javaban olyan osztályt készítenénk, amelynek privát attribútumai, hozzájuk tartozó getterek, megfelelő konstruktor és összehasonlító függvényei lennének; itt ez a felesleges kód nem látszódik, hanem a háttérben legenerálódik. Most viszont nem is ez a lényeg, hanem az az alatti rész: láthatjuk, hogy osztályokat is használhatunk a mintaillesztésnél, megadhatunk konkrét értékeket (a példában kétszer is szerepel az egység), plusz feltételeket és joker karaktert is. A fordító nem kényszeríti ki azt, hogy minden ágat lefedjünk.

A fenti kódot nem magyarázom túl, elég egyértelmű szerintem, hogy mit csinál. Képzeljük el, hogy a Java-ban hogyan valósítanánk meg a fenti programot, melynek a része egyetlen utasítás! Már eleve a definiált osztályok minden függvénnyel egész oldalakat foglalnának el, és a példa érdemi része is jó eséllyel kilógna egy teljes oldalról, ha mindent tisztességgel lefejlesztünk. Ha a Scalában ügyesen alkalmazzuk a mintaillesztést, akkor egy jól olvasható és tömör kódot kapunk. A lehetőségek tárháza pedig - amint láthattuk - szinte végtelen.

A while ciklus

A Scala-ban kétféle while ciklus van: elöltesztelős (while) és hátultesztelős (do … while). Ez utóbbi tehát úgymond "visszakerült" a nyelvbe; a Java elődjének számító C/C++-ban ugyanis meg volt, a Java-ból kikerült, de a Scala-ba belekerült. A megszokott módon működik mindkettő:

var i = 1
while (i < 5) {
  println(i)
  i += 1
}
var j = 1
do {
  println(j)
  j += 1
} while (j < 5)

Viszonylag ritkán használjuk a Scala-ban.

A for ciklus

A while ciklussal ellentétben a for ciklust igencsak tovább gondolták a Scala-ban.

Az alap for ciklus

A többi programozási nyelvben megszokott for (i = 1; i < 5; i++) … jellegű szintaxis Scala-ban nincs. A Scala alapszintaxis a következő: for (elem <- collection) …. Például:

for (i <- List(3, 5, 7, 8)) {
  println(i)
}

A for ciklusoknál gyakori az, hogy egyesével lépkedünk a számokon; erre hozták létre az a to b struktúrát, ami valójában egy Range típusú gyűjteményt hoz létre:

for (i <- 1 to 5) {
  println(i)
}

Ez kiírja a számokat 1-től 5-ig.

A programozók megszokták, hogy a ciklusnál az első elemet kiírja, az utolsót nem (ld. a bevezető példát, ahol az i megy 1-től 5-ig, de az 5-öt már nem írja ki); a Scala-ban erre az a until b struktúrát használhatjuk (ami ugyancsak egy Range típusú gyűjteményt generál):

for (i <- 1 until 5) {
  println(i)
}

A to és until utasításokat generátoroknak hívjuk.

Idáig nem is tűnik annyira komplikáltnak…

for ciklus több változóval

Kezdjük bonyolítani! Egy cikluson belül több ciklusváltozót is használhatunk, ami egymásba ágyazott ciklust eredményez, pl.:

for (i <- 1 to 3; j <- 1 to 4) {
  println(i + " " + j)
}

A példában a külső ciklusmag az i, a belső a j, az eredmény sorrendje tehát ez: 1 1, 1 2, 1 3, 1 4, 2 1, …

A kapcsos zárójeles szintaxis

A for ciklusban a kerek zárójel helyett használhatunk kapcsos zárójelet is, és ez esetben az egymásba ágyazott ciklus változóit külön sorba írhajtuk, és a pontosvessző is elhagyható, pl.:

for {i <- 1 to 3
  j <- 1 to 4} println(i + " " + j)

Feltételkezelés for cikluson belül

A for ciklus tartalmazhat feltételt, pl.

for (i <- 1 to 3; j <- 1 to 4; if i + j < 6) {
  println(i + " " + j)
}

A példában csak azokat az i j párokat írja ki, ahol az összes kisebb mint 6, tehát pl. a 2 4-et nem. Ez kb. ekvivalens azzal, mintha a cikluson belül nyitnánk egy if-et, de a most bemutatott szintaxis tömörebb kódot eredményez.

A yield

Gyakran előfordul, hogy a ciklus során egy újabb struktúrát hozunk létre, Ha pl. a Java-ban létre szeretnénk hozni egy újabb gyűjteményt, ami az eredeti gyűjteményben található értékek kétszeresét tartalmazza, akkor létrehozunk egy újat, majd a ciklusmagban egyesével hozzáadjuk. A Scala-ban ennek leegyszerűsítésére hozták létre a yield mechanizmust:

for (i <- List(4, 2, 3)) yield i * 2

A példában egy olyan Listát kapunk eredményül, ami az eredeti lista elemeinek a kétszeresét tartalmazza. Látható, mennyire tömör a kód.

Ha bonyolultabb műveletet szeretnénk végrehajtani, vagy a ciklus során mást is szeretnénk tenni a ciklusváltozóval (pl. kiíratni), akkor a szintaxis az alábbi:

for (i <- List(4, 2, 3)) yield {
  println(i)
  i * 2
}

Tehát a yield utáni részt tesszük kapcsos zárójelbe. A for és a yield közé helyezett kapcsos zárójel szintaktikai hibát okoz.

A ciklus megszakítása

A Scala nyelvben hiányzik a break és a continue. Ne feledjük, hogy Basic-ből örökölt, és a Pascal-on, valamint a C/C++-ban is jelen levő goto utasítást, aminek a használata nehezen olvasható kódot eredményezett, száműzték a Java nyelvből. Megmaradt viszont kissé strukturáltabb ugró utasításként a break és a continue. Valójában ezek is olyan ugró utasítások, amelyek nehezen áttekinthetővé teszik a kódot, különösen ha hosszú a ciklus, mély if egymásba ágyazások vannak; első ránézésre gyakran nem könnyű megállapítani, hogy most az a break vagy continue hova ugrik. Így ezeknek az utasításoknak a száműzésére a már megkezdett folyamat újabb állomásaként tekinthetünk.

De mit tegyünk akkor, ha szükségünk van ezekre az utasításokra?

break

Tegyük fel, hogy van egy lista, melyben meg szeretnénk keresni egy elemet! Lássuk a kezdeti megvalósítást!

var found = false
for (i <- List(4, 5, 2, 3)) {
  println(i)
  if (i == 5) found = true
}
println(found)

A probléma ezzel a hatékonyság: noha már a második lépésben meg van az elem, így a megoldás is, még két további elemet vizsgálunk meg feleslegesen. Használhatjuk viszont a ciklus feltételkezelését:

var found = false
for (i <- List(4, 5, 2, 3); if !found) {
  println(i)
  if (i == 5) found = true
}
println(found)

Hasonlóan a while ciklus esetén is vissza tudjuk vezetni egy feltételkezelésre a break utasítást.

continue

A continue utasítást is vissza tudjuk vezetni egyéb struktúrákra, de az már kissé nyakatekertebb. Tegyük fel, hogy a következőt szeretnénk megvalósítani:

for (i <- List(4, 5, 2, 3)) {
  println("1 " + i)
  if (i == 5) continue // syntax error
  println("2 " + i)
}

Ez szintaktikailag hibás a Scala-ban. A megoldás a következő: vegyük a continue előtti feltétel fordítottját, és az utána következő utasításokat pedig tegyük kapcsos zárójelbe:

for (i <- List(4, 5, 2, 3)) {
  println("1 " + i)
  if (!(i == 5)) {
    println("2 " + i)
  }
}

A megoldás nem túl elegáns, de működik, és azt is tegyük hozzá, hogy a continue elég ritka utasítás a programokban.

Végül azok számára, akik nem tudnak élni a break nélkül, a breakable struktúrával mégis hagytak egy "menekülő útvonalat":

import util.control.Breaks._
var found = false
breakable {
  for (i <- List(4, 5, 2, 3)) {
    println(i)
    if (i == 5) {
      found = true
      break
    }
  }
}
println(found)
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License