Extending method of inner classes

scala 3.2.2
Consider the following code:

class Outer:
  final class Inner:
    def foo : Boolean = false

  extension (inner:Inner) 
    def bar : Boolean = false

def test(v:Outer#Inner) =
	v.foo  // compiles 
	v.bar  // fails

Nothing unexpected from a language implementation perspective: v:Outer#Inner is not a Outer.this.Inner, so extension method bar(inner:Outer.this.Inner) can not be applied.

From programmer’s perspective though, it is quite unexpected to see v.bar fail when v.foo compiles. This is the nominal usage of an extension methods, except the argument is an inner class value.

Of course, it is possible to let the extension operate on Outer#Inner, but then the native access to Outer.this is lost, which is unfortunate, since it was the reason to use an inner class in the first place.

The best I got as a workaround is:

object Outer:
  extension (inner:Outer#Inner) 
    def bar : Boolean = inner.deflect(_.bar)		

trait Outer:
  final class Inner:
    def deflect[B]( m: (outer:Outer.this.type) => (outer.Inner) => B) : B =
      m(Outer.this)(this)
    def foo : Boolean =  false

  def bar(inner:Inner) : Boolean =
    // Outer.this is available
    false

def test(v:Outer#Inner) =
	v.foo 
	v.bar 

The workaround is really poor:

  • limited (passing type parameters creates another level of complication)
  • anti-pattern
  • deflect method cryptic and dirty.
  • the natural extension is split between the actual extension (In Outer companion, on Outer#Inner), and the implementation (In Outer class, on Inner)

The power of the pattern appears when implementing subclasses of Outer.

trait Left extends Outer:
  extension(inner:Inner)
    def leftOnlyMethod = ...

trait Right extends Outer:
  extension(inner:Inner)
    def rightOnlyMethod = ...

leftOnlyMethod (resp. rightOnlyMethod) looks defined on Left#Inner (resp. Right#Inner), while none exists on Outer#Inner.

1 Like

I think the usual pattern here would be to define an explicit way to access the Outer instance:

class Outer:
  class Inner:
    def outer: Outer = Outer.this

Then one can call outer on any instance of Outer#Inner.

2 Likes

But then:

object Outer:
  extension (inner:Outer#Inner) 
    def bar : Boolean =
      inner.outer.bar(inner) // fails: Found: Outer#Inner , Expected: ?1.Inner

Downcast is possible, but ugly.

      val outer = inner.outer
      outer.bar(inner.asInstanceOf[outer.Inner])

The deflect method above is meant to do the same, without casting.

Yes, but that would be bad, no? A cast is needed since one does not know statically that an Outer#Inner is an outer.Inner. So if the compiler would let you write deflect without a cast, that smells like a type hole.

I did not feel like a smuggler while writing the deflect method :slight_smile:
deflect is on Inner, which knows itself to be an Outer.this.Inner.
It then forwards the constraint through its dependent function type parameter m: (outer:Outer.this.type) => (outer.Inner) => B.

My bad. I misunderstood. Yes, deflect looks sound, but it is cryptic.

Ah, I missed that bar took Inner as an argument. Since extension methods desugar into regular methods, I don’t think there’s much we can do here.

The best alternative I can come up with is to give up on on inner classes and use parameterized classes instead:

trait Outer:
  def bar(inner: Inner[Outer]): Boolean = false

class Inner[+T <: Outer](val outer: T)

extension (inner: Inner[Outer]) 
  def bar: Boolean = inner.outer.bar(inner)

trait Left extends Outer
extension (inner: Inner[Left]) 
  def leftOnlyMethod = ???

Indeed!
I initiated the post because of the contrast between the apparently natural intention and the head scratching session to make it fly.

I have been twisting my code like a glove between Inner[+O<:Outer] and Outer#Inner for some time now…

In general, the drawback of Inner[+O<:Outer] is that a priori O is typed-erased. Type checks on Inner[O] must be redirected to val outer:O, … with some casts again ! ( TypeTest[-U,+T] only operates on abstract types, thus not on trait Inner[+O<:Outer]. Please see there and tell me if I should open another thread on scala contributors. )

But here

The proposal is treacherous, because inner is a Inner[Left] not a Inner[Left.this.type].
There is a significant risk to use Left.this (or any other method on Left), on some left which is not inner.outer !

One should write:

trait Left extends Outer:
  def leftOnlyMethod(inner:Inner[Left.this.type]) =
    // Left.this or other Left methods may be used here.

object Left:
  extension(inner:Left[Inner])
    def leftMethodOnly =
      // something that will look very much like a deflection

Switching from Outer#Inner to Inner[+O<:Outer] does not fundamentally change the nature of the issue.

One way to avoid having to worry about type erasure is to define a GADT, but that’s only applicable if you can define in advance all the classes that extend Outer:

trait Left extends Outer
trait Right extends Outer

enum Inner[+T <: Outer](val outer: T):
  case L(o: Left) extends Inner(o)
  case R(o: Right) extends Inner(o)

def test[T <: Outer](x: Inner[T]) = x match
  case x: Inner.L =>
    // In this scope, the compiler knows that T <: Left.
  case _ =>

I’m not sure I understand what the risk is here, could you give a concrete example of code you’d want to disallow?

Duly noted, but the initial objective is to articulate a final class Inner with a open class Outer.
The problem is similar to the chisel’s cloneType. A central system is meant to create new instances of an open class. In my case the central system duplicates (final) Inner instances, while Outer is left open.

Treacherous case:

class Inner[+O<:Outer](val outer:O):

class Outer

case class Left(name:String) extends Outer:
	extension(inner:Inner[Left])
		def foo : Unit =
			println(s"I am operating on ${inner.outer} from within ${Left.this}")

  

val left1 = new Left("left1")
val left2 = new Left("left2")
val left1Inner = new Inner(left1)

// left1Inner.foo // fails 
import left2.*
left1Inner.foo

Results In: I am operating on Left(left1) from within Left(left2)

1 Like