How does type Dynamic work and how to use it?

I heard that with Dynamic you can somehow do dynamic typing in Scala. But I can’t imagine how it might look or how it works.

I found out what inherits from trait Dynamic

 class DynImpl extends Dynamic 

The API says you can use it as follows:

foo.method ("blah") ~~> foo.applyDynamic ("method") ("blah")

But when I check it, it does not work:

 scala> (new DynImpl).method("blah") <console>:17: error: value applyDynamic is not a member of DynImpl error after rewriting to new DynImpl().<applyDynamic: error>("method") possible cause: maybe a wrong Dynamic method signature? (new DynImpl).method("blah") ^ 

This is quite logical, because, looking at the sources, it turned out that this sign is completely empty. There is no applyDynamic method, and I cannot imagine how to implement it myself.

Can someone show me what I need to do to make it work?

+86
scala
Apr 03 '13 at
source share
1 answer

The Scalas Dynamic type allows you to call methods on objects that do not exist, or, in other words, this is a “missing method” replica in dynamic languages.

That's right, scala.Dynamic does not have any members, this is just a token interface - the concrete implementation is populated by the compiler. As for the Scalas String Interpolation function, there are clearly defined rules that describe the generated implementation. In fact, four different methods can be implemented:

  • selectDynamic - allows you to record field accessories: foo.bar
  • updateDynamic - allows you to record field updates: foo.bar = 0
  • applyDynamic - allows you to call methods with arguments: foo.bar(0)
  • applyDynamicNamed - allows you to call methods with named arguments: foo.bar(f = 0)

To use one of these methods, just write a class that extends Dynamic and implement the methods there:

 class DynImpl extends Dynamic { // method implementations here } 

In addition, you need to add

 import scala.language.dynamics 

or set the compiler -language:dynamics , because the function is hidden by default.

selectDynamic

selectDynamic is the easiest way to implement. The compiler translates the call to foo.bar to foo.selectDynamic("bar") , so this method requires an argument list that expects String :

 class DynImpl extends Dynamic { def selectDynamic(name: String) = name } scala> val d = new DynImpl d: DynImpl = DynImpl@6040af64 scala> d.foo res37: String = foo scala> d.bar res38: String = bar scala> d.selectDynamic("foo") res54: String = foo 

As you can see, you can also explicitly call dynamic methods.

updateDynamic

Because updateDynamic used to update the value that this method should return Unit . In addition, the name of the field to update and its value are passed to the compiler in different argument lists:

 class DynImpl extends Dynamic { var map = Map.empty[String, Any] def selectDynamic(name: String) = map get name getOrElse sys.error("method not found") def updateDynamic(name: String)(value: Any) { map += name -> value } } scala> val d = new DynImpl d: DynImpl = DynImpl@7711a38f scala> d.foo java.lang.RuntimeException: method not found scala> d.foo = 10 d.foo: Any = 10 scala> d.foo res56: Any = 10 

The code works as expected - you can add methods at runtime. On the other hand, the code is no longer mutable, and if a method that does not exist is called, it must be processed at runtime. In addition, this code is not as useful as in dynamic languages, because it is impossible to create methods that need to be called at runtime. This means that we cannot do something like

 val name = "foo" d.$name 

where d.$name will be converted to d.foo at runtime. But this is not so bad, because even in dynamic languages ​​it is a dangerous function.

Another thing to note here is that updateDynamic must be implemented with selectDynamic . If we do not, we get a compilation error - this rule is similar to the Setter implementation, which only works if there is a Getter with the same name.

applyDynamic

The ability to call methods with arguments is provided by applyDynamic :

 class DynImpl extends Dynamic { def applyDynamic(name: String)(args: Any*) = s"method '$name' called with arguments ${args.mkString("'", "', '", "'")}" } scala> val d = new DynImpl d: DynImpl = DynImpl@766bd19d scala> d.ints(1, 2, 3) res68: String = method 'ints' called with arguments '1', '2', '3' scala> d.foo() res69: String = method 'foo' called with arguments '' scala> d.foo <console>:19: error: value selectDynamic is not a member of DynImpl 

The method name and its arguments are again divided into different parameter lists. We can call arbitrary methods with an arbitrary number of arguments if we want, but if we want to call a method without any parentheses, we need to implement selectDynamic .

Hint: You can also use apply syntax with applyDynamic :

 scala> d(5) res1: String = method 'apply' called with arguments '5' 

applyDynamicNamed

The last available method allows us to name our arguments if we want:

 class DynImpl extends Dynamic { def applyDynamicNamed(name: String)(args: (String, Any)*) = s"method '$name' called with arguments ${args.mkString("'", "', '", "'")}" } scala> val d = new DynImpl d: DynImpl = DynImpl@123810d1 scala> d.ints(i1 = 1, i2 = 2, 3) res73: String = method 'ints' called with arguments '(i1,1)', '(i2,2)', '(,3)' 

The difference in the method signature is that applyDynamicNamed expects tuples of the form (String, A) , where A is an arbitrary type.




All the above methods have a common meaning that their parameters can be parameterized:

 class DynImpl extends Dynamic { import reflect.runtime.universe._ def applyDynamic[A : TypeTag](name: String)(args: A*): A = name match { case "sum" if typeOf[A] =:= typeOf[Int] => args.asInstanceOf[Seq[Int]].sum.asInstanceOf[A] case "concat" if typeOf[A] =:= typeOf[String] => args.mkString.asInstanceOf[A] } } scala> val d = new DynImpl d: DynImpl = DynImpl@5d98e533 scala> d.sum(1, 2, 3) res0: Int = 6 scala> d.concat("a", "b", "c") res1: String = abc 

Fortunately, you can also add implicit arguments - if we add a TypeTag binding, we can easily check the types of arguments. And the best thing is that even the return type is correct - although we had to add some casts.

But Scala will not be Scala when there is no way to find a way to address such shortcomings. In our case, we can use type classes to avoid casting:

 object DynTypes { sealed abstract class DynType[A] { def exec(as: A*): A } implicit object SumType extends DynType[Int] { def exec(as: Int*): Int = as.sum } implicit object ConcatType extends DynType[String] { def exec(as: String*): String = as.mkString } } class DynImpl extends Dynamic { import reflect.runtime.universe._ import DynTypes._ def applyDynamic[A : TypeTag : DynType](name: String)(args: A*): A = name match { case "sum" if typeOf[A] =:= typeOf[Int] => implicitly[DynType[A]].exec(args: _*) case "concat" if typeOf[A] =:= typeOf[String] => implicitly[DynType[A]].exec(args: _*) } } 

As long as the implementation does not look so enjoyable, its power cannot be called into question:

 scala> val d = new DynImpl d: DynImpl = DynImpl@24a519a2 scala> d.sum(1, 2, 3) res89: Int = 6 scala> d.concat("a", "b", "c") res90: String = abc 

At the top of everything, you can also combine Dynamic with macros:

 class DynImpl extends Dynamic { import language.experimental.macros def applyDynamic[A](name: String)(args: A*): A = macro DynImpl.applyDynamic[A] } object DynImpl { import reflect.macros.Context import DynTypes._ def applyDynamic[A : c.WeakTypeTag](c: Context)(name: c.Expr[String])(args: c.Expr[A]*) = { import c.universe._ val Literal(Constant(defName: String)) = name.tree val res = defName match { case "sum" if weakTypeOf[A] =:= weakTypeOf[Int] => val seq = args map(_.tree) map { case Literal(Constant(c: Int)) => c } implicitly[DynType[Int]].exec(seq: _*) case "concat" if weakTypeOf[A] =:= weakTypeOf[String] => val seq = args map(_.tree) map { case Literal(Constant(c: String)) => c } implicitly[DynType[String]].exec(seq: _*) case _ => val seq = args map(_.tree) map { case Literal(Constant(c)) => c } c.abort(c.enclosingPosition, s"method '$defName' with args ${seq.mkString("'", "', '", "'")} doesn't exist") } c.Expr(Literal(Constant(res))) } } scala> val d = new DynImpl d: DynImpl = DynImpl@c487600 scala> d.sum(1, 2, 3) res0: Int = 6 scala> d.concat("a", "b", "c") res1: String = abc scala> d.noexist("a", "b", "c") <console>:11: error: method 'noexist' with args 'a', 'b', 'c' doesn't exist d.noexist("a", "b", "c") ^ 

Macros return all compile-time guarantees, and although this is not so useful in the above case, it may be very useful for some Scala DSL systems.

If you want even more information about Dynamic , there are a few more resources:

+175
Apr 03 '13 at
source share



All Articles