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 <[email protected]> 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