'Create a collection of case class names
I'm working in Spark 3.1 with Scala 2.12.10.
I'd like to create a collection (Seq, whatever) of case classes that have implemented a common trait, so I can execute some generic dataset code on the collection members without having to type each incantation separately. I think I can get the erased type via TypeTag, but I'm unable to define the collection in the first place!
Given these types:
trait HasID { val id: String }
case class Customer(id: String, name: String) extends HasID
case class Product(id: String, desc: Int) extends HasID
case class Sale(id: String, value: Float) extends HasID
class Appender[T <: HasID : Encoder] { ... } // dataset methods
I can call the code:
new Appender[Customer]() ...
new Appender[Product]() ...
new Appender[Sale]() ...
But what I want is to put the classes in a collection and loop through it:
Seq[HasID](Customer, Product, Sale).foreach(c =>
// get type C of c somehow
new Appender[C]() ...
)
Solution 1:[1]
This:
case class Customer(id: String) extends HasId {...}
Is compiled into something like this (not completely equal):
class Customer(val id: String) extends HasId {...}
object Customer { def apply(id: String): Customer = ??? }
So pay attention that the object is not extending HasId. Companion objects kind of hold the static members of a class, but the id here is instance-dependent. Now if you just need the companion objects, and you can use reflection or something to access the id of an actual object, I recommend you to define a class and a companion object instead of a case class:
trait HasId {
def id: String
}
trait ClassHasIdMarker
class Customer private(override val id: String) extends HasId
object Customer extends ClassHasIdMarker {
def apply(id: String): Customer = new Customer(id)
}
// These 2 are companions
// all the companion objects now extend a single trait (you can create a seq now)
// if you use reflection to access objects of the class, you can have the id
Please let me know if I understood your question correctly and if this helps!
Update
A type class would also be a good idea (as @Luis Miguel Suarez mentioned)
trait ObjectCreaterWithId[ActualType <: HasId] {
def apply(id: String): HasId
}
// class Customer ...
object Customer extends ObjectCreaterWithId[Customer] {
override def apply(id: String): HasId = new Customer(id)
}
Update #2 So if you need the runtime types, you should use reflection, it has easy APIs to use, you can take a look on the internet, here's a brief code about the same thing:
import scala.reflect.runtime.universe._
val mirror = runtimeMirror(getClass.getClassLoader)
val customerClass = classOf[Customer]
def getConstructor[T: TypeTag] =
typeOf[T].decl(termNames.CONSTRUCTOR)
.asMethod // this is the primary constructor
val classMirror = mirror.reflectClass(mirror.classSymbol(customerClass))
val instance = classMirror.reflectConstructor(getConstructor[Customer]).apply("two").asInstanceOf[Customer]
Solution 2:[2]
Actually, it works if you change the type Seq[HasID]
to Seq[String => HasID]
. You don't need a ObjectCreaterWithId
or HasIDMaker
at all:
scala> val objs = Seq[String => HasID](Customer, Product, Sale)
val objs: Seq[String => HasID] = List(Customer, Product, Sale)
// And then you can create a list of instances by objs
scala> val ids = objs map (_ ("one"))
val ids: Seq[HasID] = List(Customer(one), Product(one), Sale(one))
EDIT: @aminmal, talk is cheep, check the code yourself.
Welcome to Scala 2.13.8 (OpenJDK 64-Bit Server VM, Java 1.8.0_312).
Type in expressions for evaluation. Or try :help.
scala> case class A(i: Int)
class A
// Note: A.type is a subclass of Function1.
// SO, YES **object A is An instance of function Int => A**
scala> classOf[A.type].getSuperclass()
val res0: Class[_ >: A.type] = class scala.runtime.AbstractFunction1
By the way, there is a question Why do case class companion objects extend FunctionN? answered by the scala creator Martin Odersky.
Solution 3:[3]
When you say that you are failing to create the collection itself, are you talking about the collection Seq[HasID](Customer, Product, Sale)
?
This part will cause an error because Customer
, Product
, Sale
in Seq[HasID](Customer, Product, Sale)
are actually compaion objects of those case classes. And these companion objects have no relation so HasId
.
If you want to create a collection ofhave instances of Customer
, Product
and Sale
then you will have to put instances of these classes inside the Seq/List.
val list = List[HasID](
Customer("c1", "c1"),
Product("p1", 1),
Sale("s1", 1.0F)
)
Now, we can focus on your actual objective.
You can use the canonical class names as the keys when you are creating your map for appenders.
trait HasID {
val id: String
}
case class Customer(id: String, name: String) extends HasID
case class Product(id: String, desc: Int) extends HasID
case class Sale(id: String, value: Float) extends HasID
import java.security.InvalidParameterException
abstract class Appender[T <: HasID] {
def append(item: T): Unit
def appendHasId(item: HasID): Unit = item match {
case t: T => append(t)
case _ => throw new InvalidParameterException()
}
}
val appenders = Map(
classOf[Customer].getCanonicalName -> new Appender[Customer] {
override def append(item: Customer): Unit = println("appended Customer")
},
classOf[Product].getCanonicalName -> new Appender[Product] {
override def append(item: Product): Unit = println("appended Product")
},
classOf[Sale].getCanonicalName -> new Appender[Sale] {
override def append(item: Sale): Unit = println("appended Sale")
}
)
val list = List[HasID](
Customer("c1", "c1"),
Product("p1", 1),
Sale("s1", 1.0F)
)
list.foreach { x => appenders(x.getClass.getCanonicalName).appendHasId(x) }
Output:
appended Customer
appended Product
appended Sale
Solution 4:[4]
val appendCustomer = new Appender[Customer]()
val appendProduct = new Appender[Product]()
val appendSale = new Appender[Sale]()
//get your instances
val aCustomer: Custome = Customer(....)
val aProduct = Product(...)
val aSale = Sale()
//loop throw them and apply the correspondent appender
Seq[HasID](aCustomer, aProduct, aSale).foreach {
case x: Customer => appenderCustomer.method(x)
case x: Product => appenderProduct.method(x)
case x: Sale => appenderSale.method(x)
)
I've used that logic but apply whatever you need in the right side of the pattern maching
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | |
Solution 3 | sarveshseri |
Solution 4 | Alfilercio |