On Oct 20, 2018, at 8:04 PM, Guy Steele <guy.ste...@oracle.com> wrote: > >> On Oct 20, 2018, at 12:42 PM, Brian Goetz <brian.go...@oracle.com> wrote: >> >>>> case Point(var x, var y) p: ... >>>> >>> >>> Is there a situation where you could have reached that case without >>> having a reference to p? If I've understood so far: >>> >>> SomeSuperTypeOfPoint q; >>> >>> switch (q) { >>> case Point(var x, var y) p: ... // p == q and p : Point >>> } >> >> Yes, you could have gotten there via >> >> switch (getAnObjectForMe()) { >> case Point(var x, var y) p: >> } >> >> But, one could always refactor to >> >> Object o = getAnObjectForMe(); >> switch (o) { >> case Point(var x, var y) p: >> } >> >> and we’re back to the previous case. > > I believe that @-patterns (I’ll call them that for now) are especially useful > in nested situations, where it is not convenient to do that kind of > refactoring: > > Object o = getAnObjectForMe(); > switch (o) { > case Line(Point(var x1, var y1) p1, Point(var x2, var y2) p2): > // Now you have your hands on the two points as well as their x > and y coordinates > } > > So the question is how much that comes up in practice. > > But even without nesting, the original example has the benefit of having > verified the type of o and made it available in p with the matched type Point. > > This is a very convenient idiom if you want to test x and y in order to > decide which method of p to call, for example.
When working with ASTs (or sea-of-nodes neighborhoods) in a compiler, you often want to match something and then do some extra ad hoc logic to decide what to do with the matched parts. But in real world cases there's often some condition where you can't complete the intended transform, due to some corner-case constraint failure (e.g., div-by-zero, non-loaded class) and you need to return the original AST unchanged. If I don't have @-patterns I need to refactor my switch to capture the original node in a temp above the switch, so that's not a direct use case for @-patterns. But the patterns can get complex, with multiple nesting levels; this happens routinely in compilers. Then, I might want to return a result composed from an interior node of the pattern, wired together with some extra stuff (unrelated leaf nodes, for example). If I want to use the workaround of a temp above the switch, I must first refactor the single switch over a nested pattern into a nested switch over simpler patterns. That feels like falling off a cliff. Here's a second, more general observation about @-patterns. (Can you tell that I like them?) If all deconstruction patterns are always only for record types, and record types are defined as being *solely* *completely* determined from their deconstruction parameters (their "state vector"), then the only argument for @-patterns is a weak one: You can always recover an arbitrary interior node of a pattern-matched object by rebuilding from the leaves. The downsides to this are acceptable in many cases: Extra GC work, and extra verbosity (key objection: it's error-prone). But that argument runs out of steam when you go beyond record deconstruction. If you fully combine object-oriented APIs with patterned deconstructors, you must allow that an object "p" may match a pattern "foo(var x, var y)" without any contract that "p" can be fully reconstructed from "x" and "y". After all, "p" is an object with potential private variables and special behaviors, even if it *also* reports a match for "foo(x,y)". So @-patterns become more useful when patterns are generalized to true pattern queries over encapsulated objects, rather than mere record deconstructors. When those things nest, you need to hold onto the interior nodes of the pattern via @-patterns. Or else, as noted above, the workaround is to unwind the nested pattern into a nested switch of simpler patterns with associated var declarations for the desired interior nodes. But, again, that's a sharp edge for users who need complex nested patterns *and* ad hoc logic to visit interior nodes. — John