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


Scala - trait Dynamic 📷

publikováno 23. 6. 2011 od k47

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é .(Do you see what I did there?)" 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á":[class).


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

- Scala 2.9RC2 – applyDynamic
- Scala's upcoming dynamic capabilities
- Ukázka



vstoupit do diskuze    sdílet na facebooku, twitteru, google+

publikováno 23. 6. 2011

příbuzné články:
ScalaQuery 📷
Scala - typově bezpečné eventy 📷
Scala type bounds vs. C# constraints on type parameters 📷
Scala for expression vs. C# LINQ
Scala - tranzitivita implicitních konverzí 📷
Scala versus C# 4.0 - Strukturální typy versus Dynamic member lookup 📷

sem odkazují:
Co je dneska k večeři? Zase Scala!

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