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 Outerclass, 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.
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 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.
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 prioriO 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 notinner.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)