Scala - trait Dynamic

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.Expression
z C#), 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