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 - trait Dynamic

23. 6. 2011 — k47 (CC by-nc)

Scala mě nikdy ne­pře­stane udi­vo­vat.

Před ně­ja­kou dobou jsem po­rov­ná­val Scalu s C# s tím vý­sled­kem, že na­prostá vět­šina z široké palety vlast­ností C# se dá po­ho­dlně vy­já­d­řit ve Scale. Jednou z pár chy­bě­jí­cích vlast­ností, byla obdoba pseu­do­typu dy­na­mic.

Ale há­dejte co? V po­slední verzi Scaly je možné i tohle. Skoro. Scala jde na věc jinak.


C# ve verzi 4.0 zavedl pseu­do­typ dynamic, který je po­va­žo­ván za System.Object s tím roz­dí­lem, že všechna volání metod jsou na něm do­vo­lena bez ja­ké­koli typové kon­t­roly a hle­dání od­po­ví­da­jí­cích metod je pro­vá­děno až během run­time.

Scala ve verzi 2.9 zavádí trait Dy­na­mic, který se chová jako každý spo­řá­daný trait a jenom lehce po­změní cho­vání kom­pi­lá­toru. Ten, když zjistí, že se na po­tom­kovi typu Dy­na­mic sna­žíme volat ne­e­xis­tu­jící metodu, pře­píše volání d.method(args) na d.applyDynamic("method")(args). Nikde není řeč o dy­na­mic­kém volání metod a re­flexi. Scala Dy­na­mic je obecný ná­stroj,, který si můžeme snadno při­ohnout, aby dělal skoro to samé, co C# dy­na­mic.


Je­li­kož jde o ex­pe­ri­men­tální funkci, musíte kom­pi­lá­tor/REPL na­kr­mit pa­ra­me­t­rem -Xexperimental.

class DynTest extends Dynamic {
  def ordinaryMethod = println("this is ordinary method")
  def applyDynamic(method: String)(args: Any*) = println("dynamic method "+method+"("+args.mkString(" ")+")")
}
val dyn = new DynTest

dyn.ordinaryMethod
dyn.dynamicMethod(1, 2, 3) // přeloží se na dyn.applyDynamic("dynamicMethod")(1, 2, 3)

Dynamic by se mohl hodit pro syn­tak­ticky sladké způ­soby vy­tvá­ření nebo pro­chá­zení struk­tu­ro­va­ných dat jako např. JSON nebo XML.

Např: ná­sle­du­jící JSON:

{
  "name": "Anon",
  "posts": [
    { "title": "Post #1", "text": "lorem ipsum" },
    { "title": "Post #2", "text": "another lorem ipsum" }
  ]
}

Se bude pro­chá­zet třeba takhle:

jsonData.name             // "Anon"
jsonData.posts(0).title   // "Post #1"


Pokud bysme chtěli C# ko­pí­ro­vat přesně, můžeme si jed­no­du­chou proxy pro dy­na­mické metody udělat ná­sle­dovně:

case class DynProxy(value: Any) extends Dynamic {
  def applyDynamic(method: String)(args: Any*): Any =
    value.asInstanceOf[AnyRef]
      .getClass
      .getMethod(method, args.map(_.asInstanceOf[AnyRef].getClass): _*)
      .invoke(value, args.map(_.asInstanceOf[AnyRef]) : _*)
}
object DynProxy {
  implicit def dynamically(v: Any) = DynProxy(v)
}

// ***

def test(a: DynProxy) = a.length

test("string")
test(List(1,2,3))
test(Option(47))  // runtime výjimka NoSuchMethodException
test(Array(47))   // ouha! pole nemá metodu length, tu mu zajišťuje až implicitní konverze na ArrayOps

Jak je vidět z po­sled­ního řádku, im­pli­citní kon­verze se ne­be­rou v potaz a to je s při­hléd­nu­tám k faktu, že Scala jich masivně vy­u­žívá, docela pod­pá­sovka. Řešení je te­o­re­ticky snadné, te­o­re­ticky: za běhu najít pomocí re­flexe od­po­ví­da­jící im­pli­citní kon­verzi, přes re­flexi ji vy­ko­nat a pak re­flexí za­vo­lat tu správ­nou metodu. Ale to bo­hu­žel bez na­tivní Sca­lov­ské re­flexe (a kusu kom­pi­lá­toru ve sta­nardní knihovně) ne­pů­jde. Dalším pro­blé­mem jsou im­pli­citní pa­ra­me­try a hod­noty vý­cho­zích pa­ra­me­trů.

Pro dy­na­mické cho­vání ob­jektů Dy­na­mic nebude úplně ide­ální. Ale ani nemusí, pro­tože pro duck typing exis­tuje lepší řešení: struk­tu­rální typy, které od­ve­dou stej­nou práci + jsou typově bez­pečné.

def test(a: { def length: Int }) = a.length

test("string")
test(List(1,2,3))
test(Option(47))  // chyba odhalena už během kompilace
test(Array(47))   // najednou všechno funguje

Po­slední řádek fun­guje kvůli tomu, že kom­pi­lá­tor pro­vede im­pli­citní kon­verzi ar­gu­mentu před tím, než ho pošle funkci. Jde o běžné cho­vání, ale než mi to do­cvaklo, musel jsem pro­čí­tat zdro­jáky Scaly, de­kom­pi­lo­vat, luštit by­te­kód a hledat, kde se děje všechna magie, než jsem si řekl: „co kdyby to bylo jinak?“. No jo, někdy mi to zrovna dva­krát nepálí.


Další věc kterou C# dynamic za­jiš­ťuje, je dy­na­mický výběr pře­tí­žené metody, které je dynamic předán jako ar­gu­ment.

void Print(dynamic obj) {
  System.Console.WriteLine(obj); // která přetížená WriteLine() se zavolá, se rozhodna v runtime
}

Print(123);   // zavolá WriteLine(int)
Print("abc"); // zavolá WriteLine(string)

Ta­ko­vého cho­vání není možné ve Scale do­cí­lit jenom tím, že Dy­na­mic bude v se­znamu ar­gu­mentů. Ten jenom při­pi­suje metody, které jsou na něm volány, nemění okolní kon­text. Kdy­bychom přesto tou­žili právě po tomhle cho­vání, museli bychom ho si­mu­lo­vat nějak takto:

object Test {
  def testPrint(a: String) = println("string")
  def testPrint(a: Int)    = println("int")
}

def print(d: DynProxy) {
  DynProxy.dynamicaly(Test).testPrint(d.value) // musíme zdynamiovat příjemce metody
}

print(1)
print("asdf")

Po­slední však ukázka nebude fun­go­vat. Jsou v ní pro­blémy s dy­na­mic­kým hle­dá­ním metod s pri­mi­tiv­ními a ge­ne­ric­kými typy ar­gu­mentů. Hodilo by se něco jako run­ti­meC­lass (a „tadá“:[class).


PS: Scala je samé ta­jem­ství a pře­kva­pení. V před­nášce o by­te­code ge­ne­rá­toru Mne­mo­nics jsem se do­zvě­děl o další za­jí­mavé (a ex­pe­ri­men­tální a ne­do­ku­men­to­vané) funk­ci­o­na­litě scala.reflect.Code (obdoba System.Linq.Expressions.ExpressionC#), která z funkč­ního ob­jektu získá jeho parse tree.

val f: reflect.Code[Int => Int] = i => i * 100 // kompilátor zkonvertuje kód funkce na AST
f.tree // obsahuje celý AST strom


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