1

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]() ...
)
David Farrell
  • 427
  • 6
  • 16
  • 1
    The `Sale` object does not extend `HasID` - In any case, the language does not provide any way to talk or reference companion objects of a class or _vice versa_; from the language point of view those types are completely unrelated. - Usually, you may use a typeclass instead, but those are considered a bad practice in **Spark**, so I guess reflection is the accepted way to solve whatever you are trying to solve. – Luis Miguel Mejía Suárez Apr 26 '22 at 20:59
  • 1
    Assume your code compiles, how do you plan to use `objs`? Also auto generated companion object from case class does not extend that HasID. The instance class does, but the companion object does not. – SwiftMango Apr 26 '22 at 21:48
  • `Customer`, `Product`, `Sale` in `Seq[HasID](Customer, Product, Sale)` are the companion objects of respective classes. And these companion objects do not extend `HasId`, so how is this part of code even compiling ? – sarveshseri May 05 '22 at 07:18

4 Answers4

3

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]
AminMal
  • 3,070
  • 2
  • 6
  • 15
0

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.

esse
  • 1,415
  • 5
  • 10
  • That's very interesting, thank you! I realized my classes actually have different shapes, have updated the question with more detail. – David Farrell Apr 27 '22 at 21:16
  • No it does not, why on earth would object Customer have type [String => HasID]?? Is it a function? or is it a trait with single unimplemented method? I wanted to make sure, so I tried it with Scala 2.13.8, I get `type mismatch error, required: String => HasID, found: Customer.type`. And even if it compiled, it wouldn't work anymore if any of these classes had an static method (I mean in companion object) – AminMal Apr 28 '22 at 14:47
  • In my original question each class had a shape of String, and the code in this answer worked. – David Farrell Apr 28 '22 at 19:03
  • 1
    @DavidFarrell, this pretty much depends on the Scala version, in some of them it gets a compile time error (like scala 2.3.18 which is the latest version), in some versions it does something so called “auto insertion”, which is also deprecated, compiler replaces the object with its apply method, so its not something to rely on (it’s not even the companion object). See more here: https://scastie.scala-lang.org/revEfGMbRgizNO0HMr6BCA – AminMal Apr 28 '22 at 19:52
  • @AminMal I add a note in the answer to reply your comment of "No it does not, why on earth would object Customer have type [String => HasID]?? Is it a function? or is it a trait with single unimplemented method? I wanted to make sure, so I tried it with Scala 2.13.8, I get type mismatch error, required: String => HasID, found: Customer.type." – esse Apr 29 '22 at 02:02
  • @esse Yes of course, as far as you don't do anything with companion object, try this inside your REPL: `case class Person(name: String); object Person; Person.isInstanceOf[String => Person]`. you'll get false, even when you have not defined any static method or value inside the companion object. Do you agree that object Person is still a companion to class Person? If yes, as I mentioned earlier, only in some scala versions, only in some cases, your assumption is true, so it's not true in all the cases. – AminMal Apr 29 '22 at 10:28
0

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
sarveshseri
  • 13,738
  • 28
  • 47
0
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

Alfilercio
  • 1,088
  • 6
  • 13