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 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,,2311,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)}
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.
A Pont tehát mint "típus", az Int×Int típusnak felel meg, és el van nevezve Pontnak.

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:

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