I know I can add dynamic "fields" like this:
import collection.mutable
class DynamicType extends Dynamic {
private val fields = mutable.Map.empty[String, Any].withDefault {key => throw new NoSuchFieldError(key)}
def selectDynamic(key: String) = fields(key)
def updateDynamic(key: String)(value: Any) = fields(key) = value
def applyDynamic(key: String)(args: Any*) = fields(key)
}
I can then do stuff like this:
val foo = new DynamicType
foo.age = 23
foo.name = "Rick"
But, I want to extend this one step farther and add dynamic methods e.g:
foo.greet = (name: String) => s"Nice to meet you $name, my name is ${this.name}"
foo.greet("Nat"); //should return "Nice to meet you Nat, my name is Rick"
I tried storing all methods in separate map in updateDynamic but I could not figure out a generic way to handle the arity problem. So is there a way to use Macros + Dynamics to have something like this?
EDIT: Based on @Petr Pudlak's answer, I tried implementing something like this:
import collection.mutable
import DynamicType._
/**
* An useful dynamic type that let's you add/delete fields and methods during runtime to a structure
*/
class DynamicType extends Dynamic {
private val fields = mutable.Map.empty[String, Any] withDefault { key => throw new NoSuchFieldError(key) }
private val methods = mutable.Map.empty[String, GenFn] withDefault { key => throw new NoSuchMethodError(key) }
def selectDynamic(key: String) = fields(key)
def updateDynamic(key: String)(value: Any) = value match {
case fn0: Function0[Any] => methods(key) = {case Seq() => fn0()}
case fn1: Function1[Any, Any] => methods(key) = fn1
case fn2: Function2[Any, Any, Any] => methods(key) = fn2
case _ => fields(key) = value
}
def applyDynamic(key: String)(args: Any*) = methods(key)(args)
/**
* Deletes a field (methods are fields too)
* @return the old field value
*/
def delete(key: String) = fields.remove(key)
//todo: export/print to json
}
object DynamicType {
import reflect.ClassTag
type GenFn = PartialFunction[Seq[Any],Any]
implicit def toGenFn1[A: ClassTag](f: (A) => Any): GenFn = { case Seq(a: A) => f(a) }
implicit def toGenFn2[A: ClassTag, B: ClassTag](f: (A, B) => Any): GenFn = { case Seq(a: A, b: B) => f(a, b) }
// todo: generalize to 22-args
}
Full code here
1) It correctly handles fields vs methods (even 0-args) but is quite verbose (currently works upto 2 arg methods only). Is there anyway to simplify my code?
2) Is there anyway to support dynamic method overloading (e.g. adding 2 dynamic methods with different signatures?) If I can get the signature of the function, I can use that as a key in my methods map.
In order to do that, we have to solve two problems:
Here is one possibility:
First, let's define the most generic function type: Take any number of any arguments and produce a result, or fail, if the number or types of the arguments dont' match:
type GenFn = PartialFunction[Seq[Any],Any]
Now we create a dynamic type where everything is GenFn:
class DynamicType extends Dynamic {
import collection.mutable
private val fields =
mutable.Map.empty[String,GenFn]
.withDefault{ key => throw new NoSuchFieldError(key) }
def selectDynamic(key: String) = fields(key)
def updateDynamic(key: String)(value: GenFn) = fields(key) = value
def applyDynamic(key: String)(args: Any*) = fields(key)(args);
}
Next, let's create implicit conversions that convert functions of different arities into this type:
import scala.reflect.ClassTag
implicit def toGenFn0(f: => Any): GenFn =
{ case Seq() => f; }
implicit def toGenFn1[A: ClassTag](f: (A) => Any): GenFn =
{ case Seq(x1: A) => f(x1); }
implicit def toGenFn2[A: ClassTag,B: ClassTag](f: (A,B) => Any): GenFn =
{ case Seq(x1: A, x2: B) => f(x1, x2); }
// ... other arities ...
Each conversion converts a function (or a value) to a GenFn - a partial function, which fails if it's given a wrong number/types of arguments.
We use ClassTag in order to be able to match the correct types of arguments. Note that we treat values as functions of zero arity. This way we deal with 2. at the cost of using retrieving values by giving zero arguments as in name().
Finally, we can do something like:
val foo = new DynamicType
foo.name = "Rick"
foo.greet = (name: String) =>
s"Nice to meet you $name, my name is ${foo.name()}"
println(foo.greet("Nat"));
In order to support method overloading, all we need is to chain PartialFunctions. This canbe accomplished as
def updateDynamic(key: String)(value: GenFn) =
fields.get(key) match {
case None => fields(key) = value
case Some(f) => fields(key) = f.orElse(value);
}
(note that it isn't thread safe). Then we can call something like
val foo = new DynamicType
foo.name = "Rick"
foo.greet = (name: String)
=> s"Nice to meet you $name, my name is ${foo.name()}"
foo.greet = (firstName: String, surname: String)
=> s"Nice to meet you $firstName $surname, my name is ${foo.name()}"
println(foo.greet("Nat"));
println(foo.greet("Nat Smith"));
Note that this solution works a bit differently than the standard method overloading. Here it depends on the order in which functions are added. If a more general function is added first, the more specific one will never be invoked. So always add more specific functions first.
It will be probably more difficult the way you've done it, because it seems you don't distinguish the types of functions (like my toGenFn... methods do), so if a function gets wrong arguments, it'll just throw an exception instead of passing them to the next in line. But it should work with functions with different number of arguments.
I don't think it's possible to avoid the verbosity caused by examining functions of various arguments, but I don't think it really matters. This is just a one-time work, the clients of DynamicType aren't affected by it.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With