classon, objecten belül is lehet függvényeket vagy értékeket is deklarálni, pl:
case class Vektor(x: Double, y: Double) {
def length = Math.sqrt(this.x * this.x + this.y * this.y)
}
val v = Vektor(1.0,2.0)
println(v.length) //prints 2.23606797749979
Mi történik ennél a hívásnál?
- Ha a C osztályban egy f(x1:T1,…,xn:Tn) tagmetódusát hívjuk az a példányon (amit pl. val a =… deklaráltunk), akkor a.f(a1,…,an)hívjuk a függvényt, ahol az ai argumentumok Ti típusúak (most nincs argumentum, de nemsokára lesz)
- a függvény törzsében az xi-k helyére az ai értékek kerülnek
- és a this kulcsszó helyére pedig maga az a érték.
(matematikailag legalábbis ez történik, ha pure funkcionális stílusban programozunk.)
Pl. a fenti v.length hívásnál:
v.length▹Vektor(1.0,2.0).length▹Math.sqrt(Vektor(1.0,2.0).x⋅Vektor(1.0,2.0).x+Vektor(1.0,2.0).y⋅Vektor(1.0,2.0).y)▹Math.sqrt(1.0⋅1.0+2.0⋅0.2)▹Math.sqrt(5.0)▹2.2360679…
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:
case class Vektor(x: Double, y: Double) {
def length_def = Math.sqrt(this.x * this.x + this.y * this.y)
val length_val = Math.sqrt(this.x * this.x + this.y * this.y)
lazy val length_lazy_val = Math.sqrt(this.x * this.x + this.y * this.y)
}
val v = Vektor(1.0,2.0)
println(v.length_def) //prints 2.23606797749979
println(v.length_val) //prints 2.23606797749979
println(v.length_lazy_val) //prints 2.23606797749979
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:
Ennek megfelelően ennek a kódnak:
a kimenete:
(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:
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:
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
(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
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?
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.
- 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.
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
}
//így használjuk
val list = Nonempty(3, Nonempty(4, Nonempty(7, Empty)))
println(list.sum) // prints 14
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)) //prints Vektor(4.0,6.0)
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 ) //prints Vektor(4.0,6.0)
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
No comments:
Post a Comment