Yes, this is something we have to get “on the record”.  

Record patterns are a special case of deconstruction patterns; in general, we 
will invoke the deconstructor (which is some sort of imperative code) as part 
of the match, which may have side-effects or throw exceptions.  With records, 
we go right to the accessors, but its the same game, so I’ll just say “invoke 
the deconstructor” to describe both.  

While we can do what we can to discourage side-effects in deconstructors, they 
will happen.  This raises all sorts of questions about what flexibility the 
compiler has.  

Q: if we have 

    case Foo(Bar(String s)):
    case Foo(Bar(Integer i)):

must we call the Foo and Bar deconstructors once, twice, or “dealer’s choice”?  
(I know you like the trick of factoring a common head, and this is a good 
trick, but it doesn’t answer the general question.)  

Q: To illustrate the limitations of the “common head” trick, if we have 

    case Foo(P, Bar(String s)):
    case Foo(Q, Bar(String s)):

can we factor a common “tail”, where we invoke Foo and Bar just once, and then 
use P and Q against the first binding?  

Q: What about reordering?  If we have disjoint patterns, can we reorder:

    case Foo(Bar x): 
    case TypeDisjointWithFoo t: 
    case Foo(Baz x): 

into 

    case Foo(Bar x): 
    case Foo(Baz x): 
    case TypeDisjointWithFoo t: 

and then fold the head, so we only invoke the Foo dtor once?

Most of the papers about efficient pattern dispatch are relatively little help 
on this front, because the come with the assumption of purity / 
side-effect-freedom.  But it seems obvious that if we were trying to optimize 
dispatch, our cost model would be something like arithmetic op << type test << 
dtor invocation, and so we’d want to optimize for minimizing dtor invocations 
where we can.  

We’ve already asked one of the questions on side effects (though not sure we 
agreed on the answer): what if the dtor throws?  The working story is that the 
exception is wrapped in a MatchException.  (I know you don’t like this, but 
let’s not rehash the same arguments.)  

But, exceptions are easier than general side effects because you can throw at 
most one exception before we bail out; what if your accessor increments some 
global state?  Do we specify a strict order of execution?  

You are appealing to a left-to-right constraint; this is a reasonable thing to 
consider, but surely not the only path.  But I think its a lower-order bit; the 
higher-order bit is whether we are allowed to, or required to, fold multiple 
dtor invocations into one, and similarly whether we are allowed to reorder 
disjoint cases.  

One consistent rule is that we are not allowed to reorder or optimize anything, 
and do everything strictly left-to-right, top-to-bottom.  That would surely be 
a credible answer, and arguably the answer that builds on how the language 
works today.  But I don’t like it so much, because it means we give up a lot of 
optimization ability for something that should never happen.  (This relates to 
a more general question (again, not here) of whether a dtor / declared pattern 
is more like a method (as in Scala, returning Option[Tuple]) or “something 
else”.  The more like a method we tell people it is, the more pattern 
evaluation will feel like method invocation, and the more constrained we are to 
do things strictly top-to-bottom, left-to-right.)  

Alternately, we could let the language have freedom to “cache” the result of 
partial matches, where if we learned, in a previous case, that x matches 
Foo(STUFF), we can reuse that judgment.  And we can go further, to require the 
language to cache in some cases and not in others (I know this is your 
preferred answer.)  



> On Apr 17, 2022, at 5:48 AM, Remi Forax <fo...@univ-mlv.fr> wrote:
> 
> This is something i think we have no discussed, with a record pattern, the 
> switch has to call the record accessors, and those can do side effects,
> revealing the order of the calls to the accessors.
> 
> So by example, with a code like this
> 
>  record Foo(Object o1, Object o2) {
>    public Object o2() {
>      throw new AssertionError();
>    }
>  }
> 
>  void int m(Foo foo) {
>    return switch(foo) {
>      case Foo(String s, Object o2) -> 1
>      case Foo foo -> 2
>    };
>  }
> 
>  m(new Foo(3, 4));   // throw AssertionError ?
> 
> Do the call throw an AssertionError ?
> I believe the answer is no, because 3 is not a String, so Foo::o2() is not 
> called.
> 
> Rémi

Reply via email to