Search code examples
scalapattern-matchingimplicitcase-class

Scala: how to use types as first-class values inside of case class constructors?


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!


Solution

  • 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)