k47.cz
mastodon twitter RSS
bandcamp explorer

Scala - trait Dynamic

23. 6. 2011 (před 12 lety) — k47 (CC by-nc)

Scala mě nikdy nepřestane udivovat.

Před nějakou dobou jsem porovnával Scalu s C# s tím výsledkem, že naprostá většina z široké palety vlastností C# se dá pohodlně vyjádřit ve Scale. Jednou z pár „chybějících vlastností .(chybějící ve smyslu chybějící ve výčtu, ne že by po ní všichni toužili)“, byla obdoba pseudotypu dynamic.

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


C# ve verzi 4.0 zavedl pseudotyp dynamic, který je považován za System.Object s tím rozdílem, že všechna volání metod jsou na něm dovolena bez jakékoli typové kontroly a hledání odpovídajících metod je prováděno až během runtime.

Scala ve verzi 2.9 zavádí trait Dynamic, který se chová jako každý spořádaný trait a jenom lehce pozmění chování kompilátoru. Ten, když zjistí, že se na potomkovi typu Dynamic snažíme volat neexistující metodu, přepíše volání d.method(args) na d.applyDynamic("method")(args). Nikde není řeč o dynamickém volání metod a reflexi. Scala Dynamic je „obecný nástroj, .(Mluvil jsem už o jazycích širokých a hlubokých? C# je široký, Scala hluboký)“, který si můžeme snadno přiohnout, aby dělal skoro to samé, co C# dynamic.


Jelikož jde o experimentální funkci, musíte kompilátor/REPL nakrmit parametrem -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 syntakticky sladké způsoby vytváření nebo procházení strukturovaných dat jako např. JSON nebo XML.

Např: následující JSON:

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

Se bude procházet třeba takhle:

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

Pokud bysme chtěli C# kopírovat přesně, můžeme si jednoduchou proxy pro dynamické metody udělat následovně:

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 posledního řádku, implicitní konverze se neberou v potaz a to je s přihlédnutám k faktu, že Scala jich masivně využívá, docela podpásovka. Řešení je teoreticky snadné, teoreticky: za běhu najít pomocí reflexe odpovídající implicitní konverzi, přes reflexi ji vykonat a pak reflexí zavolat tu správnou metodu. Ale to bohužel bez nativní Scalovské reflexe (a kusu kompilátoru ve stanardní knihovně) nepůjde. Dalším problémem jsou implicitní parametry a hodnoty výchozích parametrů.

Pro dynamické chování objektů Dynamic nebude úplně ideální. Ale ani nemusí, protože pro duck typing existuje lepší řešení: strukturální typy, které odvedou stejnou práci + jsou typově bezpeč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

Poslední řádek funguje kvůli tomu, že kompilátor provede implicitní konverzi argumentu před tím, než ho pošle funkci. Jde o běžné chování, ale než mi to docvaklo, musel jsem pročítat zdrojáky Scaly, dekompilovat, luštit bytekó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 dvakrát nepálí.


Další věc kterou C# dynamic zajišťuje, je dynamický výběr přetížené metody, které je dynamic předán jako argument.

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)

Takového chování není možné ve Scale docílit jenom tím, že Dynamic bude v seznamu argumentů. Ten jenom připisuje metody, které jsou na něm volány, nemění okolní kontext. Kdybychom přesto toužili právě po tomhle chování, museli bychom ho simulovat 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")

Poslední však ukázka nebude fungovat. Jsou v ní problémy s dynamickým hledáním metod s primitivními a generickými typy argumentů. Hodilo by se něco jako runtimeClass (a tadá).


PS: Scala je samé tajemství a překvapení. V přednášce o bytecode generátoru Mnemonics jsem se dozvěděl o další zajímavé (a experimentální a nedokumentované) funkcionalitě scala.reflect.Code (obdoba System.Linq.Expressions.ExpressionC#), která z funkčního objektu 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, ascii@k47.cz