-1

Suppose I have several auto-generated classes, like MyEnum1, MyEnum2, ... (they are not necessarily Scala enum types, just some auto-generated classes). Though the type of MyEnum1 is different than the type of MyEnum2 (and they share no auto-generated parent types except Any), I can guarantee that all of these auto-generated types have exactly the same public, static methods available, in particular findById and findByName, which allow looking up the enum value based on index or string name.

I am trying to create a function that would utilize the type-specific version of findById and findByName, but is generic to accept any of MyEnum1, MyEnum2, ... as the function parameter.

Note that a typical sealed trait + case class pattern to create a sum type out of the different enums would not help here, because I am talking about dispatching different static methods based on a type parameter, and there is never any actual value parameter involved at all.

For example, suppose that MyEnum1 encodes male/female gender. So that MyEnum1.findById(0) return MyEnum1.Female which has type MyEnum1. And say MyEnum2 encodes eye color, so that MyEnum2.findById(0) returns MyEnum2.Green which has type MyEnum2.

I am given a Map where the key is the type and the value is the index to look up, such as

val typeMap = Map(
  MyEnum1 -> 0,
  MyEnum2 -> 0
)

and I would like to generically do this:

for ( (elemType, idx) <- typeMap ) yield elemType.findById(v)
                                         |---------------|
                                          the goal is to
                                          avoid boilerplate
                                          of defining this
                                          with different
                                          pattern matching
                                          for every enum.

and get back some sequence type (can have element type Any) that looks like

MyEnum1.Female, MyEnum2.Green, ...

I've struggled with the sealed trait + case class boilerplate for a while and it does not seem to be conceptually the right way. No matter if I wrap values of MyEnum1 or MyEnum2 into case class value constructors like FromMyEnum1(e: MyEnum1) and try to define implicits for operating on that value, it doesn't help in my code example above when I want to do elemType.findById(...), because the compiler still says that type Any (what it resolves for the key type in my Map), has no method findById.

I'd strongly prefer to not wrap the types themselves in a case class pattern to serve as the keys, but I could do that -- except I cannot see how it's possible to treat the type itself as a first class value in a case class constructor, something naively like

case class FromMyEnum1(e: MyEnum1.getClass) extends EnumTrait

(so that the Map keys could have type EnumTrait and presumably there could be some implicit that matched each case class constructor to the correct implementation of findById or findByName).

Any help to understand how Scala enables using types themselves as the values inside of case class value constructors would be appreciated!

ely
  • 74,674
  • 34
  • 147
  • 228
  • "Any help to understand how Scala enables using types themselves as the values inside of case class value constructors would be appreciated!" – Unfortunately, this is fundamentally impossible in Scala (and in fact almost all statically-typed programming languages with the exception of a very small number of highly obscure research languages). In Scala, like most statically-typed programming languages, the universes of *types* and *values* are strictly separated. – Jörg W Mittag Aug 31 '18 at 15:15
  • You can easily see that in Scala, where you can have both a type and a value with the same name (e.g. `Seq`) without running into any conflict. – Jörg W Mittag Aug 31 '18 at 15:15
  • @JörgWMittag I think your comment is incorrect. For example, in Haskell, because you can make existing types into instances of typeclasses by providing solely their implementation, you can easily dispatch on types without adding boilerplate value constructors to wrap everything. Haskell and also Idris provide full dependent typing capabilities too. Haskell, at least, is quite mainstream, and these types of things are routinely used for non-research purposes. – ely Aug 31 '18 at 15:49
  • I'm not saying this to argue in favor of Haskell (I happen to like Scala a lot). Only that Scala's capacity for extending existing types with new dispatching behavior requires a degree of boilerplate that is not feasible with huge sets of types that are handed to you from elsewhere (like in my current case, working with a big bucket of autogenerated types from a legacy system, where the type generation procedure cannot be changed, but the types it creates have to all be identically extended later on). Matching on the structural type solves it in this case. – ely Aug 31 '18 at 15:51

2 Answers2

1

There are some fundamental misconceptions in your question.

Firstly, there are no "static methods" in Scala, all methods are attached to an instance of a class. If you want a method that is the same for every instance of a class you add a method to the companion object for that class and call it on that object.

Secondly, you can't call a method on a type, you can only call a method on an instance of a type. So you can't call findById on one of your MyEnum types, you can only call it on an instance of one of those types.

Thirdly, you can't return a type from a method, you can only return an instance of a type.


It is difficult to tell exactly what you are trying to achieve, but I suspect that MyEnum1, MyEnum2 should be objects, not classes. These inherit from the common interface you have defined (findById, findByName). Then you can create a Map from an instance of the common type to an index to be used in the findById call.


Sample code:

trait MyEnum {
  def findById(id: Int): Any
  def findByName(name: String): Any
}

object MyEnum1 extends MyEnum {
  trait Gender
  object Male extends Gender
  object Female extends Gender

  def findById(id: Int): Gender = Male
  def findByName(name: String): Gender = Female
}

object MyEnum2 extends MyEnum {
  trait Colour
  object Red extends Colour
  object Green extends Colour
  object Blue extends Colour

  def findById(id: Int): Colour = Red
  def findByName(name: String): Colour = Blue
}

val typeMap = Map(
  MyEnum1 -> 0,
  MyEnum2 -> 0,
)


for ((elemType, idx) <- typeMap ) yield elemType.findById(idx)

If you can't provide a common parent trait, use a structural type:

object MyEnum1 {
  trait Gender
  object Male extends Gender
  object Female extends Gender

  def findById(id: Int): Gender = Male
  def findByName(name: String): Gender = Female
}

object MyEnum2 {
  trait Colour
  object Red extends Colour
  object Green extends Colour
  object Blue extends Colour

  def findById(id: Int): Colour = Red
  def findByName(name: String): Colour = Blue
}

type MyEnum = {
  def findById(id: Int): Any
  def findByName(name: String): Any
}

val typeMap = Map[MyEnum, Int](
  MyEnum1 -> 0,
  MyEnum2 -> 0,
)

for ((elemType, idx) <- typeMap) yield elemType.findById(idx)
Tim
  • 26,753
  • 2
  • 16
  • 29
  • The enum classes are autogenerated (there are thousands of them, this cannot change, and can't autogenerate new ones), so it's not possible to make the trait you describe and have the enums extend it. When I say static method, I mean from the companion object. You most definitely can call `MyEnum1.findById` in my case, with no need for an *instance* of `MyEnum1`. Point 3 is technically right, but consider `def fn(): Any = {Double}`, then `fn()` returns `object scala.Double`. This means passing around the type itself has pragmatic meaning. It's too parochial to say you "can't return the type." – ely Aug 31 '18 at 14:41
  • Perhaps you should have said what you meant and used terms like like "type" and "class" more accurately? It would have saved some effort for those of us trying to help you – Tim Aug 31 '18 at 14:54
  • 1
    I disagree. I spent a lot of effort to write the question in a way that would make it obvious what I was seeking and in a way that described the constraints (for example, not being able to extend a new trait because that is implied by the classes being autogenerated). I feel like your first three points are uncharitable and unreasonably pedantic. Nonetheless, your edited answer about structural types is very helpful. – ely Aug 31 '18 at 15:06
0

If class has an actual instance (singleton objects count) you can use structural types:

type Enum[A] = {
  def findById(id: Int): E
  def findByName(name: String): E
  def values(): Array[E]
}

trait SomeEnum
object SomeEnum {
  case object Value1 extends SomeEnum
  case object Value2 extends SomeEnum

  def findById(id: Int): SomeEnum = ???
  def findByName(name: String): SomeEnum = ???
  def values(): Array[SomeEnum] = ???
}

trait SomeEnum2
object SomeEnum2 {
  case object Value1 extends SomeEnum2
  case object Value2 extends SomeEnum2

  def findById(id: Int): SomeEnum2 = ???
  def findByName(name: String): SomeEnum2 = ???
  def values(): Array[SomeEnum2] = ???
}

val x: Enum[SomeEnum] = SomeEnum
val y: Enum[SomeEnum2] = SomeEnum2

So if you work only with Scala, things are simple.

But Java classes does not have companion objects - you would end up with object mypackage.MyEnum is not a value. This would not work. You would have to use reflection for this, so you would have an issue with keeping API consistend for all cases.

However, what you could do would be something like this:

  1. define a common set of operations e.g.

    trait Enum[A] {
    
      def findById(id: Int): A = ???
      def findByName(name: String): A = ???
      def values(): Array[A] = ???
    }
    
  2. handle each case in separation:

    def buildJavaEnumInstance[E <: java.util.Enum: ClassTag]: Enum[E] = new Enum[E] {
      // use reflection here to implement methods
      // you dont
    }
    
    def buildCoproductEnum = // use shapeless or macros to get all known instances
    // https://stackoverflow.com/questions/12078366/can-i-get-a-compile-time-list-of-all-of-the-case-objects-which-derive-from-a-sea
    
    ...
    
  3. create a companion object and make these cases handled with implicits:

    object Enum {
    
      def apply[E](implicit e: Enum[E]): Enum[E] = e
      implicit def buildJavaEnumInstance[E <: java.util.Enum: ClassTag] = ???
      implicit def buildCoproductEnum = ???
      ...
    }
    
  4. Use Enum as a type class or something.

    def iNeedSomeEnumHere[E: Enum](param: String): E =
      Enum[E].findByName(param)
    

I agree though, that this would require a lot of upfront coding. Probably a good idea for a library, since I believe it is not only you who have this problem.

Mateusz Kubuszok
  • 24,995
  • 4
  • 42
  • 64