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

  • 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 Elolenyt 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 Allatot vár paraméterben, azt is kap, és Macska típusra értékelődik ki, melyet odaadhatunk értékül bamnak, hiszen az bármilyen Allatot 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úttalfmaegyMacskaparamétert vár mindenképp, semmi garancia, hogy aza, amit kapott, az is egyMacska; lehet bármilyenAllat`.

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 mapben 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