Kihagyás

31 - a variancia szabályai II

Ha az előző rész alapján meg is próbáltuk implementálni a Nil objectet, a következő furcsaságba botolhattunk: ami eddig ez volt

1
2
3
case class Nil[T]() extends List[T] {
  def foreach[U]( f: T => U ): Unit
}
az most ezzé kéne váljon?
1
2
3
case object Nil extends List[Nothing] {
  def foreach[U]( f: Nothing => U ): Unit //mit jelent az, hogy Nothing => U?
}
Egyáltalán: ha K1 => V1 és K2 => V2 függvény típusok (az is egy perfectly fine típus), mikor mondhatjuk azt, hogy (K1 => V1) <: (K2 => V2)? Mikor ,,speciálisabb'' egy függvény típus egy másiknál?

Macska, Allat, Eloleny

Azért, hogy tudjuk ellenőrizni is, amire jutunk, hogy hogy ,,kellene'' működnie a függvény típusok közti hierarchiának, hozzunk létre néhány traitet:

1
2
3
trait Eloleny
trait Allat extends Eloleny
trait Macska extends Allat
és induljunk ki ebből a kódból:
1
2
3
4
def a: Allat = ???            //a típusa Allat
def f: Allat => Allat = ???   //f típusa Allat => Allat

val b: Allat = f(a)           //b típusa Allat
Futtatni persze nem fogunk semmit, hiszen semminek nincs implementációja, minden kivételt dobna; de nem is kéne, hiszen a fordító se futtatja a kódot, mikor eldönti, hogy valami típus valahova behelyettesíthető-e vagy sem.

Kb. ez minden, amit tudunk csinálni egy függvénnyel úgy általánosságban. Pontosan mi is akkor az, amit egy K=>V típusú függvénnyel biztosan megtehetünk?

Egy `K=>V` típusú `f` függvénybe mindenképp * behelyettesíthetünk egy `K` típusú argumentumot * értéke pedig `V` típusú lesz, így azt behelyettesíthetjük mindenhova, ahova szabad `V` típust írni, pl. egy `V` típusú érték inicializálásába. Láthatunk itt máris valami asszimetriát: ha `K' <: K`, akkor persze hogy `K'` típusút is behelyettesíthetünk `f`-be, hiszen ami `K'` típusú, az egyben `K` típusú is. Viszont ha `V <: V'` **(pont a másik irányba haladunk a típushierarchiában)**, akkor lesz az igaz, hogy ha valami `V'` típust vár (pl. egy `V'` típusú változó inicializálása), oda beírhatjuk az értékét a függvénynek, hiszen az `V` típusú lesz, ami egyben `V'` is. Válasz mutatása

Az biztos, hogy ha (K=>V) <: (Allat=>Allat), akkor egy K=>V típusú függvényre szabad kicseréljük a val b: Allat = f(a) inicializálásban f-et, hiszen erről szól a subtype (szintaktikailag): ahova az általánosabb típus minden elemét be szabad írni, oda a speciálisabb típus minden elemét is be szabad írni.

Nézzük meg, mi történik, ha az f függvény szignatúrájában mindkét oldalon átírjuk az Allat osztályt Macska-ra (szűkebb típusra) vagy Eloleny-re (bővebb típusra):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def a  : Allat              = ???
def f  : Allat   => Allat   = ???
def fae: Allat   => Eloleny = ???
def fam: Allat   => Macska  = ???
def fea: Eloleny => Allat   = ???
def fma: Macska  => Allat   = ???

val b  : Allat = f  (a)  //ez fordult, ezt tudjuk
val bae: Allat = fae(a)
val bam: Allat = fam(a)
val bea: Allat = fea(a)
val bma: Allat = fma(a)

Melyik fordul le a fenti kifejezések közül és melyikek nem? El tudjuk magyarázni, miért?

![famfem](31/famfemfom.png) * `val bae: Allat = fae(a)` azért nem fordul le, mert a `fae: Allat => Eloleny` függvényről csak azt tudjuk a szignatúrája alapján biztosan, hogy `Eloleny`t ad vissza! Semmi garancia arra, hogy ezen belül `Allat` típusú lesz, ezért **az értékadás rész nem fordul**. Ez azt jelenti, hogy ha `V1 <: V2` két (különböző) típus, és `K` egy harmadik, akkor **nem** lesz igaz az, hogy `(K=>V2) <: (K=>V1)`, hiszen most sem tudtunk egy `Allat=>Allat` típusú függvényt lecserélni egy `Allat=>Eloleny` típusúra, tehát az utóbbi biztos, hogy nem szákebb típus. * **Visszafelé viszont igen:** `val bam: Allat = fam(a)` lefordul, hiszen a `fam` függvény `Allat`ot vár paraméterben, azt is kap, és `Macska` típusra értékelődik ki, melyet odaadhatunk értékül `bam`nak, hiszen az bármilyen `Allat`ot elfogad, akkor is, ha `Macska`. Ez általában is igaz: **ha `V1 <: V2` két típus és `K` is típus, akkor `(K=>V1) <: (K<=V2)`**. Fordított a helyzet, ha az input paraméterek típusát állítgatjuk: * `val bea: Allat = fea(a)` **lefordul**, hiszen `fea` bármilyen `Eloleny` típusú argumentumot elfogad, így `a`-t is, ami `Macska`, tehát élőlény; értéke pedig `Allat` lesz, tehát továbbra is odaadhatjuk a `bea` nevű értéknek. * val bma: Allat = fma(a)` **nem fordul le**, hiszen ezúttal `fma` egy `Macska` paramétert vár mindenképp, semmi garancia, hogy az `a`, amit kapott, az is egy `Macska`; lehet bármilyen `Allat`. Ezt a kettőt összefoglalva: **ha `K2 <: K1` két típus és `V` is típus, akkor `(K1=>V) <: (K2=>V)`**. Válasz mutatása

Vagyis: (K1=>V1) <: (K2=>V2) akkor teljesül, ha K2 <: K1 és V1 <: V2. Tehát a függvények bemeneti típusaikban kontravariánsak, kimeneti típusukban kovariánsak.

Így például minden függvény Nothing => Any típusú, mert a Nothing van a típushierarchia alján, az Any pedig a tetején.

Ha pedig egy Nothing => T típusú függvényt váró metódust látunk, akkor oda ezek szerint akkor helyettesíthetünk be egy K=>V típusú függvényt, ha Nothing <: K (ez mindig teljesül) és V <: T. Mivel a Nil objektum foreach metódusában az U egy generic típusparaméter volt, így oda bármit írhatunk, tehát a foreach[U]( f: Nothing => U ): Unit fejléc azt jelenti, hogy ennek a foreach metódusnak bármilyen függvényt odaadhatunk, a kifejezés értéke pedid Unit típusú (tehát ()) lesz.

Kovariáns és kontravariáns típusparaméterek

Ez sokkal többet segít nekünk abban, hogy felmérjük, melyik generikus paraméterek tehetők ko- vagy kontravariánssá egy osztálydeklarációban, mint az elsőre tűnhet.

Vegyük ugyanis pl. a List[T] eredeti traitünket, kicsit kibővítve:

1
2
3
4
5
6
7
trait List[T] {
  def foreach[U]( f: T=>U ): Unit
  def map[U]( f: T=>U ): List[U]
  def head: T
  def tail: List[T]
  def ::( head: T ): List[T]
}
Az ebben a deklarációban szereplő metódusokat is átírhatjuk függvény alakba a következőképp (és ezt bármilyen osztállyal megtehetjük):
1
2
3
4
5
6
7
trait List[T] {
  def foreach[U]: (T=>U)=>Unit
  def map[U]: (T=>U)=>List[U]
  def head: T
  def tail: List[T]
  def :: : T=>List[T]
}
Ha lenne egy def f(n: Int, m: Long): String metódus, abból pl. mi készülne egy ilyen átíráskor?

`def f: (Int,Long)=>String`. Válasz mutatása

Röviden mondva, egy ilyen átírás után amit ellenőriznünk kell (miután megjelöljük az osztály generikus paramétereit kovariánssá - jele a +T vagy kontravariánssá - jele a -T), a következő:

  • kovariáns típusparaméter minden fieldben csak kovariáns pozícióban szerepelhet,
  • kontravariáns típusparaméter minden fieldben csak kontravariáns pozícióban szerepelhet.

Mit jelent ez? Minden típusban minden típusparaméter vagy kovariáns, vagy kontravariáns, vagy invariáns pozíción szerepel, a szabályok a következők (legalábbis arra vonatkozólag, amit látunk):

  • maga a T típus magában kovariáns pozíció. Így például a def head: T mező deklarációjában szereplő T kovariáns pozíción van. Ez már önmagában kizárja azt, hogy a T paramétert kontravariánsnak deklaráljuk a fejlécben.
  • Ha a T típust generikusként használjuk belül egy C osztályon (pl. a def tail: List[T] mező típusában), akkor varianciája megegyezik a C osztálybeli varianciájával:
    • ha C-ben T kovariáns, akkor a C[T] típusban szereplő T is az;
    • ha C-ben T kontravariáns, akkor a C[T] típusban szereplő T is az;
    • ha C-ben T invariáns, akkor a C[T] típusban szereplő T is az.
  • Ha a T típus egy K=>V típus V-beli pozícióján szerepel, akkor K=>V-ben ugyanannak a pozíciónak a varianciája megegyezik a V-beli varianciájával.
  • Ha a T típus egy K=>V típus K-beli pozícióján szerepel, akkor K=>V-ben ugyanannak a pozíciónak a varianciája ,,átvált'' a K-beli varianciájához képest:
    • a K-beli kovariáns pozíciók K=>V-ben kontravariánsak,
    • a K-beli kontravariáns pozíciók K=>V-ben kovariánsak,
    • a K-beli invariáns pozíciók K=>V-ben is invariánsak.
  • Ha T egy U generikus típus alsó korlátjaként szerepel, [U >: T] alakban, akkor itt T kovariáns.

Nézzük végig ezek alapján, hogy ha a fejlécben a T típust kovariánssá alakítjuk, akkor a belsejében lévő (összesen hat darab) T pozíción melyek lesznek kovariánsak, kontravariánsak vagy invariánsak? Miért emeltem ki félkövérrel, hogy ha kovariánssá alakítjuk?

Talán kezdjük az egyszerűbbekkel és úgy haladjunk a bonyolultabak felé. * `def head: T`, magában álló `T` kovariáns pozíció, mivel ez az egész típus, ebben a mezőben `T` kovariánsan szerepel. * `def tail: List[T]`, itt fontos, hogy ,,ha a `List[+T]`'', mert ezen az alapon tudjuk, hogy itt akkor a `List[T]`-ben `T` kovariáns, és mivel ez az egész típus, így itt is kovariáns a mezőben `T`. * `def foreach[U]: (T=>U)=>Unit`, itt a kis részektől a nagyobbak felé haladunk: * `T` magában kovariáns, * akkor `T=>U`-ban, mivel a függvény bal oldalán kovariáns pozícióban szerepel, így `T=>U`-ban ez a pozíció már kontravariáns, * akkor `(T=>U)=>Unit`-ban, mivel a függvény bal oldalán, `T=>U`-ban kontravariáns pozíción szerepel, így `(T=>U)=>Unit`-ban megint kovariáns ez a pozíció, * és mivel ez az egész típus, így a `foreach`-ban is kovariáns pozíción szerepel `T`. (Ezért fordult le a múltkor, mikor kovariánssá tettük.) * `def map[U]: (T=>U)=>List[U]`, itt ismét * `T` magában kovariáns, * így `T=>U`-ban kontravariánssá válik, * így `(T=>U)=>List[U]`-ban (mivel megint a bal oldalon szerepel) ismét kovariáns, * tehát a `map`ben is kovariáns pozíción szerepel `T`. * `def :: : T=>List[T]`-ben **kétszer** is szerepel, * `T` magában kovariáns, * akkor a `T => List[T]` bal oldalán szereplő `T` mivel a bal oldal típusában kovariáns, **az egész típusban kontravariáns ez a pozíció**, * a jobb oldalon `List[T]`-ben kovariáns, mert `List`-ben azzá válik, * így a `T=>List[T]`-ben **a jobb oldalon lévő `T`** varianciája ugyanaz, mint `List[T]`-ben volt, azaz **az a pozíció pedig kovariáns**. Összességében mivel van **egy** olyan pozíció, ahol `T` kontravariáns, ezért ebben az osztályban így nem lehet `T`-t kovariánsnak deklarálni, fordítási hibát kapunk. Válasz mutatása

egy kontravariáns osztály

A PartialFunction trait olyan K=>V függvényt reprezentál, mely nem feltétlenül van minden K típusú értékre definiálva:

1
2
3
4
trait PartialFunction[K,V] {
  def apply( key: K ): V
  def isDefinedAt( key: K ): Boolean
}
Határozzuk meg, hogy a generikus paraméterek mlyen varianciát engednek meg!

Átírva a két metódust csak a típusukra fókuszálva, kapunk egy `apply: K => V` és egy `isDefinedAt: K => Boolean` függvényt. Mivel `V` magában kovariáns, és `K => V`-nek ezek szerint a jobb oldalán kovariáns pozícióban szerepel, így az `apply` metódusban ő kovariáns pozícióban áll, az `isDefinedAt` metódusban nem szerepel, így **`V` lehet kovariáns** (kontravariáns pedig nem). Mivel `K` magában kovariáns, és ezek szerint a `K=>V` típus **bal** oldalán kovariáns pozícióban szerepel, így az egész `K=>V` típusban (és a `K=>Boolean` típusban is) kontravariánssá válik ez a pozíció. Máshol nem szerepel, tehát **`K` lehet kontravariáns** (kovariáns pedig nem). És a trait is ezt írja a doksiban:
1
2
3
4
trait PartialFunction[-K,+V] {
  def apply( key: K ): V
  def isDefinedAt( key: K ): Boolean
}
Hasonlóan, a `Map` is `Map[-K,+V]` varianciájú. Válasz mutatása

Utolsó frissítés: 2020-12-25 22:14:32