További nyelvi elemek Scalában

Kategória: Scala.

Kivételkezelés

A kivételkezelés logikája hasonló mint a Java-ban, de itt is történt némi továbbgondolás.

Lényeges eltérés a két nyelv között az, hogy a Scala-ban nincsenek ellenőrzött (checked) kivételek. Tehát a függvények fejléceit nem kell throws-zal "összepiszkítani", vagy használni a try … catch … finally struktúrát akkor is, ha a kivétellel nem tudunk mit kezdeni.

Lássunk egy példát!

object ExceptionExample {
  class MyException1 extends Exception
  class MyException2 extends Exception

  def func(n: Int) = n match {
    case 1 => throw new MyException1
    case 2 => throw new MyException2
    case _ => n + 1
  }

  def main(args: Array[String]) = {
    try {
      println(func(1))
    } catch {
      case e1: MyException1 => e1.printStackTrace()
      case e2: MyException2 => e2.printStackTrace()
    } finally {
      println("Finally")
    }
  }
}

Látható, hogy az elkapott kivételeknél a mintaillesztés szintaxisát alkalmazzuk, egyébként megegyezik a Java-ban megszokottal.

A Scala ugyanakkor beépített struktúrákat is tartalmaz a kivételkezelés még elegánsabb kezelésére. A Try generikus absztrakt osztálynak két megvalósítás is lehetséges: a Success és a Failure; az előbbi a sikeres lefutás eredményét tartalmazza, ez utóbbi viszont a kivételt. A fenti példa némi átalakítással a következő:

import scala.util.{Failure, Success, Try}

object ExceptionExample {
  class MyException1 extends Exception
  class MyException2 extends Exception

  def func(n: Int): Try[Int] = n match {
    case 1 => throw new MyException1
    case 2 => throw new MyException2
    case _ => Success(n + 1)
  }

  def main(args: Array[String]) = {
    func(1) match {
      case Success(result) => println(result)
      case Failure(exception) => exception.printStackTrace()
    }
    println("End of program")
  }
}

Ezzel viszont van egy kis probléma: hiba esetén az End of program nem íródik ki. Szervezzük át ismét a kódot: a func() maradjon az eredeti, és hívó oldalon kezeljük a kivételt:

import scala.util.Try

object ExceptionExample {
  class MyException1 extends Exception
  class MyException2 extends Exception

  def func(n: Int) = n match {
    case 1 => throw new MyException1
    case 2 => throw new MyException2
    case _ => n + 1
  }

  def main(args: Array[String]) = {
    val result: Option[Int] = Try {
      Some(func(1))
    }.recover {
      case e: Exception =>
        e.printStackTrace()
        None
    }.get
    println(result.getOrElse("Exception occurred"))
    println("End of program")
  }
}

Ez esetben az eredmény egy Option, ami hibamentes lefutás esetén kap értéket, egyébként None, és a futás folytatódik. A recover hívással jelezzük, hogy kezelni tudjuk a kivételt.

Annotációk

Az annotációk nem változtatják meg a program logikáját, inkább jelzések a fordítónak. Az a program elem (függvény, osztály stb.) elé kell írni, amelyre a módosítást jelezni szeretnénk. Ezek a @ karakterrel kezdődnek. Paramétereik is lehetnek, zárójelben. Példák:

  • @deprecated: azt jelezzük, hogy a függvény elavult.
  • @tailrec: azt jelezzük, hogy a függvény farok-rekurzív. Ennek akkor van jelentősége, ha a későbbi karbantartás során valaki véletlenül úgy nyúl bele a kódba, hogy ne maradjon farok-rekurzív; ez esetben hibát jelez a fordító.
  • @transient: azt jelezzük, hogy egy adatmezőt nem mentünk le tartós tárolóba.
  • @volatile: azt jelzi, hogy az érték szálon kívül is megváltozhat.

A fejlesztő maga is készíthet saját annotációt, bár ez igen ritka a Scala-ban.

Implicit

A Scala sok esetben megpróbálja "kitalálni", hogy mit szeretne a programozó, ezáltal is csökkentve a kód méretét a felesleges dolgok eliminálásával.

Implicit paraméter

Egy függvénynek lehetnek implicit paraméterei, ami azt jelenti, hogy az implicitként definiált változók közül megpróbálja beilleszteni a neki megfelelő típusút. Fontos, hogy pontosan egy ilyen típusú implicit változó legyen. Egy egyszerű, de nem igazán jó példa az alábbi:

def increment(a: Int)(implicit b: Int) = a + b

implicit val defaultIncrement = 1
increment(5)(3) // 8 
increment(5) // 6

A függvény második paraméterlistája egy implicit változót vár. Ezt explicit meg is adhatjuk (increment(5)(3)), de ha kihagyjuk, akkor a defaultIncrement implicit változó lesz az alapértelmezés. Fontos, hogy a hívó oldalon keresi az implicitet, tehát ha valahonnan importáljuk a fenti increment függvényt, és ott egy másik Int típusú implicit változó van, akkor azt szúrja be implicitként.

Ami miatt a fenti példa a gyakorlatban nem igazán jó az az, hogy az implicit paraméter típusa Int, ami egész egyszerűen túl gyakori. Célszerű saját típust használni, ahol nem fordulhat elő az, hogy csak úgy talál a fordító egy implicit példányt abból a típusból

Implicit osztály

A fenti módszer "túltolása": egy osztály műveleteit úgy egészítjük ki, hogy nem nyúlunk magához az osztályhoz. Szinte minden valamirevaló méretű programban megjelenik a StringUtil osztály, ami az ottani string műveletek gyűjteménye. Igazából az lenne a legjobb, ha magát a String osztályt lehetne módosítani, de ezt nyilván nem lehetséges. Itt jön képbe az implicit osztály (ami a Scala 2.10-ben jelent meg): létrehozunk egy implicit kulcsszóval ellátott osztályt, melynek konstruktora tartalmaz egy, a kiterjesztendő osztály típusú paramétert, és benne definiálhatunk függvényeket. Majd meg tudjuk hívni úgy az objektumokon, mintha az adott osztályban definiálták volna. Pl.:

implicit class MyStringUtil(s: String) {
  def fancyFormat = "~[" + s + "]~"
}

"apple".fancyFormat

Egy másik gyakori probléma: nincs hatványozás művelet. A ** műveletet lehetne erre használni, pl. a 3 ** 4 jelenthetné azt, hogy 3 a negyediken, azaz 3*3*3*3=81. Ezt a következőképpen tudjuk megvalósítani:

implicit class MyInt(i: Int) {
  def **(j: Int): Int = j match {
    case 0 => 1
    case _ => i * **(j-1)
  }
}

3 ** 4

Ez persze nem tartalmaz hibakezelést (pl. negatív j-re nem működik, ill. nincs lekezelve a nulla a nulladikon), de látható az implicit osztályokban rejlő lehetőség.

Ennek a mintának az angol elnevezése az, hogy Pimp My Library. Ezzel kapcsolatban van pár megkötés:

  • Másik trait, object vagy class-on belül lehet csak definiálni, nem állhat legfelül.
  • Egy nem implicit paramétere lehet csak.
  • Nem ütközhet másik ugyanolyan típusú implicittel.

Implicit konverzió

Lépjünk vissza kettőt a történelemben: az erősen típusos nyelvek között is vannak a nagyon erősen típusos nyelvek, ahol minden aprósághoz explicit konverzió kell. Olyan szinten, hogy a fordító képtelen mondjuk intből shortot csinálni, vagy const-ból nem const-ot. Kezdetben a Java is ilyen volt, ami igen bosszantó felesleges kódot eredményezett. A legnyilvánvalóbb konverziókat időközben bevezették, de sok esetben így is helyben kell explicit konvertálni.

A Scala-ban itt is gondoltak egy nagyot, és bevezették az implicit konverzió fogalmát, mellyel lényegben bárhonnan bárhova tudunk konvertálni. Vegyünk egy viszonylag egyszerű példát!

case class TypeA(name: String, number: Int)
case class TypeB(number: Int, name: String)

Itt azt szeretnénk elérni, hogy a következő leforduljon:

val typeA = TypeA("apple", 5)
val typeB: TypeB = typeA

Az erősen típusos nyelvekhez szokott szemek erre azt mondanák, hogy teljes képtelenség, a Scalaban viszont ez is lehetséges, mégpedig a következő sorral, amit a fenti kettő közé helyezzünk (először tehát a típusdefiníció jöjjenek, utána az implicit konverzió, végül a használat):

import scala.language.implicitConversions
implicit def TypeAToTypeB(typeA: TypeA) = TypeB(typeA.number, typeA.name)

Konklúzió

Áttekintettük a Scala implicit konverziós lehetőségeit. Mindez igen rugalmassá teszi a kódot, ugyanakkor a személyes véleményem az, hogy érdemes ezekkel óvatosan bánni. Ha túl sok az implicit, akkor az áttekinthetetlenebbé teszi a kódot mg annál is, mintha tele lenne tűzdelve goto-val. Statikusan ugyanis igen nehéz felderíteni, hogy melyik kód fut le pontosan. Képzeljük csak el a fenti konverziót úgy, hogy az implicit valahol egészen máshol van definiálva, az adott komponensből importáljuk, a konverziós függvénynek van mellékhatása, és valamilyen hibát kell felderíteni! És tegyük még hozzá azt is, hogy több implicit konverzió van, A-ból B-be, majd B-ből C-be, akár különböző helyeken definiálva, vannak természetes konverziók (leszármazottból ősosztályba) stb. Használhatjuk a konverziókat, de csak óvatosan! Mindig gondosan mérlegeljünk a tömör kód és a karbantarthatóság között!

Reguláris kifejezések

A reguláris kifejezések és a mintaillesztés mélyen integráltan jelen van a Scala-ban, a következőképpen:

val pattern = "banana".r
pattern.findFirstIn("apple banana cherry") match {
  case Some(fruit) => fruit
  case None => "[not found]"
}

Területspecifikus nyelv

A Scala nyelv sok esetben rugalmas: pl. ha egy függvénynek egyetlen paramétere van, és objektumon keresztül hívjuk, akkor elhagyató a pont és a zárójel is. Pl. a Math.sqrt(2) és a Math sqrt 2 egyenértékű. Láthattuk az implicit konverziókat is, amikor "láthatatlan módon" konvertálja a nyelv egyik típusból a másikba a dolgokat. Mág számos egyéb nyelvi elem segíti azt, hogy "szokatlan" kódot írjunk.

A nyelvnek ez a megengedő volta teszi azt alkalmassá, hogy domén specifikus nyelvet (Domain Specific Language, DSL) definiáljunk segítségével. Vegyük példaként a következőt!

class Flight(var airline: String, var flightNumber: Int, var departure: String, var destination: String, var passengers: Int) {
  def airline(airline: String): Unit = this.airline = airline
  def flight(flightNumber: Int): Unit = this.flightNumber = flightNumber
  def departure(departure: String): Unit = this.departure = departure
  def destination(destination: String): Unit =  this.destination = destination
  def passengers(passengers: Int): Unit = this.passengers = passengers
  def is(expected: String): Unit = {
    val actual = toString()
    if (expected == actual) {
      println("Correct!")
    } else {
      println("Something went wrong!")
      println("Expected: " + expected)
      println("Actual: " + actual)
   }
  }
  override def toString() = "[" + airline + flightNumber + " " + departure + "-" + destination + " (" + passengers + ")]"
}

object DSL {
  val set = new Flight("", 0, "", "", 0)
  val create = set
  val add = set
  val display = set
  val result = set
  val LH = "LH"
  val BUD = "BUD"
  val FRA = "FRA"

  def main(args: Array[String]) = {
    set airline LH
    create flight 1340
    set departure BUD
    set destination FRA
    add passengers 50
    result is "[LH1340 BUD-FRA (50)]"
  }
}

Talán kissé "izzadtságszagú", és nem is igazán jól kidolgozott, de a végeredmény olyan, melyről nem is gondolnánk, hogy szabályos Scla program. Lássuk még egyszer, külön a lényegi részt!

set airline LH
create flight 1340
set departure BUD
set destination FRA
add passengers 50
result is "[LH1340 BUD-FRA (50)]"

Valójában nem egyszerű DSL-t írni! A Scala ugyan rugalmas, de nem mindig. Például kötelező kiírni a zárójelet, ha a hívás nem objektumon keresztül történik. Ugyanakkor ügyes trükközéssel (pl. megfelelő öröklődési struktúra, implicitek stb. segítségével) egészen meglepő dolgokra lehetünk képesek. A neten böngészve találtam az alábbi példára: https://www.scala-lang.org/old/node/1403, ahol a DSK egy BASIC nyelv. A lényeges részét ide másolom:

10 PRINT "Welcome to Baysick Lunar Lander v0.9"
20 LET ('dist := 100)
30 LET ('v := 1)
40 LET ('fuel := 1000)
50 LET ('mass := 1000)

60 PRINT "You are drifting towards the moon."

70 PRINT "You must decide how much fuel to burn."
80 PRINT "To accelerate enter a positive number"
90 PRINT "To decelerate a negative"

100 PRINT "Distance " % 'dist % "km, " % "Velocity " % 'v % "km/s, " % "Fuel " % 'fuel
110 INPUT 'burn
120 IF ABS('burn) <= 'fuel THEN 150
130 PRINT "You don't have that much fuel"

140 GOTO 100
150 LET ('v := 'v + 'burn * 10 / ('fuel + 'mass))
160 LET ('fuel := 'fuel - ABS('burn))
170 LET ('dist := 'dist - 'v)
180 IF 'dist > 0 THEN 100
190 PRINT "You have hit the surface"
200 IF 'v < 3 THEN 240
210 PRINT "Hit surface too fast (" % 'v % ")km/s"
220 PRINT "You Crashed!"

230 GOTO 250
240 PRINT "Well done"

250 END

RUN

Ez a megfelelő könyvtárat importálva szabályos, futtatható Scala kód!

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License