Escape to an Inner Object
An Inner Trait Puzzle
I blundered into this strange puzzle with visibility, inner traits, and inner objects while putting together code to plug Semirings into Dijkstra's algorithm. I'm not sure if the problem rates a bug report, or two, or a feature request, or is just something I don't fully understand. I'd like to hear some advice before reporting (or not reporting) it.
Find some code you can paste into the REPL in this gist to try different hacks.
Here's code that illustrates the dissonance fully:
// A top-level trait defines a def
trait TopTrait {
def topDef:String
}
// A trait that has an inner trait that extends TopTrait
trait BeyondTrait {
def innerThing:InnerTrait
trait InnerTrait extends TopTrait{
def innerDef = "innerDef"
def topDef = "topDef"
}
}
// A class that extends that more complex trait
class Beyond extends BeyondTrait {
// An inner class that extends the InnerTrait, and a def that implements the more complex trait's contract
override def innerThing = new InnerTrait{
def beyondDef = "innerThing from beyond"
}
}
// A class that uses the class that implements the complex trait
class OuterClass(beyond: Beyond){
object InnerObject {
// this won't compile
// "private value beyond escapes its defining scope as part of type OuterClass.this.beyond.InnerTrait"
// the compiler won't walk up the tree to find TopTrait
// and it won't decide that it has access to the private beyond member
val innerThing = beyond.innerThing
// and innerThing won't be defined for these two vals
val topDefResult = innerThing.topDef
val innerDefResult = innerThing.innerDef
val comboResult = s"$innerDefResult and $topDefResult"
}
}
It looked like it would work; even Intellij thought it would. However, the REPL tells me:
:20: error: private value beyond escapes its defining scope as part of type OuterClass.this.beyond.InnerTrait
val innerThing = beyond.innerThing
I'd blundered into this problem while refactoring some code to better fit the Law of Demeter and make it more readable. The original code just used the outer class' reference inside the inner object. It was cluttered, but it worked.
class OuterClass(beyond: Beyond){
...
object InnerObject {
...
//this works as expected
val innerDefResult = beyond.innerThing.innerDef
//as does this
val topDefResult = beyond.innerThing.topDef
...
I didn't see why this should work but my first example did not. One of the reasons I like Scala is that I get to learn something new most days, so I decided to invest an hour and learn about what is going on.
The error message says beyond is private. Making beyond a public member appeases the compiler, but doesn't provide much understanding:
class OuterClass(val beyond: Beyond){
...
Assigning a type of TopTrait to innerThing compiles past that, but the compiler can't find the inner trait's def. This seems correct to me and is the fix I settled for in the project's code. However, it doesn't explain what's going on.
val innerThing:TopTrait = beyond.innerThing
I think I've tilled up two problems. First, some things - types from the outer class' member variables - that should be visible inside the inner object are not. Second, the compiler is giving up inferring innerThing's type instead of either using the publicly visible TypeTrait or shrugging and inferring that the type is Any.
Or possibly the behavior is correct, my intuition is faulty, and there's more for me to learn from this code. Thoughts?
Comments from 2015
Preserved from the Disqus thread this post carried at the time.
First, some things - types from the outer class' member variables - that should be visible inside the inner object are not
The type _is_ visible inside the inner object. But because `InnerObject.innerThing` is itself public, its type has to be visible outside of `OuterClass` class as well, and it isn't because it contains a reference to a private variable.
The following line works: val innerThing: BeyondTrait#InnerTrait = beyond.innerThing
Thanks for the comment. I've spent a few days puzzling through it. I'm having trouble mapping the "it"s.
InnerObject.innerThing is public. InnerObject.innerThing's type has to be visible outside OuterClass (). That type is a Beyond's anonymous extension of InnerTrait. The anonymous extension is private inside Beyond, not public.
The compiler doesn't walk up the hierarchy from that anonymous extension to something that is public. Should it?
Thanks again,
David
Thanks! That's a good option. Any idea why the compiler doesn't walk up from the anonymous extension of InnerTrait?
> The anonymous extension is private inside Beyond, not public.
Not quite. The type of InnerObject.innerThing is beyond.InnerTrait. It's the `beyond` part which is private, just as the message says.
> The compiler doesn't walk up the hierarchy from that anonymous extension to something that is public. Should it?
I don't think it should.
1. The type may be private unintentionally (e.g. the programmer intended to make `beyond` public). Better to give the message and allow the programmer to choose what to do.
2. Do you have different "external" and "internal" types for `InnerObject.innerThing`? Or should making it private change the inferred type?
3. What if you have several public supertypes? E.g. what should the type of X.x be here:
object X {
trait Foo
trait Bar
private class FooBar extends Foo with Bar
val x = new FooBar
}
Thanks for staying with me on this. I've got the example shrunk to just
scala> object X {
| private class FooBar;
| val x = new FooBar;
| }
<console>:9: error: private class FooBar escapes its defining scope as part of type X.FooBar
val x = new FooBar;
The compiler would have to chose between making x of class Any or FooBar. Any is a bad choice. FooBar won't work because the FooBar class is private. And all the rest is just window dressing for the puzzle.
I think I've got it. Thanks again.
Comments