Ausgabe
Angenommen, ich habe diese Hierarchie
trait Base {
val tag: String
}
case class Derived1(tag: String = "Derived 1") extends Base
case class Derived2(tag: String = "Derived 2") extends Base
//etc ...
und ich möchte Methode mit folgender Signatur definieren
def tag[T <: Base](instance: T, tag: String): T
T
die eine Instanz des Typs mit modify zurückgibt tag: String
. Wenn also zB eine Derived1
Instanz übergeben wird, wird eine modifizierte Instanz des gleichen Typs zurückgegeben.
Dieses Ziel könnte leicht erreicht werden, indem veränderliche tag
Variablen verwendet werden var tag: String
. Wie erreicht man das gewünschte Verhalten mit Scala und funktionaler Programmierung?
Mein Gedanke:
Ich könnte eine Typklasse und ihre Instanzen erstellen
trait Tagger[T] {
def tag(t: T, state: String): T
}
implicit object TaggerDerived1 extends Tagger[Derived1] {
override def tag(t: Derived1, state: String): Derived1 = ???
}
implicit object TaggerDerived2 extends Tagger[Derived2] {
override def tag(t: Derived2, state: String): Derived2 = ???
}
implicit object TaggerBase extends Tagger[Base] {
override def tag(t: Base, state: String): Base = ???
}
und eine Methode
def tag[T <: Base](instance: T, tag: String)(implicit tagger: Tagger[T]): T = tagger.tag(instance, tag)
Dies ist nicht ideal, da sich der Benutzer zunächst dessen bewusst sein muss, wenn er seine eigenen abgeleiteten Klassen definiert. Wenn keine definiert wird, würde die implizite Auflösung auf die Basisimplementierung zurückgreifen und den zurückgegebenen Typ einschränken.
case class Derived3(tag: String = "Derived 3") extends Base
tag(Derived3(), "test") // falls back to `tag[Base](...)`
Jetzt neige ich dazu, den veränderlichen Zustand zu verwenden, indem ich var tag: String
. Ich würde jedoch gerne einige Meinungen hören, wie dies rein funktional in Scala gelöst werden kann.
Lösung
Sie können Ihre Typklasse ableitenTagger
, und dann müssen die Benutzer ihre Instanzen nicht für jede neue Fallklassenerweiterung definierenBase
// libraryDependencies += "com.chuusai" %% "shapeless" % "2.3.10"
import shapeless.labelled.{FieldType, field}
import shapeless.{::, HList, HNil, LabelledGeneric, Witness}
trait Tagger[T] {
def tag(t: T, state: String): T
}
trait LowPriorityTagger {
implicit def notTagFieldTagger[K <: Symbol : Witness.Aux, V, T <: HList](implicit
tagger: Tagger[T]
): Tagger[FieldType[K, V] :: T] =
(t, state) => t.head :: tagger.tag(t.tail, state)
}
object Tagger extends LowPriorityTagger {
implicit def genericTagger[T <: Base with Product, L <: HList](implicit
generic: LabelledGeneric.Aux[T, L],
tagger: Tagger[L]
): Tagger[T] = (t, state) => generic.from(tagger.tag(generic.to(t), state))
implicit val hnilTagger: Tagger[HNil] = (_, _) => HNil
implicit def tagFieldTagger[T <: HList]:
Tagger[FieldType[Witness.`'tag`.T, String] :: T] =
(t, state) => field[Witness.`'tag`.T](state) :: t.tail
}
case class Derived1(tag: String = "Derived 1") extends Base
case class Derived2(tag: String = "Derived 2") extends Base
case class Derived3(i: Int, tag: String = "Derived 3", s: String) extends Base
tag(Derived1("aaa"), "bbb") // Derived1(bbb)
tag(Derived2("ccc"), "ddd") // Derived2(ddd)
tag(Derived3(1, "ccc", "xxx"), "ddd") // Derived3(1,ddd,xxx)
Alternativ können Sie für Einzelparameter-Case-Klassen einschränken T
, dass dies der Fall ist.copy
import scala.language.reflectiveCalls
def tag[T <: Base {def copy(tag: String): T}](instance: T, tag: String): T =
instance.copy(tag = tag)
Bei Fallklassen mit mehreren Parametern ist es schwieriger, die Existenz von in Typen auszudrücken, .copy
da die Methodensignatur unbekannt wird (zu berechnen ist).
Damit kannst du tag
ein Makro erstellen
// libraryDependencies += scalaOrganization.value % "scala-reflect" % scalaVersion.value
import scala.language.experimental.macros
import scala.reflect.macros.blackbox
def tag[T <: Base](instance: T, tag: String): T = macro tagImpl
def tagImpl(c: blackbox.Context)(instance: c.Tree, tag: c.Tree): c.Tree = {
import c.universe._
q"$instance.copy(tag = $tag)"
}
Oder Sie können die Laufzeitreflexion verwenden (Java oder Scala, mit Product
oder ohne Funktionalität) .
import scala.reflect.{ClassTag, classTag}
import scala.reflect.runtime.{currentMirror => rm}
import scala.reflect.runtime.universe.{TermName, termNames}
def tag[T <: Base with Product : ClassTag](instance: T, tag: String): T = {
// Product
val values = instance.productElementNames.zip(instance.productIterator)
.map {case fieldName -> fieldValue => if (fieldName == "tag") tag else fieldValue}.toSeq
// Java reflection
// val clazz = instance.getClass
// clazz.getMethods.find(_.getName == "copy").get.invoke(instance, values: _*).asInstanceOf[T]
// clazz.getConstructors.head.newInstance(values: _*).asInstanceOf[T]
// Scala reflection
val clazz = classTag[T].runtimeClass
val classSymbol = rm.classSymbol(clazz)
// val copyMethodSymbol = classSymbol.typeSignature.decl(TermName("copy")).asMethod
// rm.reflect(instance).reflectMethod(copyMethodSymbol)(values: _*).asInstanceOf[T]
val constructorSymbol = classSymbol.typeSignature.decl(termNames.CONSTRUCTOR).asMethod
rm.reflectClass(classSymbol).reflectConstructor(constructorSymbol)(values: _*).asInstanceOf[T]
}
Beantwortet von – Dmytro Mitin
Antwort geprüft von – Clifford M. (FixError Volunteer)