Search code examples
scalacase-class

Scala collection whose elements can construct sibling instances using named parameters and default values?


I want to have a collection of objects, each object a companion of a different class, which classes all share a common method defined in a superclass that can be invoked when looping through the collection with a foreach(). I want the constructors of these sibling-classes to have the same named parameters and default parameter values as each other. Finally, I want to minimize repeated code.

Thus far, I am trying to do this with case classes, since--if it worked--it would eliminate all the duplicated code of the companion-objects for each type. The problem is that if I put all these companion objects into a Set, when I take them out again I lose the default parameters and parameter names.

Here is some example code of what I am describing:

trait MyType {
  val param: String
  def label = param   // all instances of all subclasses have this method
}

case class caseOne(override val param: String = "default") extends MyType
case class caseTwo(override val param: String = "default") extends MyType

object Main extends App {
  // I can construct instances using the companion objects' `apply()` method:
  val works1 = caseOne(param = "I have been explicitly set").label
  // I can construct instances that have the default parameter value
  val works2 = caseOne().label

  // But what I want to do is something like this:
  val set = Set(caseOne, caseTwo)
  for {
    companion <- set
  } {
    val fail1 = companion()                      // Fails to compile--not enough arguments
    val fail2 = companion(param = "not default") // Fails also as param has lost its name
    val succeeds = companion("nameless param")   // this works but not what I want
    println(fail1.label + fail2.label)           // this line is my goal
  }
}

Notably if the Set has only one element, then it compiles, suggesting the inferred type of the multi-element Set lacks the parameter name--even though they are the same--and the default values. Also suggesting that if I gave the Set the right type parameter this could work. But what would that type be? Not MyType since that is the type of the companion classes rather that the objects in the Set.

I could define the companion objects explicitly, but that is the repeated code I want to avoid.

How can I loop through my collection, constructing instances of MyType subclasses on each iteration, with constructors that have my desired parameter names and default values? All while minimizing repeated code?

Update: Originally the example code showed caseOne and caseTwo as having different default values for param. That was incorrect; they are now the same.


Solution

  • You're not going to be able to get exactly what you want since you don't really have much control over the auto-generated companion objects. In particular for this to work they would all need to extend a common trait. This is why it fails to compile when the set has more than one companion object; even though they all have a method with the same signature, they don't extend a common trait for the compiler to utilize.

    You can use a nested case class and get something very similar though:

    trait MyType {
      val param: String
      def label = param   // all instances of all subclasses have this method
    }
    
    abstract class MyTypeHelper(default: String) {
    
      case class Case(param: String) extends MyType
    
      def apply(param: String) : Case = Case(param)
      def apply(): Case = apply(default)
    }
    
    object One extends MyTypeHelper("default one")
    object Two extends MyTypeHelper("default two")
    
    object Example {
    
      val works1 = One(param = "I have been explicitly set").label
      val works2 = One().label
    
      val set = Set(One, Two)
      for {
        companion <- set
      } {
        val a = companion()                      
        val b = companion(param = "not default") 
        val c = companion("nameless param")   
        println(a.label + b.label)           
      }
    }
    

    Instead of having a caseOne type, you have One.Case, but it still implements MyType so you shouldn't have any issue anywhere else in the code that uses that trait.