Way to create a class that extends nested class

Hello,

I have a class hierarchy in a package outside of my control that looks like this:

class Parent:
  class Nested(a: Int):
    ???

I want some way to create a convenience class that wraps Nested, but so far, due to the dependent nature of Nested on its parent, I can only do it inside the body of the parent’s subclass like this:

new Parent:
  case class MyNested(a: Int) extends Nested(a):
    ???

  (new MyNested(1)).copy() // example use

The problem is that, if I want to extend Parent multiple times, I have to duplicate the convenience case class in the body of every subclass I create. In order to prevent having to copy this class over and over, I would like to define a class that works with any Parent#Nested Here’s my attempt at a solution:

case class MyNested(a: Int)(p: Parent) extends p.Nested(a):
  ???

new Parent:
  self =>
  MyNested(1)(self).copy()(self)

new Parent:
  self =>
  val m = MyNested(2)(self).copy()(self)
  val n: Nested = m // problematic line

The problem is when I go to use a MyNested polymorphically where a Nested is expected, I get the error:

Found: MyNested
Required: Nested

I’m a Scala novice, so I’m sure there’s a better way to do what I’m asking. There is probably a theoretic reason that what I’m trying to do directly breaks the type system. In any case, I would greatly appreciate any help in the direction towards a “right” way to extend a nested class.

That does not even compile with a recent Scala 3. The reason is that it is unsound to refer to a constructor parameter in the parent type. See Unsoundness: constructor params must not be in scope for the parent types · Issue #16270 · scala/scala3 · GitHub for context. Downstream issues in earlier versions of the compiler (like the one you are experiencing) are often consequences of that unsoundness.

You can structure your solution this way:

class Parent:
  class Nested(a: Int):
    ???

final class Container[P <: Parent](val p: P):
  case class MyNested(a: Int) extends p.Nested(a):
    ???
  
new Parent:
  self =>
  Container[this.type](this).MyNested(1).copy()

new Parent:
  self =>
  val c: Container[this.type] = Container(this)
  val m = c.MyNested(2).copy()
  val n: Nested = m // now this works
3 Likes

Thanks for the solution, this is exactly what I was looking for!

You’re right that the case class declaration as stated does not compile on 3.8.3 - it fails with non-private class MyNested refers to private value p, but if you change the declaration to private, it seems to compile fine.

I’ll note two things that were non-obvious to me about the solution, for my own sake and any others who might be interested:

  1. the constructor parameter p needs to be val in order to be a stable path identifier
  2. p must be upper-bounded by Parent and not a Parent itself - I’m guessing to allow for a singleton type to be used when creating the path-dependent type?

I haven’t looked deeply into the unsoundness bug you linked to - my takeaway is just that you can’t use a reference to a constructor parameter to specify the superclass of a class. I’m guessing the proposed workaround in this thread does not allow the same soundness bug, though I would need to think more about why it doesn’t.

Thanks,

Max