Tuesday, February 25, 2020

13 - generic

Hogy csak $\mathrm{Int}$eket tároló listákkal kelljen dolgozzunk, az nem egy real-life scenario. Lehet szükség $\mathrm{Double}$ listákra, $\mathrm{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 $\mathrm{Lista}$ néven és valahogy futás közben megmondani, hogy most épp egy $\mathrm{Int}$ listát, vagy $\mathrm{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 $\mathrm{Lista}$ osztályunk kap egy $[T]$ típusparamétert, ami bármi lehet: $\mathrm{Int}$, $\mathrm{String}$, $\mathrm{Boolean}$, sőt akár $\mathrm{Lista}[\mathrm{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 $\mathrm{Int}$ és egy $\mathrm{String}$ listát.

A $\mathrm{map}$ azért elsőre még mindig úgy tűnhet, mintha külön-külön kéne kezelnünk a $T\Rightarrow\mathrm{Int}$, $T\Rightarrow\mathrm{String}$, $T\Rightarrow\mathrm{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:

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 $\mathrm{map}$nak 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 $\mathrm{Boolean}$, $\mathrm{Int}$, $\mathrm{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 $\mathrm{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:T\Rightarrow U$ és a kimenete $\mathrm{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 $\mathrm{Nothing}$ típust nézzük.)

No comments:

Post a Comment