From izzeldeen03 at gmail.com Mon Apr 18 21:49:47 2022 From: izzeldeen03 at gmail.com (Izz Rainy) Date: Mon, 18 Apr 2022 22:49:47 +0100 Subject: Record pattern and side effects Message-ID: While it's pretty easy to say that record deconstruction should never have side effects (or generally be stateful beyond the record), would you also extend that to all custom patterns? It seems to me that stateful, effectful patterns could be useful if explicit enough. Given some IntelliJ-like AST system, we could have code like ``` AstElementReference elem = ...; switch(elem){ case DirectRef(var ast) -> ... case AstCache.cacheOf(var ast) -> ... case Stubs.stubOf(var ast) -> ... case FileIndex.refToUnparsed(var ast) -> ... default -> throw ... } ``` where accessing (or creating) an underlying AST element might be stateful, and where proper polymorphism may not be appropriate (e.g. I don't control these types, or what I'm doing with them is not meaningful for all subtypes, or...). An equivalent if/else chain might look like ``` if(elem instanceof DirectReference(var ast)){ ... }else if(elem.isCache()){ var ast = AstCache.get(elem.key()); ... }else if(elem.isStub()){ var ast = Stubs.createMirror(elem.key()); ... }else if(elem.isFileRef()){ var ast = FileIndex.parse(elem.key()); ... } ``` or something. An enum might be more appropriate for representing types, but that's irrelevant to what the patterns are doing; they're moving out the "obvious" step of data extraction into the conditional, making it clearer what the "actual" logic is, similar to type patterns but more domain specific. In this example, it's clear that side effects are only appropriate on a successful match. Stateful failures *may* be required if a stateful pattern is nested within another pattern, or guarded by a when clause, though; ``` switch(elem){ case Stubs.stubOf(AstClass.classAst(var clss)) -> ... case Stubs.stubOf(var ast) when ast.isPhysical() -> ... default -> throw new IllegalArgumentException(); } ``` Factoring out a common head would be the "correct"/more efficient behaviour in this case, but as pointed out already, it's not possible to do that for all duplicate occurrences of a pattern. The behaviour I would expect here is essentially "mimicking an equivalent if/else chain", ensuring that I can always refactor between a switch and ifs without new behaviour, always evaluating top-to-bottom left-to-right. But that's also bad for the majority of patterns that are expected to be pure. I'd suggest providing an annotation for impure patterns, then, which prevents the compiler from optimizing the switch in "unexpected" ways, and allows warning when an impure pattern is repeated (in the compiler or by an IDE), alongside making it clearly documented and explicit. If the annotation is not present in a switch, the compiler gets to reorder and factor out any part it wants. For the purposes of JDK 19? record patterns, where the dtor cannot be explicitly written out, the annotation would have to be applied to the whole type, or a particular accessor. From dcrystalmails at gmail.com Wed Apr 20 20:23:26 2022 From: dcrystalmails at gmail.com (Dimitris Paltatzidis) Date: Wed, 20 Apr 2022 23:23:26 +0300 Subject: Extending when clauses beyond pattern matching for switch Message-ID: Pattern matching is asking questions about what something is. "when" clauses complement it, so we can fine tune: Collection c = ... String r = switch (c) { case List l when l.isEmpty() -> "empty list"; case List l when l.size() > 10 -> "big list"; case List l when l.contains("test") -> "it does"; default -> "none"; }; But, what if we already know the answer to our question? If case's' are asking "what is it?" then when's' are asking "how it behaves?" List l = ... String r = switch (l) { when l.isEmpty() -> "empty list"; when l.size() > 10 -> "big list"; when l.contains("test") -> "it does"; default -> "none"; }; That is, making a distinction of when switching over a type against when switching over a value. Effectively, we already have behavioral switches: int a = ... switch (a) { case 1 -> ... case 2 -> ... default -> ... } Even though somewhat ambiguous, we can say that the above switch is not asking what a is (here "is" refers to the type, not the value), but how it behaves. Patterns are type-based, while behaviours are value-based. Of course, we are all aware of the range checking situation in a switch, that currently no construct supports it. This is not about solely solving it, but a behavioral generalization to all types. A few questions are raised: - Totality: In a behavioral switch, it's nearly impossible if not to reason about it. This effectively pushes users to a permanent boilerplate default clause. - Dominance: More or less this is already solved with the current guards. - Purity: Can we have a mixed pattern and behavioral switch at the same time? Is it even meaningful as a general construct? // purity example - does it even make sense? Collection c = ... String r = switch (c) { when c.isEmpty() -> "empty collection"; //switching over value case Set s when s.size() > 10 -> "big set" //first over type then over value case Set s -> "a set" //switching over type default -> "none"; }; The above raises the science fiction question: // Looks mostly as a no-no, but in the context of this behavioural generalization, could it stand? // We filter first on the behaviour and after that on the type when c.isEmpty() case Set s -> "empty set"; Yes, this could be an attack on the ill-used long-ish if-else-ifs. Nevertheless, the gravity is towards a philosophical take on what patterns, guards and switches are, could be and what are the problems they tackle. Personally, I don't know how I feel about them (these non-existent solely "when" switches). They certainly could add a lot of complexity into the language that is probably unjustifiable.