29 - a variancia szabályai I.

Tovább haladva a kovarianciában, és megörülve annak, hogy tudtunk kovariáns Optiont írni, most megpróbálunk egy kovariáns listát is írni. Azaz egy saját List[T] típust, ami az előző osztályhierarchiánk mellett pl. ha egy metódus List[NamedObject]-et vár, akkor tudjunk neki átadni egy List[Cat]-et.

az első közelítés

Kezdetnek legyen mondjuk egy foreach metódusunk is ezen a listán. Az eddigi tapasztalataink alapján hogyan implementálnánk?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
trait List[+T] {                   // kovariánsnak szeretnénk deklarálni
  def foreach[U]( f: T => U ): Unit  // szokásos foreach fejléc
}
case class Nil[T]() extends List[T] {
  override def foreach[U]( f: T => U ) = () //nothing to do here
}
case class Cons[T]( head: T, tail: List[T] ) extends List[T] {
  override def foreach[U]( f: T => U ) = {
    f(head)
    tail foreach f
  }
}
Válasz mutatása

Eddig oké, tudunk írni olyan metódust is, mely kiprinteli egy NamedObject lista minden elemének a nevét:

1
2
def printNamedList( list: List[NamedObject] ) =
  list.foreach( x => println(x.name) )

Ha most két macskát hozunk létre, berakjuk őket a kételemű listánkba, és kiíratjuk:

1
2
3
4
val morcos = Cat("Morcos")
val nyuszi = Cat("Nyuszi")
val catList = Cons( morcos, Cons( nyuszi, Ures() ) )
printNamedList( catList ) // prints Morcos\nNyuszi

Ez eddig teljesen rendben van.

Hogyan is érjük el, hogy pl. lehessen egy ilyen listát List(morcos, nyuszi)val is példányosítani?

Companion objectbe implementált generic apply metódussal:

1
2
3
4
5
object List {
  def apply[T](): List[T] = Nil()
  def apply[T]( head: T ) = Cons( head, Ures() )
  def apply[T]( first: T, second: T ) = Cons( first, Cons( second, Nil() ) )
}

Válasz mutatása

prepend operátor?

Most szeretnénk egy :: operátort a listánkba, amit már korábban írtunk. Így például elérnénk azt, hogy az előző catList ha már kész, akkor Cat("Omega") :: catList egy három macskás lista lenne (recall: mivel kettőspontra végződik a metódus neve, balról írjuk az objektumhoz, ha nem tesszük ki a pontot; ez így ugyanaz, mint a catList.::( Cat("Omega") ) kifejezés.

Eddig ez működött:

1
2
3
trait List[T] {
  def ::( head: T ): Cons[T] = Cons(head, this)
}

Beírhattuk a traitbe, alap implementációt adhattunk neki, hiszen bármi is a listám, elé úgy fűzök még egy elemet, hogy létrehozok egy új listaelemet, minek is a feje ez az új elem lesz, a farka meg az aktuális listánk, vagyis this.

Csakhogy ha ezt a metódust beírjuk a traitünkbe:

Nem fordul

Miért nem fordul

OK, ez így nem fordul, ezt az osztályt így nem fordítja le, hogy valamit a varianciára panaszkodik. Ha kivesszük a +T elől a +-t, akkor a listánk fordul (mint ahogy eddig is), de persze akkor

De így se jó

a NamedObject lista kiírató függvényünk nem fogja elfogadni a macskalistát, ahhoz kell a kovariancia.

Úgy fest, a kovariancia deklarálásának vannak szabályai, nem lehet csak úgy mindent kovariánssá tenni.

a probléma: T típus függvényparaméterként

Gondoljuk végig, mi is okozza a problémát? Ha mondjuk

  • van egy catList: List[Cat]-ünk,
  • és ezt odaadjuk egy insertFreya( petList: List[Pet] ): List[Pet] metódusnak,
  • ami a bejövő List[Pet] elé pluszban beszúrja a val freya = Dog("Freya") nevű kutyát és ezt adja vissza

kódban hogy is nézne ez ki?

1
def insertFreya( petList: List[Pet] ): List[Pet] = Dog("Freya") :: petList //ezzel nem kéne gond legyen

Válasz mutatása
akkor ezzel a fenti insertFreya függvénnyel nem kéne bármi gond legyen így implementálva: ha bejön egy List[Pet]-em, akkor miért is ne szúrhatnánk be egy petet egy petlist elé és adhatnánk vissza ezt az új, kibővített petlistet?

Viszont

  • ha a catList alapvetően egy List[Cat], és ezt adjuk oda az insertFreya( List[Pet] ) metódusnak,
  • akkor ha így deklaráljuk a ::(head: T) metódust a List[+T] osztályban, ahogy tettük, akkor acatList nem támogatná a Pet beszúrást, csak a Cat beszúrást, hiszen egy List[Cat] objektumban a függvény fejléce ::(head: Cat): List[Cat] lenne. Viszont az insertFreya metódus meg arra számít (joggal), hogy ha egy List[Pet] jön be, akkor annak legyen egy ::(head: Pet): List[Pet] metódusa.

Amit ebből leszűrhetünk: Ha T kovariáns típusparaméter egy osztályban, akkor T nem szerepelhet metódus paraméterének típusaként.

a megoldás: típuskorlát

Megoldaná a problémát, ha egy List[Cat]-nek garantáltan lenne pl. a ::(head: Cat) metóduson kívül egy ::(head: Pet) metódusa (meg egy ::(head: NamedObject) metódusa, hiszen épp odaadhatjuk olyan függvénynek is, mely pont egy List[NamedObject]et is vár), akkor az előző bekezdés insertFreya metódusa se lenne baj, ha kapna egy catListet, mert annak is garantáltan lenne Pet beszúró metódusa is.

Mivel ha T <: U, akkor List[T] <: List[U] az, amit szeretnénk elérni, ez valójában azt jelenti, hogy bármi is a T típus, ha egy metódus paraméterként elfogadhat T típust, akkor el kell fogadnia minden T-nél nagyobb U típust is, ha T-t kovariánssá akarjuk tenni. Persze mivel semmit nem tudunk a generic osztály megírásakor arról, hogy majd aki használja az osztályunkat, az milyen típushierarchiát fog építeni alá, arról szó nincs, hogy előre fel tudnánk írni az összes osztályt, ami csak szóba jöhet.

Megoldás lenne az, hogy T helyett ,,bármit'' elé tudjunk szúrni a listánknak, ezt egy másik típusparaméterrel oldanánk meg:

consany

Csakhogy ezzel az a baj, hogy amint szürkével látjuk, az eredmény típusáról nem tudunk semmit ezen a ponton, csak hogy ,,Cons[Any]'', ami (látni fogjuk nemsokára, de az Any típus mindennek az őse, minden Any, ami a Scalában létezik, és

Insert Freya nem fordul

így pedig az insertFreya nem fordul úgy, ahogy szeretnénk: azt szeretnénk, hogy a Pet lista elé szúrt újabb Pet az Pet lista legyen, így meg nem az.

Tehát az nem megoldás, hogy egy korlátozás nélküli újabb U típusparamétert adjunk hozzá a metódusunkhoz, de erre nincs is szükség: csak annyit akartunk eleve biztosítani, hogy ha T-nek egy őse az U, akkor forduljon U-val is. Erre van Scalában jelölés, így:

1
2
3
trait List[+T] {         
  def ::[U >: T]( head: U ) = Cons(head, this)  //ez a generic így azt mondja, hogy ha T <: U, akkor elfogadja
}

Ezzel minden, amit eddig begépeltünk, fordulni is fog. A :: függvény visszatérési értéke egyébként Cons[U] lesz.

Azaz: ha egy T típusparamétert kovariánssá teszünk, onnan az osztályban az eddig T típusú paraméterek egy új U típusú paraméterré kell váljanak, azzal, hogy a metódus nevénél deklaráljuk, hogy U egy ,,felső korlátja'' T-nek.

Mi lesz annak a listának a típusa és miért, amit úgy kapunk, hogy egy List[Cat] elé beszúrunk egy Dogot?

Ebben az osztályhierarchiában List[Pet]. Miért is:

  • ha pl. catList egy List[Cat], és kiértékeljük a dog :: catList kifejezést, ahol dog persze egy Dog,
  • akkor azt látja a fordító, hogy a :: metódusnak egy olyan U paraméterét kéne keresni, ami egyszerre őse a Dognak (mert a paraméter U típusú) és a Catnek (mert U >: T ezt mondja, most T=Cat)
  • matematikailag minden ilyen közös ős jó lenne, akár az Any is, de a Scala fordító ilyenkor megkeresi a legszűkebb olyan típust, ami mindkét osztálynak őse és ez az ős most a Pet lesz.

Válasz mutatása

A történet, hogy hova szabad T-t írni és hova nem, ennél egy kicsit összetettebb, mert ahogy láthatjuk, a foreach[U]( f: T=>U ): Unit metódus fejlécével eleve nem volt gond (persze f nem T típusú, hanem T=>U típusú), és ha T egy metódus visszatérési értéke, azzal sincs. Ha itt a foreachben f egy U=>T függvény lenne, azzal megint csak lenne problémája, ahogy egy (T=>U)=>V-vel is. Hogy miért, azt nemsokára jobban érteni fogjuk.


Utolsó frissítés: 2020-12-25 19:16:06