Come funziona il tipo dinamico e come si usa?


95

Ho sentito che con Dynamicesso è in qualche modo possibile eseguire la digitazione dinamica in Scala. Ma non riesco a immaginare come potrebbe apparire o come funziona.

Ho scoperto che si può ereditare dal tratto Dynamic

class DynImpl extends Dynamic

L' API dice che si può usarlo in questo modo:

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

Ma quando lo provo non funziona:

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")
               ^

Questo è completamente logico, perché dopo aver esaminato le fonti , si è scoperto che questo tratto è completamente vuoto. Non esiste un metodo applyDynamicdefinito e non riesco a immaginare come implementarlo da solo.

Qualcuno può mostrarmi cosa devo fare per farlo funzionare?

Risposte:


188

Il tipo Scalas Dynamicconsente di chiamare metodi su oggetti che non esistono o in altre parole è una replica del "metodo mancante" nei linguaggi dinamici.

È corretto, scala.Dynamicnon 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 Dynamice 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:dynamicsperché la funzionalità è nascosta per impostazione predefinita.

selectDynamic

selectDynamicè il più semplice da implementare. Il compilatore traduce una chiamata di foo.barin 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é updateDynamicviene 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.$nameverrebbe trasformato d.fooin runtime. Ma non è così male perché anche nei linguaggi dinamici questa è una caratteristica pericolosa.

Un'altra cosa da notare qui è che updateDynamicdeve 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 applyDynamicNamedaspetta 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 TypeTagcontesto 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 Dynamiccon 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, Dynamicci sono altre risorse:


1
Sicuramente un'ottima risposta e una vetrina di Scala Power
Herrington Darkholme

Non lo chiamerei potere nel caso in cui la funzione sia nascosta per impostazione predefinita, ad esempio potrebbe essere sperimentale o non giocare bene con gli altri, o no?
matante

Esistono informazioni sulle prestazioni di Scala Dynamic? So che Scala Reflection è lento (quindi nasce Scala-macro). L'uso di Scala Dynamic rallenterà notevolmente le prestazioni?
windweller

1
@AllenNie Come puoi vedere nella mia risposta, ci sono diversi modi per implementarlo. Se utilizzi le macro, non c'è più alcun sovraccarico, poiché la chiamata dinamica viene risolta in fase di compilazione. Se utilizzi do checks in fase di esecuzione, devi eseguire il controllo dei parametri per inviare correttamente il percorso del codice corretto. Questo non dovrebbe essere più sovraccarico di qualsiasi altro controllo dei parametri nella tua applicazione. Se usi la riflessione, ottieni ovviamente più overhead ma devi misurare da solo quanto rallenta la tua applicazione.
kiritsuku

1
"Le macro ci restituiscono tutte le garanzie sui tempi di compilazione" - questo mi fa
impazzire
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.