Progalapból is volt több módszer arra, hogy "egybe tartozó adatokat" összepakoljunk egyetlen adattípusba, classic példa lehet erre mondjuk egy Pont típus, ami a síkon rendelkezik egy x és egy y koordinátával. Az egyszerűség kedvéért most csak egész koordinátákkal foglalkozunk.
C-ben valahogy így deklarálhatunk és használhatunk egy Pont típust:
typedef struct {
int x;
int y;
} Pont;
// létrehozás
Pont p;
// mezők beállítása
p.x = 1;
p.y = 3;
// függetlenek
printf( "(%d,%d)\n", p.x, p.y ); //prints (1,3)
Tehát, az összetett adattípusnak, a structnak, itt két mezője van. Matematikailag ha egy T1 típus értéktartománya a D1 halmaz, a T2 típusé meg a D2 halmaz, akkor a T1×T2 szorzattípusé, amit C-ben a struct kulcsszóval hozunk létre, a D1×D2 direkt szorzat. Magyarán: ha pl. T1=Boolean, akkor D1={true,false}, hiszen a bool típus értéktartományába ez a két érték tartozik; ha T2=Int, akkor D2={0,1,−1,2,−2,…,231−1,−231}, hiszen a 32-bites előjeles int típus értéktartományába ezek az egész számok tartoznak. Akkor a Boolean×Int típus értéktartománya pedig a D1×D2={(true,0), (false,0), (true,1), (false,1), (true,2),…,(false,−231)}
A Pont tehát mint "típus", az Int×Int típusnak felel meg, és el van nevezve Pontnak. lesz: ahány féle párt csak tudunk képezni a két típus adattartományába tartozó értékekből (sorrend számít, ha a két típus véletlenül ugyanaz), az mind reprezentálható egy structban, avagy szorzat típusban.
Scalában a szorzat típus létrehozására a case class kulcsszavak használhatók (mindaddig, amíg a célunk annyi, hogy immutable adatmezőket csoportosítsunk szorzat típusba) így:
case class Pont(x: Int, y: Int)
val p : Pont = Pont(1, 3)
val q = Pont(2, 3)
val r = Pont(x = 2, y = 3)
val s = Pont(y = 3, x = 2)
println("p.x = " + p.x + ", p.y = " + p.y) // prints p.x = 1, p.y = 3
println(s) // prints Pont(2,3)
Tehát: a typedefnek megfelelő típusdeklaráció: case class, a típus neve, nyitójel, mezők felsorolása név, kettőspont, típus formában, csukójel.
Ezek után létrehozhatunk pontot pl. explicit deklarált módon vagy anélkül (mint p és q), a p-nél egy olyan pontot hoztunk létre, aminek az x mezőjének értéke 1, az y pedig 3, ezt az első println utasításnál látható módon p.x és p.y-ként hivatkozva érjük el, pont mint egy C-beli structnál. A q esetében a fordító ki tudta következtetni, hogy q típusa is Pont lesz. Az r és s értékek létrehozásánál azt figyeljük meg, hogy az érték létrehozásakor a paraméterekre név szerint is hivatkozhatunk, és ekkor nem baj, ha nem pont abban a sorrendben adjuk meg a mezőket, mint ahogy a típusdeklarációban a fejlécben szerepelnek: r is és s is ugyanúgy az a pont, melynek x mezeje a 2, y mezeje pedig a 3 értéket veszi fel, mint ahogy q is.
A példakódban még van olyan konstrukció, amit eddig nem láttunk: az első kiíratásnál stringet adunk össze számmal. Egyelőre erről elég annyit tudnunk, hogy minden értéknek (a típusától függően) van egy olyan függvénye, ami visszaadja az értéknek egy String reprezentációját: ha pl. egy Intünk van, akkor ez egyszerűen az értékének a tízes számrendszerbeli alakja lesz. Amikor egy Stringhez adunk hozzá (balról vagy jobbról) bármilyen értéket, akkor ennek az értéknek (ha az nem String) ez a toString nevű függvénye lefut, készít egy Stringet a típusnak megfelelő módon, és ezt a stringet konkatenálja össze a másik stringgel, amihez hozzáadtuk. Így áll össze az első példában a négy tag összegéből, melyből kettő string, kettő int, egyetlen string, amit aztán kiíratunk.
Egy hasonló dolog történik az utolsó kifejezésben is: println(s), ahol s nem egy string, hanem egy Pont típusú érték. A case class kulcsszóval deklarált típusok esetében sok minden történik "rejtett" módon, az egyik az, hogy a típushoz készül egy toString metódus is, és ha stringgé kell konvertálnunk egy (jelen esetben) Pont típusú értéket, akkor ez a metódus visszaadja a típus nevét, nyitójel, majd az összes mezőjének az értékének lekéri a toString függvényének az értékét, ezeket a típus deklarációjában lévő sorrend szerint felsorolja vesszővel elválasztva, csukójel.
Így lett a println(s) kifejezés mellékhatása egy Pont(2,3) string kiírása a képernyőre, mert ennek az s értéknek a (futásidejű - ezt nemsokára látni fogjuk, mit jelent) típusa Pont, ami egy case class, és s.x=2, s.y=3 voltak.
Folytassuk az előző kódot:
Itt azt láthatjuk, hogy (természetesen) függvénynek a paramétere is, visszatérési értéke is lehet custom típus, az addOssze függvény pl. két pontot mint "vektort" összead, az eredmény x koordinátája az x-ek, az y az y-ok összege lesz és ezt az új pontot adja vissza. A típusát kikövetkeztette a fordító, hogy Pont lesz.
Amire viszont érdemes felfigyelni: a t==Pont(3,6) kifejezés értéke true lesz. Ez azért van, mert case classok esetében az egyenlőség műveletet is automatikusan elkészíti nekünk a fordító (persze ha nem tetszik az alapértelmezett, megváltoztathatjuk, később láthatjuk, hogy hogyan), mégpedig: két Pont pontosan akkor lesz egyenlő, ha az x mezőjük is és az y mezőjük is egyenlő (most ezek Intek, de ha valami másmilyen összetett típusú mező lenne, akkor annak az egyenlőség műveletét is hívná rekurzívan). Ez eltérés a Java-hoz képest: ott az egyenlőség operátor alapvetően csak azt ellenőrzi, hogy a memóriában ugyanott van-e tárolva a két érték, és ehelyett ott egy equals metódust kell hívjunk (és meg is kell írnunk), ha "érték szerinti" egyenlőséget akarunk tesztelni. Ez a hívás (Javára átírva az egészet) pl. hamisat adna, mert az addOssze metódus megkapja a két pontot és létrehoz egy harmadikat valahol a heap memóriában, ezt (valójában ennek a referenciáját, erről később) adja vissza, ez kerül a t-be; a vele összehasonlított Pont(3,6) meg a heapben egy másik helyen ekkor jön létre, egy teljesen másik memóriaterületen.
Javában egyébként hogy nagyjából ugyanezt kapjuk, mint amit ezzel a Pont osztállyal eddig kezdtünk, kb. ezt kéne kódoljuk (meg még néhány másik metódust, amit a Scala szintén megcsinál, hashCodeot és egy factoryt az osztályhoz:
Sok esetben lesz még, hogy a Scala levesz a vállunkról némi "boilerplate"-et a Javához képest. Egy későbbi posztból majd azt is megtudhatjuk, hogy a case plusz kulcsszó miben változtat még pontosan, egyelőre immutable szorzat típus modellezésére a case class tökéletes választás lesz.
A példakódban még van olyan konstrukció, amit eddig nem láttunk: az első kiíratásnál stringet adunk össze számmal. Egyelőre erről elég annyit tudnunk, hogy minden értéknek (a típusától függően) van egy olyan függvénye, ami visszaadja az értéknek egy String reprezentációját: ha pl. egy Intünk van, akkor ez egyszerűen az értékének a tízes számrendszerbeli alakja lesz. Amikor egy Stringhez adunk hozzá (balról vagy jobbról) bármilyen értéket, akkor ennek az értéknek (ha az nem String) ez a toString nevű függvénye lefut, készít egy Stringet a típusnak megfelelő módon, és ezt a stringet konkatenálja össze a másik stringgel, amihez hozzáadtuk. Így áll össze az első példában a négy tag összegéből, melyből kettő string, kettő int, egyetlen string, amit aztán kiíratunk.
Egy hasonló dolog történik az utolsó kifejezésben is: println(s), ahol s nem egy string, hanem egy Pont típusú érték. A case class kulcsszóval deklarált típusok esetében sok minden történik "rejtett" módon, az egyik az, hogy a típushoz készül egy toString metódus is, és ha stringgé kell konvertálnunk egy (jelen esetben) Pont típusú értéket, akkor ez a metódus visszaadja a típus nevét, nyitójel, majd az összes mezőjének az értékének lekéri a toString függvényének az értékét, ezeket a típus deklarációjában lévő sorrend szerint felsorolja vesszővel elválasztva, csukójel.
Így lett a println(s) kifejezés mellékhatása egy Pont(2,3) string kiírása a képernyőre, mert ennek az s értéknek a (futásidejű - ezt nemsokára látni fogjuk, mit jelent) típusa Pont, ami egy case class, és s.x=2, s.y=3 voltak.
Folytassuk az előző kódot:
def addOssze(p: Pont, q: Pont) =
Pont(p.x + q.x, p.y + q.y)
val t = addOssze(p, q) // Pont(1+2,3+3) = Pont(3,6)
println(t == Pont(3,6)) // true
Itt azt láthatjuk, hogy (természetesen) függvénynek a paramétere is, visszatérési értéke is lehet custom típus, az addOssze függvény pl. két pontot mint "vektort" összead, az eredmény x koordinátája az x-ek, az y az y-ok összege lesz és ezt az új pontot adja vissza. A típusát kikövetkeztette a fordító, hogy Pont lesz.
Amire viszont érdemes felfigyelni: a t==Pont(3,6) kifejezés értéke true lesz. Ez azért van, mert case classok esetében az egyenlőség műveletet is automatikusan elkészíti nekünk a fordító (persze ha nem tetszik az alapértelmezett, megváltoztathatjuk, később láthatjuk, hogy hogyan), mégpedig: két Pont pontosan akkor lesz egyenlő, ha az x mezőjük is és az y mezőjük is egyenlő (most ezek Intek, de ha valami másmilyen összetett típusú mező lenne, akkor annak az egyenlőség műveletét is hívná rekurzívan). Ez eltérés a Java-hoz képest: ott az egyenlőség operátor alapvetően csak azt ellenőrzi, hogy a memóriában ugyanott van-e tárolva a két érték, és ehelyett ott egy equals metódust kell hívjunk (és meg is kell írnunk), ha "érték szerinti" egyenlőséget akarunk tesztelni. Ez a hívás (Javára átírva az egészet) pl. hamisat adna, mert az addOssze metódus megkapja a két pontot és létrehoz egy harmadikat valahol a heap memóriában, ezt (valójában ennek a referenciáját, erről később) adja vissza, ez kerül a t-be; a vele összehasonlított Pont(3,6) meg a heapben egy másik helyen ekkor jön létre, egy teljesen másik memóriaterületen.
Javában egyébként hogy nagyjából ugyanezt kapjuk, mint amit ezzel a Pont osztállyal eddig kezdtünk, kb. ezt kéne kódoljuk (meg még néhány másik metódust, amit a Scala szintén megcsinál, hashCodeot és egy factoryt az osztályhoz:
//típusdeklaráció
class Pont{
final int x;
final int y;
public Pont( int x, int y ) { this.x = x; this.y = y; }
public String toString(){ return "Pont(" + x + "," + y + ")"; }
public boolean equals( Object o ) {
if( o instanceof Pont ){
Pont p = (Pont)o;
return ( x == p.x )&&( y == p.y );
}
return false;
}
}
//összeadás metódus, egyelőre a Pont osztályon kívül "valahol"
Pont addOssze( Pont p, Pont q ) {
return new Pont( p.x+q.x , p.y+q.y );
}
//létrehozás, formázott kiírás
Pont p = new Pont(1,3);
Pont q = new Pont(2,3);
System.out.println( p ); //prints Pont(1,3)
//egyenlőség check
System.out.println( addOssze(p,q).equals( new Pont(3,6) ) );
Sok esetben lesz még, hogy a Scala levesz a vállunkról némi "boilerplate"-et a Javához képest. Egy későbbi posztból majd azt is megtudhatjuk, hogy a case plusz kulcsszó miben változtat még pontosan, egyelőre immutable szorzat típus modellezésére a case class tökéletes választás lesz.
folytköv
No comments:
Post a Comment