A Scala programozási nyelv

Generikus típusok és metódusok

Példa

A Scala-ban az osztályoknak lehetnek típus paramétereik. Ezt az iterátorok segítségével mutatjuk be:

abstract class Iterator[a] with { def hasNext: Boolean; def next: a; }

A next metódus a következő elemet adja vissza, a hasNext pedig azt, hogy van-e következő elem. Egy iterátor által visszaadott elemek típusa tetszőleges lehet. Ezt azzal fejeztük ki, hogy az iterátornak lehet egy a típus paramétere.
A foreach metódus egy eljárást alkalmaz az iterátorra:

def foreach(f: (a)Unit): Unit = while (hasNext) { f(next) }

Az append metódus lényegében összefűzi két iterátor által megadott elemek listáját:

def append(that: Iterator[a]): Iterator[a] = new Iterator[a] with { def hasNext = outer.hasNext || that.hasNext; def next = if (outer.hasNext) outer.next else that.next; }

Az outer.next és outer.hasNext kifejezések az append-ben a megfelelő metódusokat hívják, vagyis azokat melyek a tartalmazó Iterator osztályban lettek definiálva. Általában az outer prefix egy kiválasztásban azt jelenti, hogy az aktuális osztályon vagy sablonon kívül közvetlenül látható azonosítóra hivatkozunk. Ha az outer nem lenne, akkor rekurzív hívást tennénk.
A filter metódus létrehoz egy iterátort, mely az összes az eredeti iterátorban levő azon elemeket adja vissza, melyekre a p kritérium igaz:

def filter(p: (a)Boolean) = new Iterator[a] with { private class Cell[T](elem_: T) with { def elem = elem_; } private var head: Cell[a] = null; private var isAhead = False; def hasNext: Boolean = if (isAhead) True else if (outer.hasNext) { head = Cell(outer.next); isAhead = p(head.elem); hasNext } else False; def next: a = if (hasNext) { isAhead = False; head.elem; } else error("next on empty iterator"); }

A map metódus az eredeti iterátor összes elemére alkalmazza az f függvényt és az eredményeket helyezi egy iterátorba:

def map[b](f: (a)b) = new Iterator[b] with { def hasNext: Boolean = outer.hasNext; def next: b = f(outer.next); }

Az f függvény által visszaadott érték tetszőleges típusú lehet. Ezért a map egy polimorfikus függvény.
A zip metódus egy olyan iterátort ad vissza, mely két iterátor elemeiből álló párokat tartalmaz:

def zip[b](that: Iterator[b]) = new Iterator[(a, b)] with { def hasNext = outer.hasNext && that.hasNext; def next = (outer.next, that.next); }

A konkrét iterátoroknak lényegében csak a next és hasNext metódusokat kell megvalósítaniuk. A legegyszerűbb iterátor az EmptyIterator:

class EmptyIterator[a] extends Iterator[a] with { def hasNext = False; def next: a = error("next on empty iterator"); }

Egy érdekesebb iterátor az arrayIterator. Ez egy tömb elemeit adja vissza:

def arrayIterator[a](xs: Array[a]) = new Iterator[a] with { private var i = 0; def hasNext: Boolean = i < xs.length; def next: a = if (i < xs.length) { val x = xs(i); i = i + 1; x } else error("next on empty iterator"); }

Egy másik iterátor egy intervallum elemeit adja vissza:

def range(lo: Int, hi: Int) = new Iterator[Int] with { private var i = lo; def hasNext: Boolean = i &lgt; hi; def next: Int = if (i &lgt; hi) { i = i + 1; i - 1; } else error("next on empty iterator); }

Lehetőség van olyan iterátor létrehozására is, amely sohasem terminál:

def from(start: Int) = new Iterator[Int] with { private var last = start - 1; def hasNext = True; def next = { last = last + 1; last } }

Az iterátorok használhatóak a következőképpen:

arrayIterator[Int](xs) foreach (x => System.out.println(x))

Ez egy egész elemeket tartalmazó tömb összes elemét kiírja.
A fenti példában az [Int] típus argumentum redundáns, mert ezt az xs-ből a fordító ki tudja következtetni, ha az egészek tömbje. Ezért írhatjuk a következőt:

arrayIterator(xs) foreach (x => System.out.println(x))

Family polimorfizmus

Scalaban lehetőség van egymással összefüggő típusok családjának a modellezésére. Gyakran szükségünk lehet rá, hogy ne csak osztályok típusparamétereire tehessünk megszorításokat, hanem esetleg az egyes osztályok típusparaméterei közötti összefüggést is leírhassuk. Ez azt jelenti, hogy osztályok egy csoportját szeretnénk együttesen kezelni egy magasabb absztrakciós szinten. Ezen a magas szinten szeretnénk megadni bizonyos kapcsolataikat, majd egy alacsonyabb szinten lehetőséget kapni arra, hogy megadjuk a kombinációit azoknak az osztályokat, amik között az őseik közötti kapcsolatot megengedjük. Ezzel több osztály esetén azt biztosítjuk, hogy az ősosztályoknak csak az általunk megadott kombinációkban állhatnak a leszármazottjai kapcsolatban egymással. A fenti nyelvi elemre például akkor lehet szükségünk, ha egy design patternt szeretnénk könyvtári eszközökkel támogatni. A jobb érthetőség miatt a továbbiakban egy példán mutatom be az előbb leírtakat. Képzeljük el, hogy az observer design patternt szeretnénk könyvtári eszközökkel támogatni. Azokban a nyelvekben amelyekben nincs family polimorfizmus, nem lehetséges a könyvtári támogatás, mert nélküle nem tudjuk garantálni, hogy ha több leszármazottja van a subject és az observer osztályoknak a programunkban (a magasabb absztrakciós szinten megadott ősosztályokból származtatva), akkor csak a megfelelő subjectből származtatott osztály egy objektumához tudjuk hozzácsatolni egy megfelelő observer osztályból származtatott osztály egy objektumát. Scalaban az observer design pattern kódja a következőképpen nézne ki:

abstract class SubjectObserver { type S <: Subject type O <: Observer abstract class Subject requires S { private var observers: List[O] = List() def subscribe(obs: O) = observers = obs :: observers def publish = for (val obs < observers) obs.notify(this) } trait Observer { def notify(sub: S): unit } }

A SubjectObserver absztrakt osztály két tagosztályt tartalmaz. A Subject absztrakt osztályban lévő subscribe metódus annyit csinál, hogy regisztrál egy observert a szükség esetén értesítendő observerekhez. Az előbb említett értesítést a publish metódus végzi az Observer osztályban definiált notify metóduson keresztül. Általában a publish metódus akkor hívandó meg, amikor valamilyen változás történik az objektum állapotában. A valóságban egy subject egyszerre több observert is értesíthet és egy observer egyszerre több subjectet is figyelhet, ezért szükséges a Subject osztály subscribe metódusának szignatúrájában az observerre és az Observer osztály notify metódusában a subjectre hivatkozni, azoban lehetőséget kell hagyni arra, hogy ezeknek a paramétereknek a típusát a későbbiekben meghatározhassuk. Ezért van szükség az S és az O típus definíciójára az első két sorban. Itt annyi kikötést teszünk, hogy amikor majd ezeket a típusokat konkrétan megadjuk az S-nek a Subject gyerekének, míg O-nak az Observer gyerekének kell lennie. A Subject osztály definíciójában a requires kulcsszó azt fejezi ki, hogy az osztályt példányosítani csak a SubjectObserverrel együtt lehet, az S megadása után. Ennek oka, hogy amikor a publish metódusban az observer notify metódusa meghívásra kerül, a fordítónak el kell tudnia dönteni, hogy a this megfelel-e az S típusnak. Miután elkészítettük a fenti osztályt, lehetőségünk van alkalmazás specifikus subject-observer osztályok létrehozására:

object SensorReader extends SubjectObserver { type S = Sensor type O = Display abstract class Sensor extends Subject { val label: String var value: double = 0.0 def changeValue(v: double) = { value = v publish } } class Display extends Observer { def println(s: String) = ... def notify(sub: Sensor) = println(sub.label + " has value " + sub.value) } }

A példában a subject a Sensor és az observer a Display osztályok lettek. A subjectnek és az observernek megfelelő osztályokat mindig kötelező megadni. Mivel a Sensor egy absztrakt osztály, a fenti kód használatához definiálnunk kell egy konkrét osztályt ami a Sensor altípusa.

class AbsSensorReader extends SubjectObserver { type S <: Sensor type O <: Display ... }

Itt megjelenik, hogy az S és az O típusnak a Sensor és a Display altípusának kell lennie. A fenti kód használata a következő képen történhet:

object Test { import SensorReader._ val s1 = new Sensor { val label = "sensor1" } val s2 = new Sensor { val label = "sensor2" } def main(args: Array[String]) = { val d1 = new Display; val d2 = new Display s1.subscribe(d1); s1.subscribe(d2) s2.subscribe(d1) s1.changeValue(2); s2.changeValue(3) } }

Amit nyertünk a family polimorfizmussal így már pontosan látszik. Új subject-observer osztályok bevezetése esetén a SubjectObserver osztályban definiált mechanizmusokat nem kell újraimplementálnunk, ráadásul a nem egymáshoz illő subject-observer párokat nem kapcsolhatjuk össze, mivel amennyiben mégis véletlenül megpróbálnánk összekapcsolni őket, a fordító még fordítási időben képes lesz jelezni a típushibát, ellentétben azokkal a nyelvekkel ahol nincsen family polimorfizmus, mivel ott egy hasonló konstrukció estén hiányoznának a fordítóprogram számára a jelenlegi példában az S-ben és az O-ban megadott típusok.

Tuple

Van, amikor egy függvény több értékkel szeretne visszatérni. Például a következő függvény esetében:

case class TwoInts(first: Int, second: Int) def divmod(x: Int, y: Int): TwoInts = new TwoInts(x / y, x % y)

A függvény két egész szám osztásakor keletkező hányadost és maradékot szeretné visszaadni. Ehhez definiálja a TwoInts case osztályt. Mivel ezt minden alkalommal megtenni rendkívül körülményes, azért hozták létre a nyelvben a Tuple típust. Az alábbi példa mutatja, hogy használhattuk volna a Tuple2-őt az eredeti metódusban a TwoInts helyett:

def divmod(x: Int, y: Int) = new Tuple2[Int, Int](x / y, x % y)

A Tuple2 definíciója a következő képen nézne ki, ha nekünk kellene implementálni:

package scala case class Tuple2[A, B](_1: A, _2: B)

A nyelv specifikációja szerint 22-ig van generikus Tuple case osztály definiálva, azaz akár 22 visszatérési értékkel rendelkező függvényt is írhatunk. Miután tudjuk, hogyan kell több értékkel visszatérni, már csak az a kérdés maradt, hogy nyerhetjük ki a Tuple-ből az értékeket. Erre egy lehetséges mód, a változó neve után a „._i”-vel történő elérés, ahol i helyére azt a számot kell írni, ahányadik visszatérési paramétert szeretnénk elérni. Például:

val xy = divmod(x, y) println("quotient: " + xy._1 + ", rest: " + xy._2)

A példában a függvény által első visszaadott értékre az „xy._1”-el, míg a második visszaadott értékre „xy._2”-vel hivatkozhatunk.