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
| case class Nil[T]() extends List[T] {
def foreach[U]( f: T => U ): Unit
}
|
az most ezzé kéne váljon?
| 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:
| trait Eloleny
trait Allat extends Eloleny
trait Macska extends Allat
|
és induljunk ki ebből a kódból:
| 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?

* `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:
| 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):
| 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:
| 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:
| 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