David Farrell
David Farrell

Reputation: 457

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]() ...
)

Upvotes: 1

Views: 683

Answers (4)

Alfilercio
Alfilercio

Reputation: 1118

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

Upvotes: 0

sarveshseri
sarveshseri

Reputation: 13985

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

Upvotes: 0

esse
esse

Reputation: 1551

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.

Upvotes: 0

AminMal
AminMal

Reputation: 3173

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]

Upvotes: 3

Related Questions