Use `@` to name an intermediate anonymous expression

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…

Yes, indeed. For example, if I write:

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,

5 Likes

reminds me of the extract variable refactoring https://refactoring.com/catalog/extractVariable.html

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 1 2 3 4 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?

4 Likes

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.