The following code does not compile, although its intention is clear.
case class Vertex(neighbors: Set[Vertex])
def simpleCircularGraph(): Vertex = {
val a: Vertex = Vertex(Set(b))
val b: Vertex = Vertex(Set(a))
a
}
It can be translated into some code that does compile, at the cost of being less clear and more verbose:
class Vertex(neighborsArgument: Set[Vertex]) {
private[this] var neighborsValue = neighborsArgument
def neighbors: Set[Vertex] = neighborsArgument
def setNeighbors$(neighborsArgument: Set[Vertex]): Unit = {
neighborsValue = neighborsArgument
}
// <additional boilerplate to emulate a case class>
}
// <additional boilerplate to emulate a case class>
def simpleCircularGraph(): Vertex = {
val a: Vertex = Vertex(Set())
val b: Vertex = Vertex(Set(a))
a.setNeighbors$(Set(b))
a
}
Could support for such forward references somehow be added to Scala?
I would say that what you’re trying to achieve here requires something more than just forward references. You can already replace your val
s with lazy val
s to make this compile but calling simpleCircularGraph()
would then crash at runtime.
The problem with the workaround suggested by you is that if Vertex
exposes a public method mutating its state it basically looses the semantic properties of a case class
BTW I’ve just realized that in your particular use case you might not want Vertex
to be a case class. E.g. if two vertices in a graph have the same neighbors this doesn’t mean they’re the same vertex (but the synthetic implementation of equals
in case classes relies on the values of class parameters).
This works and doesn’t seem that bad syntactically
class Vertex(neighborsArgument: => Set[Vertex]):
lazy val neighbors = neighborsArgument
def simpleCircularGraph(): Vertex = {
lazy val a: Vertex = Vertex(Set(b))
lazy val b = Vertex(Set(a))
a
}
@main def run() =
val a = simpleCircularGraph()
assert(a.neighbors.head.neighbors.head == a)
@prolativ Thank you! Yes, your code looks better. It’s true that for a graph, we wouldn’t want case classes, so I didn’t pick my example so well (states might have been better). Can your code be modified to work with case classes as well?
In my “workaround”, setNeighbors$
was meant to be generated by the compiler, or something, and not accessible in the code.
I feel that, ideally, the code I wrote would translate into the allocations being done for both objects first, and then their initialization. But that’s probably not possible with the JVM.
Anyways, it’s probably not worth adding support for this, as normal classes (potentially with some boilerplate and performance overhead) and lazy vals can be used, as you showed; plus, it’s not a common problem.
It depends on what you want to achieve. Could you give some proper use case for that?
In general by-name parameters cannot be used as “field parameters” (not sure if this is the right term). So you can have
class Foo(i: => Int)
but not
class Foo(val i: => Int)
If Foo
was a case class val
would be added implicitly so
case class Foo(i: => Int)
won’t work either.
However all the magic of case classes works only for the first parameters list of a constructor so theoretically you can have e.g.
case class Foo(s: String)(i: => Int)
but then people might get confused that
Foo("a")(1) == Foo("a")(2)
returns true
because only the first parameter is taken into account when equals
method is synthesized
1 Like
Why not something with a simpler immutable non-lazy design which allows using the normal Scala case class infrastructure?
case class Vertex(tag: String)
case class Edge(vertexA: Vertex, vertexB: Vertex)
case class Graph(vertexes: Set[Vertex], edges: Set[Edge]) {
def neighbors(vertex: Vertex): Set[Vertex] =
edges.map(
edge =>
if ((edge.vertexA == vertex) && (edge.vertexB != vertex)) Some(edge.vertexB)
else
if ((edge.vertexA != vertex) && (edge.vertexB == vertex)) Some(edge.vertexA)
else None
).flatten
}
View and play within Scastie!
And this design is trivially enhanced to memoize the calls to the neighbors
method.
2 Likes