Tuesday, February 25, 2020

13 - generic

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:

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)
}
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)
}
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)
}
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.
É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:

trait Lista[T] {
  def filter(p: T=>Boolean): Lista[T]
}
case class Ures[T]() extends Lista[T] {
  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)
}

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 } )
Itt tehát azt mondjuk, hogy a Lista osztályunk kap egy [T] típusparamétert, ami bármi lehet: Int, String, Boolean, sőt akár Lista[Int] is (ekkor int listák listáját tároljuk), és ezt a T paramétert az osztály belsejében teljesen legális módon, mint típust használhatjuk, ahogy a fenti kód is mutatja. Létrehozáskor pl. eljárhatunk úgy is, ahogy a kód alsó részén felépítünk egy Int és egy String listát.

A map azért elsőre még mindig úgy tűnhet, mintha külön-külön kéne kezelnünk a TInt, TString, TBoolean 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:

trait Lista[T] {
  def filter(p: T=>Boolean): Lista[T]
  def mapInt(f: T=>Int): Lista[Int]
  def mapBoolean(f: T=>Boolean): Lista[Boolean]
  def mapString(f: T=>String): Lista[String]
}
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())))

(egyébként ha mindegyiket csak mapnak neveznénk el, akkor nem fordul le, mert a JVM nem tud különbséget tenni a különböző szignatúrájú függvények mint paraméterek között). Láthatjuk, hogy itt is mennyivel jobb lenne, ha a bejövő paraméterben az a Boolean, Int, String valami (másik, mert nem feltétlenül ugyanaz, mint a T nevű paraméter) típus is parametrizálható is lenne - és az!

trait Lista[T] {
  def filter(p: T=>Boolean): Lista[T]
  def map[U](f: T=>U): Lista[U]
}
case class Ures[T]() extends Lista[T] {
  override def filter(p: T=>Boolean) = Ures()
  override def map[U](f: T=>U) = 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 map[U](f: T=>U) = Nemures(f(head), tail.map(f))
}
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())))

Egyébként a T és az U csak nevezéktan: általában az első típusparaméter "neve" T, a másodiké U szokott lenni, de ettől számos eltérés van, ha a paramétereknek van valami konkrétabb jelentésük. Azt láthatjuk tehát, hogy függvényeket is lehet generic paraméterekkel ellátni, a példában a map metódus lett generic egy U nevű típusparaméterrel, a T-re parametrizált típusú listánkon belül, ezért lett a paraméterének a szignatúrája f:TU és a kimenete Lista[U] típusú. De ettől eltekintve az U típust, miután generic paraméternek deklaráltuk, ugyanúgy használhatunk a metódus szignatúrájában és törzsében, mint bármilyen másik "létező" típust.
Egy szépséghibája azért még mindenképp van a kódnak, amit láthatunk: object nem lehet generic, ezért az üres lista már case class lett, és emiatt ki kell rakjuk utána a zárójeleket is. (Ezt még később megoldjuk, amikor a kovariáns genericeket meg a Nothing típust nézzük.)

No comments:

Post a Comment