Just like pattern matching, where @ allows to name part of a matched expression, I propose to use @ for naming part of an expression instead of having to write a separate line for it.
Example:
trait Connectable[T] {
def apply(t : T) : Graph
}
implicit val graphConnectable : Connectable[Graph] = g => g
implicit val nodeConnectable : Connectable[Node] = n => Graph(n)
final case class Node(value : Int) {
def ==> [T](t : T)(implicit c : Connectable[T]) : Graph = Graph(this) ==> c(t)
}
final case class Graph(edges : Map[Node, Node], firstNode : Option[Node], lastNode : Option[Node]) {
def ==> [T](t : T)(implicit c : Connectable[T]) : Graph = {
val g = c(t)
val connectingEdge = (lastNode, g.firstNode) match {
case (Some(l), Some(r)) => Some(l -> r)
case _ => None
}
Graph(edges ++ g.edges ++ connectingEdge, firstNode, g.lastNode)
}
def WITH (g : Graph) : Graph = Graph(edges ++ g.edges, firstNode, g.lastNode)
}
object Graph {
def apply(node : Node) : Graph = Graph(Map(), Some(node), Some(node))
}
val myGraph =
g1To3 @ (n1 @ Node(1) ==> n2 @ Node(2) ==> n3 @ Node(3)) WITH
n2 ==> n1 WITH
g4To6 @ (n4 @ Node(4) ==> n5 @ Node(5) ==> n6 @ Node(6)) WITH
n6 ==> n4 WITH
n5 ==> n1
val graph1To6 = g1To3 ==> g4To6
Another example can be seen in the def ==> where could have written:
def ==> [T](t : T)(implicit c : Connectable[T]) : Graph = {
val connectingEdge = (lastNode, (g @ c(t)).firstNode) match {
case (Some(l), Some(r)) => Some(l -> r)
case _ => None
}
Graph(edges ++ g.edges ++ connectingEdge, firstNode, g.lastNode)
}
The idea’s intriguing, and I like the idea of taking this odd little edge case in the language and applying it more generally. I suspect there are plenty of places where I would use it.
There are a lot of details to iron out, though, especially regarding the scope of the name you’re introducing here…
val myGraph = {
g1To3 @ (n1 @ Node(1) ==> n2 @ Node(2) ==> n3 @ Node(3)) WITH
n2 ==> n1 WITH
g4To6 @ (n4 @ Node(4) ==> n5 @ Node(5) ==> n6 @ Node(6)) WITH
n6 ==> n4 WITH
n5 ==> n1
}
vs
val myGraph =
g1To3 @ (n1 @ Node(1) ==> n2 @ Node(2) ==> n3 @ Node(3)) WITH
n2 ==> n1 WITH
g4To6 @ (n4 @ Node(4) ==> n5 @ Node(5) ==> n6 @ Node(6)) WITH
n6 ==> n4 WITH
n5 ==> n1
Should anything change? I believe so. I think that the scope is determined by the boundary block of {} or within the bounds after => of a case in pattern matching (as it is today).
So the examples above will compile to the following, respectively:
val myGraph = {
val n1 = Node(1)
val n2 = Node(2)
...
...
...
val g1To3 = n1 ==> n2 ==> n3
val g4To6 = n4 ==> n5 ==> n6
g1To3 WITH n2 ==> n1 WITH g4To6 WITH n6 ==> n4 WITH n5 ==> n1
}
vs.
val n1 = Node(1)
val n2 = Node(2)
...
...
...
val g1To3 = n1 ==> n2 ==> n3
val g4To6 = n4 ==> n5 ==> n6
val myGraph = g1To3 WITH n2 ==> n1 WITH g4To6 WITH n6 ==> n4 WITH n5 ==> n1
I am not keen on this. I believe good style is to break code into lots of small single-line definitions, each with a well-chosen name. This seems to encourage the opposite,
At a glance, it looks cool but I’m not sure it pays the complexity cost v.s. writing out vals? e.g. the myGraph example translated to vals is certainly more verbose, but subjectively looks ok to me:
val n1 = Node(1)
val n2 = Node(2)
val n3 = Node(3)
val n4 = Node(4)
val n5 = Node(5)
val n6 = Node(6)
val g1To3 = n1 ==> n2 ==> n3
val g4To6 = n4 ==> n5 ==> n6
val myGraph =
g1To3 WITH
n2 ==> n1 WITH
g4To6 WITH
n6 ==> n4 WITH
n5 ==> n1
val graph1To6 = g1To3 ==> g4To6
Depending on what you’re using the 1234 literals for, you might be able to use sourcecode.Name to elide them:
val n1 = Node()
val n2 = Node()
val n3 = Node()
val n4 = Node()
val n5 = Node()
val n6 = Node()
val g1To3 = n1 ==> n2 ==> n3
val g4To6 = n4 ==> n5 ==> n6
val myGraph =
g1To3 WITH
n2 ==> n1 WITH
g4To6 WITH
n6 ==> n4 WITH
n5 ==> n1
val graph1To6 = g1To3 ==> g4To6
Or even
val n1, n2, n3, n4, n5, n6 = Node()
val g1To3 = n1 ==> n2 ==> n3
val g4To6 = n4 ==> n5 ==> n6
val myGraph =
g1To3 WITH
n2 ==> n1 WITH
g4To6 WITH
n6 ==> n4 WITH
n5 ==> n1
val graph1To6 = g1To3 ==> g4To6
On a side note, I’ve been writing a lot of GraphViz DOT code recently for my book. Although it’s not quite Scala, it definitely has a similar style to what you’re proposing. I’ve tended to gravitate to an “explicitly list out nodes up top” style even though it’s optional in GraphViz, because I find it makes things clearer. YMMV.
The question of scoping is definitely a bit unclear to me. Pattern matching doesn’t really have the scoping ambiguity. While technically possible to re-use a pattern-bound variable elsewhere in the pattern, the number of times I’ve seen people take advantage of this I can count on one hand, whereas for this proposal using the name in the same expression is the whole point. For example, what if parts of the expression do not evaluate, e.g. due to short-circuiting in ||, or being in an if-else, or inside some by-name param: do those names still evaluate?
As for the node example, it was to clarify the point (including the names and values of nodes matching). I’m not necessarily aiming for the graph example, but for a general case where I wish to reuse an intermediate value without wanting to go ahead and give it a separate declaration.
Inside an if-else, the variables will be local to that block, so I don’t see the problem.
For shortcircuit || we can translate the RHS into a lazy val or generate an error.
Pattern matching already has similar problems with union matching, so we can generate errors in a similar way.
sealed trait Foo
case object Bar extends Foo
case object Baz extends Foo
case object Boo extends Foo
val foo : Foo = Bar
foo match {
case a @ Baz | b @ Bar => //which has value, a or b ?
case Boo =>
}
I think this looks very similar to the controversial walrus operator in Python, while the walrus operator tries to assign values to variables inside an expression, this proprosal tries to assign lambdas to function definitions inside an expression. So they may have similar pros and cons.
I feel @ in pattern matching is necessary becuase it helps get some intermediately matched result with no extra code to extract it again and maintains conciseness and readability, but this makes the code less readable and doesn’t make the code much simpler.