k47.cz  — každý den dokud se vám to nezačne líbit
foto Praha výběr povídky kultura
twitter FB RSS

Scala - tranzitivita implicitních konverzí

7. 2. 2011 — k47 (CC by-nc-sa)

Me­cha­nis­mus im­pli­cit­ních kon­verzí je jednou ze sil­ných strá­nek Scaly. Je bohatě po­u­ží­ván ve stan­dardní knihovně, na­pří­klad pro obo­ha­cení Ja­vov­kých typů. Právě im­pli­citní kon­verze stojí za pří­stu­pem Pimp my Lib­rary, kdy můžete vy­lep­šo­vat exis­tu­jící knihovny, aby vy­ho­věly vašim po­ža­dav­kům a mnoho dal­ších kouzel.


Na rozdíl od svého pro­tějšku v C++, jsou však bez­pečné, pro­tože nejsou glo­bální. Uplat­ňují se jenom ty kon­verze, které jsou v ak­tu­ál­ním scope do­sa­ži­telné jed­no­du­chým jménem. Takže im­pli­citní funkce A.B.C.fun musí být im­por­to­vána např. jako A.B.C._.

Scala ale ne­po­vo­luje tran­zi­ti­vitu kon­verzí z důvodu, že by se sou­bory s mnoha ty­po­vými chy­bami kom­pi­lo­valy příliš dlouho. Kom­pi­lá­tor by musel zkusit všechny kom­bi­nace všech do­stup­ných kon­verzí a to je pro­blém ve­li­kosti n^2 (viz. Pro­gra­m­ming in Scala).

Ale přesto byl v této před­nášce (video) letmo zmíněn způsob, jak si tran­zi­ti­vitu im­pli­cit­ních kon­verzí vy­nu­tit přes view­bounds.


Mějme třídy A, B, C, D, E a chceme A zkon­ver­to­vat po­stupně až na E, která ob­sa­huje po­ža­do­va­nou metodu value. Toho můžeme do­sáh­nout tak, že každá im­pli­citní kon­verze bude mít typový pa­ra­metr, který bude view­bound typu za kte­rého kon­ver­tu­jeme. Kon­verzní funkce z A na B bude tedy vy­pa­dat 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

Po­slední řádek vypíše pořadí kon­verzí, které se zdá, že je špatně.

d2e
c2d
b2c
a2b

Proč to vlastně celé fun­guje a proč je pořadí ob­rá­cené?

View­bounds [T <% A] zna­mená, že s typem T může být za­chá­zeno jako s typem A. Tedy, že exis­tuje im­pli­citní kon­verze, která dokáže typ T pře­mě­nit na A. View­bound se ex­pan­duje do jed­noho im­pli­cit­ního pa­ra­me­tru navíc.

Naše kon­verze jsou zkrat­kou pro ná­sle­du­jí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) }

Im­pli­citní ar­gu­ment funkce zá­ro­veň může být použit ve funkci, aby vy­ře­šil pří­padné typové kon­flikty. Takže naše kon­verze jsou ve sku­teč­nosti ná­sle­du­jí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) }

Kom­pi­lá­tor se pokusí najít kon­verzi na cílový typ E (který má po­ža­do­va­nou metodu value). Tady vy­ho­vuje jedině d2e, jehož první ar­gu­ment může být něco, co může být im­pli­cit­ním pa­ra­me­t­rem zkon­ver­to­váno na D. Hod­nota im­pli­cit­ního pa­ra­me­tru se bude hledat mezi im­pli­cit­ními funk­cemi. Vy­ho­vu­jící je funkce c2d, která při­jímá nějaký ar­gu­ment, který můžeme po­va­žo­vat za C (view­bound), ale zase má im­pli­citní pa­ra­metr. A tak dál. Kom­pi­lá­tor tedy po­stupně na­hra­zuje im­pli­citní pa­ra­me­try im­pli­cit­ními funk­cemi, která zase mají im­pli­citní pa­ra­me­try a tak řetězí jed­not­livé kon­verze.

Ve vý­sledku se volání A(111).value ex­pan­duje na:

d2e(A(111)){ c => c2d(c){ b => b2c(b) { a => a2b(a) { conforms _ } } } }.value

Kde conforms je funkce iden­tity de­fi­no­vaná v Predef, který se au­to­ma­ticky im­por­tuje do všech sou­borů.

A to je celé.

Jenom je po­třeba dát pozor na to, že v kon­ver­zích je ex­pli­citně uve­dený ná­vra­tový typ nebo pro­ve­den „ty­pe­cast .(B((i: A).v))“, jinak si kom­pi­lá­tor bude stě­žo­vat po­měrně kryp­tic­kou chy­bo­vou hláš­kou, která o pro­blému ne­pro­zradí vůbec nic.


Pak ještě exis­tuje ex­pli­citní verze toho samého, kdy se ty­pe­cas­tem vynutí kon­verze. Není to tak ele­gantní, ale fun­guje 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 za­jí­ma­vost si můžete kód zkom­pi­lo­vat pří­ka­zem scalac -print. Budete pře­kva­peni, kolik extra tříd a kódu Scala musí ge­ne­ro­vat, aby tohle všechno bez­pro­blé­mově fun­go­valo.

píše k47 & hosté, ascii@k47.cz