Scala - tranzitivita implicitních konverzí

Mechanismus implicitních konverzí je jednou ze silných stránek Scaly. Je bohatě používán ve standardní knihovně, například pro obohacení Javovkých typů. Právě implicitní konverze stojí za přístupem Pimp my Library, kdy můžete vylepšovat existující knihovny, aby vyhověly vašim požadavkům a mnoho dalších kouzel.
Na rozdíl od svého protějšku v C++, jsou však bezpečné, protože nejsou globální. Uplatňují se jenom ty konverze, které jsou v aktuálním scope dosažitelné jednoduchým jménem. Takže implicitní funkce A.B.C.fun
musí být importována např. jako A.B.C._
.
Scala ale nepovoluje tranzitivitu konverzí z důvodu, že by se soubory s mnoha typovými chybami kompilovaly příliš dlouho. Kompilátor by musel zkusit všechny kombinace všech dostupných konverzí a to je problém velikosti n2 (viz. Programming in Scala).
Ale přesto byl v této přednášce (video) letmo zmíněn způsob, jak si tranzitivitu implicitních konverzí vynutit přes viewbounds.
Mějme třídy A, B, C, D, E a chceme A zkonvertovat postupně až na E, která obsahuje požadovanou metodu value
. Toho můžeme dosáhnout tak, že každá implicitní konverze bude mít typový parametr, který bude viewbound typu za kterého konvertujeme. Konverzní funkce z A na B bude tedy vypadat takto: implicit def a2b[T <% A](i: T): B = B(i.v)
def p = println(_: String) // p bude alias pro funkci println case class A(v: Int) case class B(v: Int) case class C(v: Int) case class D(v: Int) case class E(v: Int) { def value = v } implicit def a2b[T <% A](i: T): B = { p("a2b"); B(i.v) } implicit def b2c[T <% B](i: T): C = { p("b2c"); C(i.v) } implicit def c2d[T <% C](i: T): D = { p("c2d"); D(i.v) } implicit def d2e[T <% D](i: T): E = { p("d2e"); E(i.v) } A(111).value
Poslední řádek vypíše pořadí konverzí, které se zdá, že je špatně.
d2e c2d b2c a2b
Proč to vlastně celé funguje a proč je pořadí obrácené?
Viewbounds [T <% A] znamená, že s typem T může být zacházeno jako s typem A. Tedy, že existuje implicitní konverze, která dokáže typ T přeměnit na A. Viewbound se expanduje do jednoho implicitního parametru navíc.
Naše konverze jsou zkratkou pro následující funkce:
implicit def a2b[T](i: T)(implicit ev: T => A): B = { p("a2b"); B(i.v) } implicit def b2c[T](i: T)(implicit ev: T => B): C = { p("b2c"); C(i.v) } implicit def c2d[T](i: T)(implicit ev: T => C): D = { p("c2d"); D(i.v) } implicit def d2e[T](i: T)(implicit ev: T => D): E = { p("d2e"); E(i.v) }
Implicitní argument funkce zároveň může být použit ve funkci, aby vyřešil případné typové konflikty. Takže naše konverze jsou ve skutečnosti následující:
implicit def a2b[T](i: T)(implicit ev: T => A): B = { p("a2b"); B(ev(i).v) } implicit def b2c[T](i: T)(implicit ev: T => B): C = { p("b2c"); C(ev(i).v) } implicit def c2d[T](i: T)(implicit ev: T => C): D = { p("c2d"); D(ev(i).v) } implicit def d2e[T](i: T)(implicit ev: T => D): E = { p("d2e"); E(ev(i).v) }
Kompilátor se pokusí najít konverzi na cílový typ E (který má požadovanou metodu value
). Tady vyhovuje jedině d2e, jehož první argument může být něco, co může být implicitním parametrem zkonvertováno na D. Hodnota implicitního parametru se bude hledat mezi implicitními funkcemi. Vyhovující je funkce c2d, která přijímá nějaký argument, který můžeme považovat za C (viewbound), ale zase má implicitní parametr. A tak dál. Kompilátor tedy postupně nahrazuje implicitní parametry implicitními funkcemi, která zase mají implicitní parametry a tak řetězí jednotlivé konverze.
Ve výsledku se volání A(111).value
expanduje na:
d2e(A(111)){ c => c2d(c){ b => b2c(b) { a => a2b(a) { conforms _ } } } }.value
Kde conforms
je funkce identity definovaná v Predef
, který se automaticky importuje do všech souborů.
A to je celé.
Jenom je potřeba dát pozor na to, že v konverzích je explicitně uvedený návratový typ nebo proveden typecast (B((i: A).v)
), jinak si kompilátor bude stěžovat poměrně kryptickou chybovou hláškou, která o problému neprozradí vůbec nic.
Pak ještě existuje explicitní verze toho samého, kdy se typecastem vynutí konverze. Není to tak elegantní, ale funguje to.
implicit def a2b(i: A): B = { p("a2b"); B(i.v) } implicit def b2c(i: B): C = { p("b2c"); C(i.v) } implicit def c2d(i: C): D = { p("c2d"); D(i.v) } implicit def d2e(i: D): E = { p("d2e"); E(i.v) } ((((A(111): B): C): D): E).value // explicitně implicitní!
A jenom pro zajímavost si můžete kód zkompilovat příkazem scalac -print
. Budete překvapeni, kolik extra tříd a kódu Scala musí generovat, aby tohle všechno bezproblémově fungovalo.