object NoPDT {
class Graph {
def useNode(node: Graph.Node) = ???
}
object Graph {
class Node(val graph: Graph)
}
// access graph given a node
def useNode(node: Graph.Node) = node.graph.useNode(node)
}
Attempts to use path-dependent types:
object PDT {
class Graph {
class Node {
def graph = Graph.this
}
def useNode(node: Node) = ???
}
// Attempt 1: does not work
def useNode(node: Graph#Node) = node.graph.useNode(node)
// Attempt 2: works but needs cast
def useNode(node: Graph#Node) = node.graph.useNode(node.asInstanceOf)
// Attempt 3: kind of works but needs extra parameter, see below
def useNode[G <: Graph](graph: G, node: graph.Node) = graph.useNode(node)
val graph = Graph()
val node = graph.Node()
// works but defies the point of accessing outer class from inner
useNode(graph, node)
// does not work
useNode(node.graph, node)
}
Based on these experiments, my conclusion is that there is no way to access outer class without a cast. Is that correct, or am I missing something?
The point of path dependant types is that there is a new Node type for each and every instance of Graph
Thus, it is impossible to talk about the Node type without a Graph value.
Thus, may I ask, what were you trying to solve here?
This is where the compiler starts losing track of what belongs to what.
For path-dependent types itâs very important that the path be stable. So, for example, you canât use a var in a path-dependent type because the compiler isnât confident when the var might change. Most of path-dependency is built around vals and the equivalent (like function arguments).
So you need some type annotations in order to help it understand that, yes, this specific instance is the one you mean. Unfortunately, in my hands, it doesnât seem to treat the Graphthis type as a stable value even though it obviously has to beâyou canât switch your own this instance out from under yourself!
Itâs possible to work around this by introducing a (seemingly superfluous) val self: this.type = this in the Graph that stabilizes the paths, even though it is just the graphâs this and is typed as such. And then the inner class needs a method that witnesses that it is in fact an inner class its own outer class. Thereâs a compilable example here: Scastie - An interactive playground for Scala.
You probably want to change the names to something more meaningful.
Anyway, yes, the problem of âwhy canât I explain to the compiler that if I have an inner class that I also have a stable instance of its outer classâ is something Iâve hit also.
I am not trying to talk about the Node type without a Graph value. I am trying to retrieve a Graph value given a Node value, without the compiler losing track that the Node is from the same Graph. See the NoPDT example - I am trying to achieve the same behavior with the added safety of PDT.
Oh nice, so this makes my attempt #3 work using useNode(node.graph, node). Thank you!
But yeah this still does not provide a way to go from Graph#Node to corresponding outer Graph, so it does not eliminate the need for redundant second parameter.
So, AFAIK, Graph#Node should not even exist anymore. I am a bit lost on the details, because I do know the syntax was re-added for cases where it is safe, but I never understood when it is safe. This is what I meant before when I said: " Thus, it is impossible to talk about the Node type without a Graph value". As far as my limited understanding of all this goes, this doesnât make sense.
I havenât been able to find a way to say that the Node.Graph type should be a GenGraph whose Node type is the same type of the GenNode.
Mainly because there is no way in Scala to refer to the type of the class of this; only to this.type
defs with singleton types are not stable (vals are):
class Graph {
class Node {
val graph = Graph.this // val (without singleton type)
}
def useNode(node: Node) = ???
}
def useNode(node: Graph#Node) = node.graph.useNode(node)
// [...] Required: node.graph.Node
class Graph {
class Node {
def graph: Graph.this.type = Graph.this // def with singleton type
}
def useNode(node: Node) = ???
}
def useNode(node: Graph#Node) = node.graph.useNode(node)
// [...] Required: [unknown type].Node
node is not seen as a node.graph.Node, even when the paths are stable:
class Graph {
class Node {
val graph = Graph.this
}
}
def test(node: Graph#Node) = node: node.graph.Node
// Found: (node : Playground.Graph#Node)
// Required: node.graph.Node
I believe 2. is the crux of the issue, and is not related to which paths are stable
(Since the compiler is fine with node.graph.Node)
Youâll note however that this is a weird thing for the compiler to think about, since node should conform to type node.something which seems very circular
All proposed solutions in one way or another avoid this circularity, node.thisNode most straightforwardly
Both 1. and 2. seem to straddle the bug-feature boundary: they are surprising, but probably implied by the spec, but fixing them would not break anything (?)