Some thoughts about the recent discussion on Member Patterns.

David Alayachew davidalayachew at gmail.com
Fri Apr 5 16:01:10 UTC 2024


Most of my points are now defunct, but I will answer just to clear my
intent.

When I said leaky, I was referring to the fact that it is very easy to add
a pattern to the class, forget to add it to the accompanying  pattern set,
and then introduce a bug. I was more bemoaning the loss of a safety net
that was available to us with sealed types, but after rereading again, that
doesn't feel like a strong argument at all.

As for the other points Brian proved them wrong. But I do appreciate you
expanding on why they are wrong! The constructor point was very useful to
communicate the migration incompatibility.

On Fri, Apr 5, 2024, 11:50 AM Red IO <redio.development at gmail.com> wrote:

> Some thoughts on your concerns.
>
> On Fri, Apr 5, 2024, 03:20 David Alayachew <davidalayachew at gmail.com>
> wrote:
>
>> Hello Amber Dev Team,
>>
>> I wanted to chime into the recent discussion about Member Patterns, but
>> on a side topic. I decided to make this a separate thread to avoid
>> distracting from the main discussion.
>>
>> In that discussion, I saw Brian Goetz make the following claim.
>>
>> > ## Exhaustiveness
>> >
>> > There is one last syntax question in front of us: how to
>> > indicate that a set of patterns are (claimed to be)
>> > exhaustive on a given match candidate type.  We see this
>> > with `Optional::of` and `Optional::empty`; it would be
>> > sad if the compiler did not realize that these two
>> > patterns together were exhaustive on `Optional`. This is
>> > not a feature that will be used often, but not having it
>> > at all will be repeated irritant.
>> >
>> > The best I've come up with is to call these `case`
>> > patterns, where a set of `case` patterns for a given
>> > match candidate type in a given class are asserted to be
>> > an exhaustive set:
>> >
>> > ```
>> > class Optional<T> {
>> >     static<T> Optional<T> of(T t) { ... }
>> >     static<T> Optional<T> empty() { ... }
>> >
>> >     static<T> case pattern of(T t) for Optional<T> { ... }
>> >     static<T> case pattern empty() for Optional<T> { ... }
>> > }
>> > ```
>> >
>> > Because they may not be truly exhaustive, `switch`
>> > constructs will have to back up the static assumption of
>> > exhaustiveness with a dynamic check, as we do for other
>> > sets of exhaustive patterns that may have remainder.
>> >
>> > I've experimented with variants of `sealed` but it felt
>> > more forced, so this is the best I've come up with.
>>
>> Later on, I saw Clement Charlin make the following response.
>>
>> > # Exhaustiveness
>> >
>> > The `case` modifier is fine, but the design should leave
>> > room for `case LABEL` or `case (LABEL1, LABEL2)` to
>> > delineate membership in exhaustive set(s), as a potential
>> > future enhancement.
>>
>> To be explicit, I am assuming that we will eventually be able to
>> exhaustively deconstruct Optional using something like the following.
>>
>> switch (someOptional)
>> {
>>
>>     case null               -> System.out.println("The Optional itself is
>> null?!");
>>     case Optional.of(var a) -> System.out.println("Here is " + a);
>>     case Optional.empty()   -> System.out.println("There's nothing here");
>>
>>     //no default clause needed because this is exhaustive
>>
>> }
>>
>> Once pattern-matching lands for normal classes, Optional is almost
>> guaranteed to be the class most frequently deconstructed/pattern-matched.
>> And since it does not use sealed types, it will really push a lot of people
>> to model exhaustiveness as a set of methods.
>>
>> It's kind of frustrating.
>>
>> One article that captures my frustration well is from Alexis King --
>> "Parse, don't Validate" [1].
>>
>> In it, she talks about the value of parsing data into a container object,
>> with the intent of capturing and RETAINING validation via the type name.
>>
>> String validEmailAddress vs record ValidEmailAddress(String email) {/**
>> Validation logic in the canonical constructor. */}
>>
>> The moment that the String validEmailAddress leaves the local scope where
>> the validation occurred, its validation is no longer known except through
>> tracing. But having a full-blown type allows you to assert that the
>> validation has already been done, with no possible chance for misuse or
>> mistakes.
>>
>> I guess my question is, in what instances would we say that modeling a
>> set of patterns rather than a set of types would be better? The only
>> argument that I can think of is conciseness. Or maybe we don't want to
>> poison our type hierarchy with an edge case scenario. That point
>> specifically seems to be the logic that Optional is following.
>>
>> My hesitation comes from the fact that pattern sets feel a little leaky.
>> And leaky gives me distress when talking about exhaustiveness checking.
>>
>> With sealed types, if I want to implement SomeSealedInterface, I **MUST**
>> acknowledge the question of exhaustiveness. There's no way for me to avoid
>> it. My implementing type MUST be sealed, final, or non-final. And even if I
>> implement/extend one of the implementing types of SomeSealedInterface, they
>> either propogate the question, or they opt-out of exhaustiveness checking.
>> Bullet proof!
>>
>> But adding a pattern to a class does not carry the same guarantee. If I
>> add a new pattern that SHOULD have been part of the exhaustive set, but
>> isn't, I have introduced a bug. This same bug is NOT POSSIBLE with sealed
>> types. Hence, leaky.
>>
>
> I don't see how it would be leaky. Adding a case pattern to a class
> would/should be considered a braking change to the class same as adding a
> new type to a sealed types hierarchy. Causing compile errors on every
> previously exhaustive pattern match.
>
>
>> I guess my thoughts could be summed up as the following -- I feel like we
>> are making an escape-hatch for Optional that I don't think would be worth
>> the weight if there was any other way for Optional to be exhaustive. And if
>> that is truly true, does that REALLY justify doing this? Feels tacked onto
>> the side and leaky imo.
>>
>> And I will close by saying, I actually used to think this was a good
>> idea. I have said so on multiple occasions on this exact mailing list. But
>> the more that I think about it, the more that I see no real good reason to
>> do this other than "Optional needs it".
>>
>
> There is a need for objects to be distinguishable in different states
> without being split in different types. As much as I love type driven
> design it isn't the only case for pattern matching. Especially with things
> like value classes that are fundamentally incompatible with identity based
> type matching of sealed types. Also old classes that expose constructors
> can't be turned into a sealed hierarchy without braking api changes. But
> changing a class from 0 to X case patterns isn't a braking change similar
> to the addition of generics in the past.
>
>
>> * Conciseness? Not a strong argument. Conciseness should be used to
>> communicate a semantic difference, not just to shorten code. Think if
>> statements vs ternary expressions.
>>
>> * Semantic difference? Barely, and not in a way that isn't otherwise
>> possible. It's just when clauses with exhaustiveness attached tbh. You're
>> better off modeling it explicitly. Again, Parse, don't validate.
>>
>> Thank you all for your time and help!
>> David Alayachew
>>
>> [1] = https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/
>>
>
> Great regards
> RedIODev
>
>>
>>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-dev/attachments/20240405/3ec880fd/attachment.htm>


More information about the amber-dev mailing list