11 - trait: az összeg

A tuple és a case class a szorzat típus névre is hallgatnak, mert segítségükkel fel tudunk építeni olyan típust, melynek a lehetséges értéktartománya az "altípusok" értéktartományának Descartes-szorzatai.

Sokszor van igény arra is, hogy unió típust hozzunk létre: azaz ha van egy T1 típusunk, aminek az értéktartománya D1, meg egy T2 típusunk, aminek az értéktartománya D2, és szeretnénk készíteni egy olyan T típust, melynek értéktartománya D1 ⊎ D2, a két értéktartomány diszjunkt uniója. Ez azt jelenti, hogy ha van is átfedés a két halmaz közt, akkor is minden elem az ,,eredeti típus infóval'' kerüljön bele, pl. ha az Int és a Long típusokból szeretnénk készíteni egy unió típust, akkor szerepeljen ennek az új típusnak az értéktartományában az int 0 is és a long 0 is külön-külön.

Ha ezt le tudjuk kódolni egy programozási nyelvben, akkor azt mondjuk, hogy az új T típus a T1 és T2 típusok összege, jelben T=T1+T2. (A case class pedig a két vagy több mezőtípusának a szorzata volt, pl. a 2D Pont típusunkra írhatjuk akár azt, hogy Pont = Int x Int.)

Háromszögek és körök C-ben

Hogy kicsit megfoghatóbb legyen: mondjuk, hogy van egy geometriai formákkal dolgozó appunk. Van benne pont, háromszög (ami három pontból tevődik össze), kör (ami egy középpontból és egy sugárból), pl. így:

1
2
3
4
case class Pont(x: Int, y: Int)

case class Haromszog(p: Pont, q: Pont, r: Pont)
case class Kor(p: Pont, r: Double)

és szeretnénk valami olyan típust (pl. mert valami kollekcióba, listába, whatever, akarunk gyűjteni geometriai formákat (egyszerűség kedvéért most a pontot ne, csak köröket meg háromszögeket akarunk uniform módon kezelni) és ne kelljen ehhez két különböző típusú (mondjuk) tömböt deklaráljunk. Ez pont az az igény, hogy szeretnénk egy Shape = Kor + Haromszog típust létrehozni.

Ennek C-ben egy implementációja pl. így nézhet ki:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
typedef struct { int x; int y; } Pont;
typedef struct { Pont p; Pont q; Pont r; } Haromszog;
typedef struct { Pont p; double r; } Kor;

typedef struct {
  int type; // 0 -- haromszog 1 -- kor
  union{
    Haromszog haromszog;
    Kor kor;
  };
} Shape;

azaz egy olyan structot készítünk, aminek a type mezője tárolja, hogy most épp kör vagy háromszög amire tulajdonképp mutatunk, és ennek függvényében van vagy egy Kor, vagy egy Haromszog adatmezőnk, de mivel egyszerre csak az egyik lehet valid, ezért őket unionba tesszük. Egy példa használata ennek az új típusnak:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Pont p; p.x = 1; p.y = 2;
Haromszog h; h.p = p; h.q = p; h.r = p;
Kor k; k.p = p; k.r = 1.0;
Shape shape;
shape.type = 0;
shape.haromszog = h;

//és ha dolgozni akarunk vele:  
printf( "Shape is: %s\n", shape.type ? "circle" : "triangle" );
switch( shape.type )
{
    case 0:
        printf( "Nodes: (%d,%d), (%d,%d), (%d,%d)\n",
            shape.haromszog.p.x,
            shape.haromszog.p.y,
            shape.haromszog.q.x,
            shape.haromszog.q.y,
            shape.haromszog.r.x,
            shape.haromszog.r.y
        ); break;
    case 1:
        printf( "Center: (%d,%d), radius: %lf\n",
            shape.kor.p.x,
            shape.kor.p.y,
            shape.kor.r
        ); break;
}

Tehát: C-ben a struct a típus mezőjének állítgatásával tudjuk azt biztosítani, hogy tudjuk, ez most épp egy háromszög lesz, vagy egy kör.

...és Scalában

Ha a case classok a saját osztályaink, és módosíthatjuk a fejlécüket, akkor a legegyszerűbb módja összeg típus létrehozásának egy trait létrehozása:

1
2
3
4
5
case class Pont(x: Int, y: Int)

trait Shape
case class Haromszog(p: Pont, q: Pont, r: Pont) extends Shape
case class Kor(p: Pont, r: Double) extends Shape

Amit tanulhatunk a fenti kódból: * új ,,típust'', mint most a Shape, létrehozhatunk a trait kulcsszóval, a létrejött típus ezen a ponton ,,üres'' * ha egy case class fejlécének végére odaírjuk, hogy extends és egy trait nevét, akkor onnantól kezdve a megadott trait adattartományába ez a case class is beletartozik * nem kell plusz mezőt létrehoznunk, ami valamilyen értelemben ,,figyel'' arra, hogy egy Shape típusú értékben lévő konkrét objektum éppen kör vagy háromszög, ez automatikusan fog történni.

A fenti osztályok használatára gyors példa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
val p = Pont(1,2)         //ez egy pont létrehozása, case class, így kell
val h = Haromszog(p,p,p)  //háromszög, mindhárom pontja ugyanaz
val k = Kor(p,1.0)        //kör, 1 sugárral akörül a pont körül

val shape: Shape = h      //na most a shape value típusa Shape!

//kódból meg fogjuk tudni különböztetni match-kifejezéssel, hogy épp milyen
//,,konkrét'' típus van a Shape típusú értékben:
shape match {
  case Haromszog(p,q,r) => println("Shape is: triangle\nNodes: " + p + "," + q + "," + r)
  case Kor(p,r) => println("Shape is: circle\nCenter: " + p + ", radius: " + r)
}

A match kifejezésre még visszatérünk, egyelőre csak azt illusztrálja, hogy ha van egy Shape típussal deklarált értékünk, akkor hogyan lehet megtudni belőle, hogy éppen milyen érték is van benne. A shape érték deklarálásakor azért van kiírva a Shape típus expliciten, mert különben - mivel a jobb oldalon szereplő h az egy Haromszog - a Scala fordító Haromszog-re inferné. A traitek szintén több mindenre lesznek jók ezen kívül: egyelőre csak az a lényeg, hogy tudunk létrehozni a beépített típusokból case classokkal és traitekkel szorzat és összeg típusokat, amikkel pedig elérkezünk az algebrai adattípus fogalmáig, ami a funkcionális nyelvek egyik legfontosabb adatszerkezet-osztálya.

Kérdések, feladatok


Utolsó frissítés: 2021-02-07 23:06:47