$\mathrm{class}$on, $\mathrm{object}$en 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(x_1:T_1,\ldots,x_n:T_n)$ tagmetódusát hívjuk az $a$ példányon (amit pl. $\mathrm{val}~a~=\ldots$ deklaráltunk), akkor \[a.f(a_1,\ldots,a_n)\] hívjuk a függvényt, ahol az $a_i$ argumentumok $T_i$ típusúak (most nincs argumentum, de nemsokára lesz)
- a függvény törzsében az $x_i$-k helyére az $a_i$ értékek kerülnek
- és a $\mathrm{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 $\mathrm{v.length}$ hívásnál:
\[\begin{align*}\mathrm{v.length}&\triangleright \mathrm{Vektor}(1.0,2.0)\mathrm{.length}\\&\triangleright \mathrm{Math.sqrt}(\mathrm{Vektor}(1.0,2.0).x\cdot \mathrm{Vektor}(1.0,2.0).x+\mathrm{Vektor}(1.0,2.0).y\cdot\mathrm{Vektor}(1.0,2.0).y)\\&\triangleright \mathrm{Math.sqrt}(1.0\cdot 1.0+2.0\cdot 0.2)\\&\triangleright\mathrm{Math.sqrt}(5.0)\\&\triangleright 2.2360679\ldots\end{align*}\]
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 $\mathrm{def~length}() = \ldots$ é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 $\mathrm{length}$ például ilyen, csak az adattagokból számít ki valamit. Ha megnézzük a hívást, $\textrm{v.length}$, szemre akár egy $\mathrm{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 $\mathrm{val}$ és $\mathrm{def}$, hanem a $\mathrm{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 $\mathrm{Unit}$, tehát $\mathrm{()}$ -, 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 $\mathrm{IntList}$ traitünket felruházni egy $\mathrm{sum}$ függvénnyel, ami visszaadja az elemei összegét:
Világos, hogy ahhoz, hogy egy listára, amiről annyit tudunk, hogy $\mathrm{IntList}$ a típusa, hívhassuk a $\mathrm{sum}$ függvényt, és ez leforduljon, tényleg kellhet az, hogy már eleve az $\mathrm{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 $\mathrm{sum}$ függvényt mindkét esetben. Célszerű ilyenkor a def elé beírni az $\mathrm{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 $\mathrm{extends}$eken 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 $\mathrm{Vektor}$ osztályunknak (amihez korábban az osztályon kívül deklaráltunk egy összeadó függvényt és kb $\mathrm{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 $\mathrm{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 $\mathrm{that}$nak nevezik el. Nem kötelező, a $\mathrm{that}$ nem kulcsszó, csak szokás.) Ez már talán eggyel kulturáltabban néz ki, hogy az $\mathrm{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 $\mathrm{toString}$et 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 $\mathrm{+}$. És így már két vektor összeadása úgy is néz ki, mint két $\mathrm{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 $\mathrm{Int}$ osztály (azaz majdnem az $\mathrm{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 $\mathrm{List}$ osztályra.
- a $\mathrm{val}$ értéke az objektum létrehozásakor (ez a $\mathrm{val}~v=\mathrm{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 $\mathrm{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 $\mathrm{lazy~val}$nak 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 $\mathrm{Unit}$, tehát $\mathrm{()}$ -, 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 $\mathrm{IntList}$ traitünket felruházni egy $\mathrm{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 $\mathrm{IntList}$ a típusa, hívhassuk a $\mathrm{sum}$ függvényt, és ez leforduljon, tényleg kellhet az, hogy már eleve az $\mathrm{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 $\mathrm{sum}$ függvényt mindkét esetben. Célszerű ilyenkor a def elé beírni az $\mathrm{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 $\mathrm{extends}$eken 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 $\mathrm{Vektor}$ osztályunknak (amihez korábban az osztályon kívül deklaráltunk egy összeadó függvényt és kb $\mathrm{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 $\mathrm{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 $\mathrm{that}$nak nevezik el. Nem kötelező, a $\mathrm{that}$ nem kulcsszó, csak szokás.) Ez már talán eggyel kulturáltabban néz ki, hogy az $\mathrm{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 $\mathrm{toString}$et 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 $\mathrm{+}$. És így már két vektor összeadása úgy is néz ki, mint két $\mathrm{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 $\mathrm{Int}$ osztály (azaz majdnem az $\mathrm{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 $\mathrm{List}$ osztályra.
folytköv
No comments:
Post a Comment