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:
//egy osztály az int listáknak, filterreltraitIntLista{deffilter(p:Int=>Boolean):IntLista}caseobjectUresIntListaextendsIntLista{overridedeffilter(p:Int=>Boolean)=UresIntLista}caseclassNemuresIntLista(head:Int,tail:IntLista){overridedeffilter(p:Int=>Boolean)=if(p(head))NemuresIntLista(head,tail.filter(p))elsetail.filter(p)}//egy osztály a double listáknak, filterreltraitDoubleLista{deffilter(p:Double=>Boolean):DoubleLista}caseobjectUresDoubleListaextendsDoubleLista{overridedeffilter(p:Double=>Boolean)=UresDoubleLista}caseclassNemuresDoubleLista(head:Double,tail:DoubleLista){overridedeffilter(p:Double=>Boolean)=if(p(head))NemuresDoubleLista(head,tail.filter(p))elsetail.filter(p)}//egy osztály a string listáknak, filterreltraitStringLista{deffilter(p:String=>Boolean):StringLista}caseobjectUresStringListaextendsStringLista{overridedeffilter(p:String=>Boolean)=UresStringLista}caseclassNemuresStringLista(head:String,tail:StringLista){overridedeffilter(p:String=>Boolean)=if(p(head))NemuresStringLista(head,tail.filter(p))elsetail.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.
É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 910111213141516
// T típusú értékeket tároló láncolt lista, filterreltraitLista[T]{deffilter(p:T=>Boolean):Lista[T]//T-t lehet használni a metódusok paramétereiben, fejlécében}caseclassUres[T]()extendsLista[T]{//slight inconvenience: megint case class lettoverridedeffilter(p:T=>Boolean)=Ures()}caseclassNemures[T](head:T,tail:Lista[T])extendsLista[T]{overridedeffilter(p:T=>Boolean)=if(p(head))Nemures(head,tail.filter(p))elsetail.filter(p)}// teszteljünkvalintList=Nemures[Int](1,Nemures[Int](4,Nemures[Int](2,Ures[Int]())))valintList2:Nemures[Int]=Nemures(1,Nemures(4,Nemures(2,Ures())))valstringList: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
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 9101112131415161718192021
traitLista[T]{deffilter(p:T=>Boolean):Lista[T]defmapInt(f:T=>Int):Lista[Int]//itt is probléma van: a JVM a függvényparaméterből csakdefmapBoolean(f:T=>Boolean):Lista[Boolean]//annyit lát, hogy "függvény", ha mint a három map lenne,defmapString(f:T=>String):Lista[String]//nem fordulna le => ezért a külön név}caseclassUres[T]()extendsLista[T]{overridedeffilter(p:T=>Boolean)=Ures()overridedefmapInt(f:T=>Int)=Ures()overridedefmapBoolean(f:T=>Boolean)=Ures()overridedefmapString(f:T=>String)=Ures()}caseclassNemures[T](head:T,tail:Lista[T])extendsLista[T]{overridedeffilter(p:T=>Boolean)=if(p(head))Nemures(head,tail.filter(p))elsetail.filter(p)overridedefmapInt(f:T=>Int)=Nemures(f(head),tail.mapInt(f))overridedefmapBoolean(f:T=>Boolean)=Nemures(f(head),tail.mapBoolean(f))overridedefmapString(f:T=>String)=Nemures(f(head),tail.mapString(f))}valintList=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 910111213141516171819
traitLista[T]{deffilter(p:T=>Boolean):Lista[T]defmap[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}caseclassUres[T]()extendsLista[T]{overridedeffilter(p:T=>Boolean)=Ures()overridedefmap[U](f:T=>U)=Ures()//és csak egyszer kell megírni őket!}caseclassNemures[T](head:T,tail:Lista[T])extendsLista[T]{overridedeffilter(p:T=>Boolean)=if(p(head))Nemures(head,tail.filter(p))elsetail.filter(p)overridedefmap[U](f:T=>U)=Nemures(f(head),tail.map(f))}//tesztelgessükvalintList=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.