17 - genericek

Hogy csak Inteket tároló listákkal kelljen dolgozzunk, az nem egy real-life scenario. Lehet szükség Double listákra, String listákra stb. Mivel pedig maga a funkcionalitás ugyanaz mindháromban elviekben, nagyon error-prone megközelítés lenne copypaste gyártani minden típusra egy újabb és újabb osztályt pl. így:

 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
28
29
30
31
32
33
34
35
36
37
//egy osztály az int listáknak, filterrel
trait IntLista {
  def filter(p: Int=>Boolean): IntLista
}
case object UresIntLista extends IntLista {
  override def filter(p: Int=>Boolean) = UresIntLista
}
case class NemuresIntLista(head: Int, tail: IntLista) {
  override def filter(p: Int=>Boolean) =
    if (p(head)) NemuresIntLista(head, tail.filter(p)) else tail.filter(p)
}

//egy osztály a double listáknak, filterrel
trait DoubleLista {
  def filter(p: Double=>Boolean): DoubleLista
}
case object UresDoubleLista extends DoubleLista {
  override def filter(p: Double=>Boolean) = UresDoubleLista
}
case class NemuresDoubleLista(head: Double, tail: DoubleLista) {
  override def filter(p: Double=>Boolean) =
    if (p(head)) NemuresDoubleLista(head, tail.filter(p)) else tail.filter(p)
}

//egy osztály a string listáknak, filterrel
trait StringLista {
  def filter(p: String=>Boolean): StringLista
}
case object UresStringLista extends StringLista {
  override def filter(p: String=>Boolean) = UresStringLista
}
case class NemuresStringLista(head: String, tail: StringLista) {
  override def filter(p: String=>Boolean) =
    if (p(head)) NemuresStringLista(head, tail.filter(p)) else tail.filter(p)
}

//mappel ez még rosszabb lenne, ha intből doublet stb is meg akarnánk engedni

Borzasztó. Ilyenkor, mikor az osztályok tényleg összesen egy mező típusában különböznek, ésszerűnek látszik az igény: bárcsak lehetne ezt parametrikusan csinálni, egyetlen osztályt implementálni mondjuk Lista néven és valahogy futás közben megmondani, hogy most épp egy Int listát, vagy Double listát akarunk létrehozni.

generic típusparaméter [T]

És lehet: (ahogy egyébként Javában is) a parametrikus típussal ellátott osztályt generic osztálynak nevezik és ilyen a szintaxisa a listánk esetében:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// T típusú értékeket tároló láncolt lista, filterrel
trait Lista[T] {
  def filter(p: T=>Boolean): Lista[T]  //T-t lehet használni a metódusok paramétereiben, fejlécében
}
case class Ures[T]() extends Lista[T] {  //slight inconvenience: megint case class lett
  override def filter(p: T=>Boolean) = Ures()
}
case class Nemures[T](head: T, tail: Lista[T]) extends Lista[T] {
  override def filter(p: T=>Boolean) = if (p(head)) Nemures(head, tail.filter(p)) else tail.filter(p)
}

// teszteljünk
val intList = Nemures[Int]( 1, Nemures[Int](4, Nemures[Int](2, Ures[Int]() )))
val intList2: Nemures[Int] = Nemures(1,Nemures(4,Nemures(2,Ures())))
val stringList: Nemures[String] = Nemures("dinnye", Nemures("szilva", Nemures("narancs", Ures())))
println( intList.filter { _%2 == 1 } )

Mit tanultunk ebből a kódból?

  • az osztály kaphat egy [] közti típusparamétert, bárhogy elnevezhetjük, sokszor az ábécé T, U környékéről jön, de lehet pl. [K] és [V] is, ha mondjuk Key és Value szavakra akarunk utalni velük
  • az objektum nem kaphat típusparamétert, mert az objektumból csak egy van, a generic meg (a JVM sajátosságai miatt) futásidőben már nem lesz generic, nem készül külön osztály valójában minden lehetséges típusparaméterre, csak egy. A típusparaméter a fejlesztési fázisban fontos, segítségével számos hibát ki tudunk küszöbölni, mert a kódunk le se fog fordulni olyan esetekben, amikor ha lefordulna, és odaérne a vezérlés, összeomlana vagy valami inkonzisztens állapotba kerülne. Ezért lett a case objectből megint case class. De majd megoldjuk.
  • ez a T paraméter a kódból, példányosításkor bármi lehet: Int, String, Boolean, sőt, egymásba ágyazható módon akár Lista[Int] is (ekkor a listánkban int listákat fogunk tárolni)
  • a T paramétert a generikus osztály belsejében teljesen legális módon használhatjuk, mint bármilyen másik típust

generic metódusok

A map azért elsőre még mindig úgy tűnhet, mintha külön-külön kéne kezelnünk a T=>Int, T=>String, T=>Boolean függvényeket (meg az összes többit, ami előjöhet) és ezek mindegyikére a megfelelő típusú kimeneti listát produkáltatni:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
trait Lista[T] {
  def filter(p: T=>Boolean): Lista[T]
  def mapInt(f: T=>Int): Lista[Int]             //itt is probléma van: a JVM a függvényparaméterből csak
  def mapBoolean(f: T=>Boolean): Lista[Boolean] //annyit lát, hogy "függvény", ha mint a három map lenne,
  def mapString(f: T=>String): Lista[String]    //nem fordulna le => ezért a külön név
}
case class Ures[T]() extends Lista[T] {
  override def filter(p: T=>Boolean) = Ures()
  override def mapInt(f: T=>Int) = Ures()
  override def mapBoolean(f: T=>Boolean) = Ures()
  override def mapString(f: T=>String) = Ures()
}
case class Nemures[T](head: T, tail: Lista[T]) extends Lista[T] {
  override def filter(p: T=>Boolean) = if (p(head)) Nemures(head, tail.filter(p)) else tail.filter(p)
  override def mapInt(f: T=>Int) = Nemures(f(head), tail.mapInt(f))
  override def mapBoolean(f: T=>Boolean) = Nemures(f(head), tail.mapBoolean(f))
  override def mapString(f: T=>String) = Nemures(f(head), tail.mapString(f))
}
val intList = Nemures[Int]( 1, Nemures[Int](4, Nemures[Int](2, Ures[Int]() )))
println( intList.filter { _%2 == 1 } )
println( intList.mapString { _.toBinaryString }) //prints Nemures(1,Nemures(100,Nemures(10,Ures())))

Több sebből is vérzik ez a megközelítés, az egyik, hogy arra esélyünk nincs, hogy minden típusra írjunk egy arra specializált mapet, a másik, hogy hát nem tudjuk mapnek elnevezni mindet, mert a type erasure miatt mind a háromból csak annyi maradna, hogy ,,map nevű, aminek input paramétere egy függvény'', és ez így nem fordulna le (próbáljátok ki).

Látszik viszont, hogy itt is egy nagy másolás az egész módszer, és sok problémát megoldana, ha a map metódus kaphatna egy T-től esetleg különböző generic típusparamétert, és az mehetne a fenti kódban az Int, String stb. kimenetek helyébe.

Kaphat:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
trait Lista[T] {
  def filter(p: T=>Boolean): Lista[T]
  def map[U](f: T=>U): Lista[U]        //így van map[Int](f: T=>Int): Lista[Int] metódusunk is,
                                       //meg map[String](f: T=>String): Lista[String] metódusunk is, stb
}
case class Ures[T]() extends Lista[T] {
  override def filter(p: T=>Boolean) = Ures()
  override def map[U](f: T=>U) = Ures()  //és csak egyszer kell megírni őket!
}
case class Nemures[T](head: T, tail: Lista[T]) extends Lista[T] {
  override def filter(p: T=>Boolean) = if (p(head)) Nemures(head, tail.filter(p)) else tail.filter(p)
  override def map[U](f: T=>U) = Nemures(f(head), tail.map(f))
}


//tesztelgessük
val intList = Nemures[Int]( 1, Nemures[Int](4, Nemures[Int](2, Ures[Int]() )))
println( intList.filter { _%2 == 1 } )
println( intList.map { _.toBinaryString }) //prints Nemures(1,Nemures(100,Nemures(10,Ures())))

Mit tanultunk ebből a kódból?

  • Metódus neve után is be lehet szúrni típusparamétert (akkor is, ha amúgy az osztály nem generikus), és ekkor ezt a típust szabadon használhatjuk a metódus fejlécében és törzsében is
  • híváskor nem mindig kell kiírnunk, hogy melyik generikus paraméterrel szeretnénk hívni a metódust: ha a Scala fordító ki tudja következtetni (pl. a legutolsó sorban a _.toBinaryString egy Int=>String függvény, ezért a map generikusát U=String-gel tölti ki, hogy megfeleljen az f: T=>U szignatúrának
  • de nem mindig lehet ezt feloldani, néha ki kell írjuk, így pl. az Ures[Int]() osztály példányosításból persze nem jönne rá a fordító, ha csak annyit írnánk, hogy Ures(), hogy pont az Intre gondoltunk.

fold is van

Kérdések, feladatok

  • Létre tudunk-e hozni egy osztályon belül egy def hello( f: Boolean=>Boolean ) = ??? és egy def hello( g: Int=>Int ) = ??? metódust? Miért?

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