Strange curried case class constructor behaviour

I found something rather strange when working with curried case classes. I am not sure if the topic is right or not, but lets first see what the issue is.

If one writes a curried case class constructor, I would assume the second parameter list behaves like the first list, but it doesn’t. The second parameter list behaves like a regular class, where you have to declare accessibility.

Lets see the example:

object VarArgsTest extends App {
    import scala.reflect.{runtime => rt}
    import rt.{universe => ru}
    import ru._
    private implicit val rm: Mirror = rt.currentMirror

    case class ccCurriedVarArgs[A, B](a: A*)(b: B*)
    val ccVarArgsTest = ccCurriedVarArgs(1,2,3,4,5)("a", "b", "c", "d")

    val reflectedObj  = rm.reflect(ccVarArgsTest)
    //    println(ccVarArgsTest.b) <-- does not work, because b is not made a public member unlike in example 2
    println(reflectedObj.symbol.typeSignature)

    case class ccCurriedVarArgs2[A, B](a: A*)(val b: B*)
    val ccVarArgsTest2 = ccCurriedVarArgs2(1,2,3,4,5)("a", "b", "c", "d")

    val reflectedObj2  = rm.reflect(ccVarArgsTest2)
    println(ccVarArgsTest2.b)
    println(reflectedObj2.symbol.typeSignature)
}

Prints:

[A, B]AnyRef
        with Product
        with Serializable {
  val a: A*
  private[this] val a: A*
  private[this] val b: B*
  def <init>(a: A*)(b: B*): app.annotation.VarArgsTest.ccCurriedVarArgs[A,B]
  override def productPrefix: String
  def productArity: Int
  def productElement(x$1: Int): Any
  override def productIterator: Iterator[Any]
  def canEqual(x$1: Any): Boolean
  override def productElementName(x$1: Int): String
  override def hashCode(): Int
  override def toString(): String
  override def equals(x$1: Any): Boolean
}
ArraySeq(a, b, c, d)
[A, B]AnyRef
        with Product
        with Serializable {
  val a: A*
  private[this] val a: A*
  val b: B*
  private[this] val b: B*
  def <init>(a: A*)(b: B*): app.annotation.VarArgsTest.ccCurriedVarArgs2[A,B]
  override def productPrefix: String
  def productArity: Int
  def productElement(x$1: Int): Any
  override def productIterator: Iterator[Any]
  def canEqual(x$1: Any): Boolean
  override def productElementName(x$1: Int): String
  override def hashCode(): Int
  override def toString(): String
  override def equals(x$1: Any): Boolean
}

Is that the intended behaviour? It doesn’t feel like that to me.

All the functionality of case classes only apply to the first constructor arguments group. AFAIK, that is the intended behaviour.

Usually one should not need the second parameter group, at least not for data. It may be used for implicits or dependent types, which then they not being made public and not used for equality and friends is not a bug, but a feature.

1 Like

That does indeed make a lot of sense. Just wondered about this, due to some work I am doing to a related topic and I wasn’t able to access the fields from outside. I did some digging with reflections and found this. On the first look it looked like something unintended, but when you look at it with equality in mind, the behaviour seems sensible.

https://scala-lang.org/files/archive/spec/2.13/05-classes-and-objects.html#case-classes

The formal parameters in the first parameter section are called elements and are treated specially.

5 Likes