Sunday, February 16, 2020

06 - case class, szorzat típus

Progalapból is volt több módszer arra, hogy "egybe tartozó adatokat" összepakoljunk egyetlen adattípusba, classic példa lehet erre mondjuk egy $\mathrm{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 $\mathrm{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 $T_1$ típus értéktartománya a $D_1$ halmaz, a $T_2$ típusé meg a $D_2$ halmaz, akkor a $T_1\times T_2$ szorzattípusé, amit C-ben a $\mathrm{struct}$ kulcsszóval hozunk létre, a $D_1\times D_2$ direkt szorzat. Magyarán: ha pl. $T_1=\mathrm{Boolean}$, akkor $D_1=\{\mathrm{true},\mathrm{false}\}$, hiszen a bool típus értéktartományába ez a két érték tartozik; ha  $T_2=\mathrm{Int}$, akkor $D_2=\{0,1,-1,2,-2,\ldots,2^{31}-1,-2^{31}\}$, hiszen a $32$-bites előjeles int típus értéktartományába ezek az egész számok tartoznak. Akkor a $\mathrm{Boolean}\times\mathrm{Int}$ típus értéktartománya pedig a $$D_1\times D_2=\{(\mathrm{true},0),~(\mathrm{false},0),~(\mathrm{true},1),~(\mathrm{false},1),~(\mathrm{true},2),\ldots,(\mathrm{false},-2^{31})\}$$ 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 $\mathrm{struct}$ban, avagy szorzat típusban.
A $\mathrm{Pont}$ tehát mint "típus", az $\mathrm{Int}\times\mathrm{Int}$ típusnak felel meg, és el van nevezve $\mathrm{Pont}$nak.

Scalában a szorzat típus létrehozására a $\mathrm{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ó: $\mathrm{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ő $\mathrm{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 $\mathrm{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 $\mathrm{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 $\mathrm{toString}$ nevű függvénye lefut, készít egy $\mathrm{String}$et 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: $\mathrm{println}(s)$, ahol $s$ nem egy string, hanem egy $\mathrm{Pont}$ típusú érték. A $\mathrm{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 $\mathrm{toString}$ metódus is, és ha stringgé kell konvertálnunk egy (jelen esetben) $\mathrm{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 $\mathrm{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 $\mathrm{println}(s)$ kifejezés mellékhatása egy $\mathrm{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 $\mathrm{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 $\mathrm{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 $\mathrm{Pont}$ lesz.
Amire viszont érdemes felfigyelni: a $t==\mathrm{Pont}(3,6)$ kifejezés értéke $\mathrm{true}$ lesz. Ez azért van, mert $\mathrm{case~class}$ok 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 $\mathrm{Pont}$ pontosan akkor lesz egyenlő, ha az $x$ mezőjük is és az $y$ mezőjük is egyenlő (most ezek $\mathrm{Int}$ek, 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 $\mathrm{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 $\mathrm{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 $\mathrm{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 $\mathrm{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, $\mathrm{hashCode}$ot é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 $\mathrm{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