Hello Swift Evolution,

I took up the cause of implementing SE-0155 
<https://github.com/apple/swift-evolution/blob/master/proposals/0155-normalize-enum-case-representation.md>,
 and am most of the way through the larger points of the proposal.  One thing 
struck me when I got to the part about normalizing the behavior of pattern 
matching 
<https://github.com/apple/swift-evolution/blob/master/proposals/0155-normalize-enum-case-representation.md#pattern-consistency>.
  The Core Team indicated in their rationale 
<https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170417/035972.html>
 that the proposal’s suggestion that a variable binding sub in for a label was 
a little much as in this example:

enum Foo {
  case foo(x: Int, y: Int)
}
if case let .foo(x: x, y: y) {} // Fine!  Labels match and are in order
if case let .foo(x, y: y) {} // Bad!  Missing label 'x'
if case let .foo(x, y) {} // Fine?  Missing labels, but variable names match 
labels

They instead suggested the following behavior:

enum Foo {
  case foo(x: Int, y: Int)
}
if case let .foo(x: x, y: y) {} // Fine!  Labels match and are in order
if case let .foo(x, y: y) {} // Bad!  Missing label 'x'
if case let .foo(x, y) {} // Fine?  Missing labels, and full name of case is 
unambiguous

Which, for example, would reject this:

enum Foo {
  case foo(x: Int, y: Int) // Note: foo(x:y:)
  case foo(x: Int, z: Int) // Note: foo(x:z:)
}
if case let .foo(x, y) {} // Bad!  Are we matching foo(x:y:) or foo(x:z:)?

With this reasoning:

>  - While an associated-value label can indeed contribute to the readability 
> of the pattern, the programmer can also choose a meaningful name to bind to 
> the associated value.  This binding name can convey at least as much 
> information as a label would.
> 
>   - The risk of mis-labelling an associated value grows as the number of 
> associated values grows.  However, very few cases carry a large number of 
> associated values.  As the amount of information which the case should carry 
> grows, it becomes more and more interesting to encapsulate that information 
> in its own struct — among other reasons, to avoid the need to revise every 
> matching case-pattern in the program.  Furthermore, when a case does carry a 
> significant number of associated values, there is often a positional 
> conventional between them that lowers the risk of re-ordering: for example, 
> the conventional left-then-right ordering of a binary search tree.  Therefore 
> this risk is somewhat over-stated, and of course the programmer should remain 
> free to include labels for cases where they feel the risk is significant.
> 
>   - It is likely that cases will continue to be predominantly distinguished 
> by their base name alone.  Methods are often distinguished by argument labels 
> because the base name identifies an entire class of operation with many 
> possible variants.  In contrast, each case of an enum is a kind of data, and 
> its name is conventionally more like the name of a property than the name of 
> a method, and thus likely to be unique among all the cases.  Even when cases 
> are distinguished using only associated value labels, it simply means that 
> the corresponding case-patterns must include those labels; we should not feel 
> required to force that burden on all other case-patterns purely to achieve 
> consistency with this presumably-unusual style.
> Accordingly, while it needs to be possible to include associated value labels 
> in a case-pattern, and in some situations it may be wise to include them, the 
> core team believes that requiring associated value labels would be unduly 
> onerous.


This sounds fine in principle, but I believe it is inconsistent with the goals 
of the proposal and doesn’t actually normalize much about the existing pattern 
matching process.  As it stands, labels may be omitted from patterns because 
Swift’s philosophy before this proposal is that associated values in enum cases 
were conceptually tuples.  With the addition of default arguments, the ability 
to overload case names with differing associated value labels, and making the 
labels part of the API name, there is no reason we should allow tuple-like 
behavior in just this one case.

> While an associated-value label...

While it is true that a user often has a domain-specific intention for 
variables created during the destructuring process, the labels do not distract 
from the original purpose of the API and the user is still free to provide 
whatever name they see fit.

> Therefore this risk is somewhat over-stated, and of course the programmer 
> should remain free to include labels for cases where they feel the risk is 
> significant...

This is phrased as a matter of choice, in practice this is perplexing.  Recall 
an earlier rejected pattern:

enum Foo {
  case foo(x: Int, y: Int)
}
if case let .foo(x, y: y) {} // Bad!  Missing label ‘x'

>From the user’s perspective, it is obvious what should happen: Either they 
>did, or did not, intend to match labels.  From the compiler’s perspective this 
>is a proper ambiguity.  Did the user intend to provide a “more meaningful 
>name” and hence meant to elide the label, or did the user intend to match all 
>the labels but forgot or deleted one?  It is not obvious why, if we’re making 
>the distinction, we should assume one way or the other.   This case only gets 
>worse when we must diagnose intent if the case is also overloaded by base name.

I don’t see how it is "unduly onerous” to teach code completion to suggest the 
full name of an enum case everywhere or to create diagnostics that always 
insert missing labels in patterns to correct the user’s mistake.  Freedom of 
choice is, in this case, only making a hard problem harder.

> It is likely that cases will continue to be predominantly distinguished by 
> their base name alone...

This makes sense given the current state of the world, but under this proposal 
we fully expect users to be overloading that base name and writing more and 
more ambiguous patterns.  We should encourage disambiguating these cases with 
labels as a matter of both principle and QoI.  

A pattern is meant to mirror the way a value was constructed with destructuring 
acting as a dual to creation.  By maintaining the structure of the value in the 
pattern, labels included, users can properly convey that they intend the label 
to be a real part of the API of an enum case with associated values instead of 
just an ancillary storage area.  Further, we can actually simplify pattern 
matching by making enum cases consistent with something function-like instead 
of tuple-like.

To that end, I'd like the rationale and the proposal to be amended to require 
labels in patterns in all cases.

Thoughts?

~Robert Widmann

_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to