Il tipo Scalas Dynamic
consente di chiamare metodi su oggetti che non esistono o in altre parole è una replica del "metodo mancante" nei linguaggi dinamici.
È corretto, scala.Dynamic
non ha membri, è solo un'interfaccia marker: l'implementazione concreta è compilata dal compilatore. Per quanto riguarda la funzionalità di interpolazione delle stringhe di Scalas , esistono regole ben definite che descrivono l'implementazione generata. Si possono infatti implementare quattro diversi metodi:
selectDynamic
- consente di scrivere funzioni di accesso al campo: foo.bar
updateDynamic
- permette di scrivere aggiornamenti di campo: foo.bar = 0
applyDynamic
- permette di chiamare metodi con argomenti: foo.bar(0)
applyDynamicNamed
- permette di chiamare metodi con argomenti denominati: foo.bar(f = 0)
Per utilizzare uno di questi metodi è sufficiente scrivere una classe che si estende Dynamic
e implementare i metodi lì:
class DynImpl extends Dynamic {
// method implementations here
}
Inoltre è necessario aggiungere un file
import scala.language.dynamics
oppure imposta l'opzione del compilatore -language:dynamics
perché la funzionalità è nascosta per impostazione predefinita.
selectDynamic
selectDynamic
è il più semplice da implementare. Il compilatore traduce una chiamata di foo.bar
in foo.selectDynamic("bar")
, quindi è necessario che questo metodo abbia un elenco di argomenti in attesa di 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
Come si può vedere, è anche possibile chiamare esplicitamente i metodi dinamici.
updateDynamic
Poiché updateDynamic
viene utilizzato per aggiornare un valore, questo metodo deve restituire Unit
. Inoltre, il nome del campo da aggiornare e il suo valore vengono passati a diversi elenchi di argomenti dal compilatore:
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
Il codice funziona come previsto: è possibile aggiungere metodi in fase di esecuzione al codice. D'altra parte, il codice non è più tipizzato e se viene chiamato un metodo che non esiste, questo deve essere gestito anche in fase di esecuzione. Inoltre, questo codice non è utile come nei linguaggi dinamici perché non è possibile creare i metodi che dovrebbero essere chiamati in fase di esecuzione. Ciò significa che non possiamo fare qualcosa di simile
val name = "foo"
d.$name
dove d.$name
verrebbe trasformato d.foo
in runtime. Ma non è così male perché anche nei linguaggi dinamici questa è una caratteristica pericolosa.
Un'altra cosa da notare qui è che updateDynamic
deve essere implementata insieme a selectDynamic
. Se non lo facciamo otterremo un errore di compilazione - questa regola è simile all'implementazione di un Setter, che funziona solo se c'è un Getter con lo stesso nome.
applyDynamic
La possibilità di chiamare metodi con argomenti è fornita da 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
Il nome del metodo e i suoi argomenti vengono nuovamente separati in diversi elenchi di parametri. Possiamo chiamare metodi arbitrari con un numero arbitrario di argomenti se vogliamo, ma se vogliamo chiamare un metodo senza parentesi dobbiamo implementarlo selectDynamic
.
Suggerimento: è anche possibile utilizzare la sintassi di applicazione con applyDynamic
:
scala> d(5)
res1: String = method 'apply' called with arguments '5'
applyDynamicNamed
L'ultimo metodo disponibile ci permette di nominare i nostri argomenti se vogliamo:
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)'
La differenza nella firma del metodo è che si applyDynamicNamed
aspetta tuple del modulo (String, A)
dove A
è un tipo arbitrario.
Tutti i metodi di cui sopra hanno in comune che i loro parametri possono essere parametrizzati:
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
Fortunatamente, è anche possibile aggiungere argomenti impliciti - se aggiungiamo un TypeTag
contesto vincolato possiamo facilmente controllare i tipi degli argomenti. E la cosa migliore è che anche il tipo restituito è corretto, anche se abbiamo dovuto aggiungere alcuni cast.
Ma Scala non sarebbe Scala quando non c'è modo di trovare un modo per aggirare tali difetti. Nel nostro caso possiamo usare classi di tipo per evitare i cast:
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: _*)
}
}
Anche se l'implementazione non sembra così bella, il suo potere non può essere messo in dubbio:
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
Innanzitutto è possibile combinare anche Dynamic
con le macro:
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")
^
Le macro ci restituiscono tutte le garanzie sui tempi di compilazione e sebbene non sia così utile nel caso precedente, forse può essere molto utile per alcuni DSL Scala.
Se vuoi avere ancora più informazioni, Dynamic
ci sono altre risorse: