(matematikailag legalábbis ez történik, ha pure funkcionális stílusban programozunk.)
Pl. a fenti v.length hívásnál:
Az előző kód példája egy paraméter nélküli metódus volt. Ilyenkor a Scala fordító elfogadja zárójelekkel is def length()=… és mint fent, anélkül is. A konvenció az, hogy ilyenkor azokat a metódusokat írjuk zárójel nélkül, melyeknek nincs mellékhatásuk. A fenti length például ilyen, csak az adattagokból számít ki valamit. Ha megnézzük a hívást, v.length, szemre akár egy val member adatmező is lehetne. És tényleg:
Nem csak a már korábban látott
val és
def, hanem a
lazy val is opció. A különbségek köztük:
- a val értéke az objektum létrehozásakor (ez a val v=Vektor(1.0,2.0) értékadás jobb oldali kifejezésének kiértékelésekor történik) kiszámítódik, ez az érték bekerül plusz egy adattagba, innentől kezdve ezt adjuk vissza.
- a def értéke minden egyes lekérdezéskor újra és újra kiértékelődik a definíciójának megfelelően, viszont nem foglalódik neki plusz adattag objektumpéldányonként.
- a lazy valnak foglalódik egy plusz adattag, de létrehozáskor még nem értékelődik ki, csak az első lekérdezéskor. Ekkor az érték bekerül az adattagba és onnantól kezdve nem számolódik újra.
Tehát: a
valok kicsit több helyet foglalnak, a
val mindenképp kiszámolja létrehozáskor az értékét, a
def lekérdezésenként tovább tart, mire megjön az eredmény. A
lazy val sem mindig jobb választás, mint a
val, mert annak is van plusz költsége (adattagban is, és a
val lekérdezéseként is), hogy minden lekérdezéskor fut egy check, hogy ki van-e már számolva ez az érték. Ezt minden esetben a programozó kell mérlegelje, hogy a három lehetőség közül melyik lenne a legjobb - de a jó hír, hogy ha később mégiscsak váltani lenne jobb és átírja az osztályban az egyik kulcsszót a másikra, akkor nem töri el a hívó kódokat, mert számukra teljesen transzparens, hogy ez most épp melyik a három közül.
Ennek megfelelően ennek a kódnak:
case class Vektor(x: Double, y: Double) {
def length_def = { println("def"); Math.sqrt(this.x * this.x + this.y * this.y) }
val length_val = { println("val"); Math.sqrt(this.x * this.x + this.y * this.y) }
lazy val length_lazy_val = { println("lazy_val"); Math.sqrt(this.x * this.x + this.y * this.y) }
}
val v = Vektor(1.0,2.0)
println("start")
v.length_def
v.length_val
v.length_lazy_val
v.length_def
v.length_val
v.length_lazy_val
v.length_def
v.length_val
v.length_lazy_val
a kimenete:
val
start
def
lazy_val
def
def
(Figyeljük meg a debug printlnt: így, hogy két kifejezés van egymás után, a másodiknak az értéke lesz az érték, az elsőt eldobjuk - ami úgyis
Unit, tehát
() -, ez is egy módja a debugolásnak, de fogunk azért látni jobbat is.
Nem csak classba vagy objectbe, de traitbe is írhatunk defet vagy valt (lazy valt nem). Ha például szeretnénk az
IntList traitünket felruházni egy
sum függvénnyel, ami visszaadja az elemei összegét:
trait IntList {
def sum: Int
}
case object Empty extends IntList {
override def sum = 0
}
case class Nonempty extends IntList {
override def sum = head + tail.sum
}
val list = Nonempty(3, Nonempty(4, Nonempty(7, Empty)))
println(list.sum)
Világos, hogy ahhoz, hogy egy listára, amiről annyit tudunk, hogy
IntList a típusa, hívhassuk a
sum függvényt, és ez leforduljon, tényleg kellhet az, hogy már eleve az
IntList traitben meglegyen a függvény deklarációja. Típust is kell adjunk neki, de definíciót nem tudunk adni ezen a ponton - szerencsére nem is kell, traitekben megengedett dolog csak deklarálni, de nem definiálni a függvényt.
A case objectben és a case classban viszont, ha akarunk is belőlük példányt létrehozni, minden létező metódusnak kell már legyen definíciója - meg kell adjuk a
sum függvényt mindkét esetben. Célszerű ilyenkor a def elé beírni az
override kulcsszót is: ez a kulcsszó azt jelzi a fordítónak, hogy legyen szíves leellenőrizni, hogy ugye volt már egy pont ilyen nevű és szignatúrájú metódus feljebb a hierarchiában (az
extendseken keresztül) és ha nem, akkor dobjon hibát. Ezzel magunkat védjük: egyrészt ha elgépeljük a fentebbről
örökölt (inherited) metódus nevét, akkor erre hamar fény derül, másrészt ha majd mások olvassák a kódunkat, ebből ránézésre látják majd ő is, hogy ebből a metódusból van följebb is másmilyen.
Persze nem csak paraméter nélküli metódust lehet létrehozni:
case class Vektor(x: Double, y: Double) {
def plus(that: Vektor) = Vektor(this.x+that.x, this.y+that.y)
}
val u = Vektor(1.0, 2.0)
val v = Vektor(3.0, 4.0)
println(u.plus(v))
Itt tehát az történik, hogy a
Vektor osztályunknak (amihez korábban az osztályon kívül deklaráltunk egy összeadó függvényt és kb
add(v1:Vektor,v2:Vektor):Vektor volt a fejléce, így is hívtuk meg)
belülre deklarálunk egy összeadó metódust, aki így már csak egy további vektort vár, hiszen a bal oldali vektor az lesz, akin hívjuk, aki behelyettesítődik a
this helyére. (Egyébként Scalában bevett konvenciónak számít, hogy ha egy osztálynak egy függvénye egyváltozós, és ugyanannak az osztálynak várja egy másik példányát, akkor ezt a formális paramétert
thatnak nevezik el. Nem kötelező, a
that nem kulcsszó, csak szokás.) Ez már talán eggyel kulturáltabban néz ki, hogy az
add függvény a két vektor
között van, mintha tényleg pl egy bináris operátor lenne...
...de bizony ezt lehet hívni így is
println( u plus(v) )
(a pont sok esetben elhagyható, ennek a konvencióiról azért még majd írok, van, aki szerint olvashatóbb a kód pontokkal, van, aki szerint nem; szerintem ha hosszú chainelés van, akkor külön sorba érdemes írni a lánc elemeit és ekkor kell is kirakni a pontot, ha meg rövid, én nem szoktam kirakni általában)
..sőt lehet hívni így is
println( u plus v )
egyváltozós member function argumentuma körül a kerek zárójel is legtöbbször elhagyható (olyankor lehet belőle értelmezési zavar, ha pl. tovább folytatjuk a kifejezést egy ponttal és pont egy olyan függvényt, mondjuk
toStringet hívunk rajta, ami van az argumentumnak is és az eredménynek is).
ez így már majdnem annyira kényelmesen néz ki, mint egy rendes, infix módon írt összeadás.
És hát why not?
case class Vektor(x: Double, y: Double) {
def +(that: Vektor) = Vektor(this.x+that.x, this.y+that.y)
}
val u = Vektor(1.0, 2.0)
val v = Vektor(3.0, 4.0)
println( u + v )
Scalában nagyon, nagyon sok minden elmegy metódusnévnek, itt például a metódus neve az lett, hogy
+. És így már két vektor összeadása úgy is néz ki, mint két
Int összeadása, egy nagyon természetes szintaxist ad a függvényhívásnak, könnyen olvashatót programozó számára.
Nem véletlen egyébként a hasonlóság: a
2+3-at úgy is írhatjuk, hogy
2.+(3), mert a
+ jel ebben az esetben az
Int osztály (azaz majdnem az
Int osztály but still) egyik tagfüggvényének a neve. Persze ettől még primitív típusra fog lefordulni, hatékony lesz, csak forráskódi szinten, kinézetre kezelődik minden úgy, mintha objektum lenne, ideértve a "built-in" operátorokat is.
Legközelebb nézünk néhány standard tagfüggvényt, fókuszálva a
List osztályra.
folytköv