[Review] SE-0155: Normalize Enum Case Representation


(John McCall) #1

Hello Swift community,

The review of "SE-0155: Normalize Enum Case Representation" begins now and runs through the Monday after next, April 10th. The proposal is available here:
  https://github.com/apple/swift-evolution/blob/master/proposals/0155-normalize-enum-case-representation.md

This is the second review of this proposal. A previous version of this proposal was returned for revision based on some significant concerns from the community. Because this is a continuation of an existing proposal, the core team is still willing to consider it, despite the fact that the review period will end after the formal end of Phase 2.

Reviews are an important part of the Swift evolution process. All reviews should be sent to the swift-evolution mailing list at
  https://lists.swift.org/mailman/listinfo/swift-evolution
or, if you would like to keep your feedback private, directly to the review manager. When replying, please try to keep the proposal link at the top of the message:

  Proposal link: https://github.com/apple/swift-evolution/blob/master/proposals/0155-normalize-enum-case-representation.md

  Reply text

  Other replies

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at https://github.com/apple/swift-evolution/blob/master/process.md

Thank you,

John McCall
Review Manager


(Matthew Johnson) #2

  • What is your evaluation of the proposal?

+1. I am very happy with the tradeoffs made in the revision of this proposal. It feels like the right step for Swift 4. It updates enum cases to align with the general direction Swift has taken. This improves consistency now and provides the right foundation for the future enhancements.

  • Is the problem being addressed significant enough to warrant a change to Swift?

Yes, enum cases as they exist in Swift 3.1 are a bit out of sync with the rest of the language. This proposal addresses this directly.

  • Does this proposal fit well with the feel and direction of Swift?

Yes, very much.

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

N/A

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

I have followed all of the relevant threads and drafts closely and provided feedback regularly. The final draft shows that the authors have done great job of listening to the community and incorporating feedback. Thank you very much for continuing to push this forward!


(Xiaodi Wu) #3

Hello Swift community,

The review of "SE-0155: Normalize Enum Case Representation" begins now and
runs through the Monday after next, April 10th. The proposal is available
here:
https://github.com/apple/swift-evolution/blob/master/
proposals/0155-normalize-enum-case-representation.md

This is the second review of this proposal. A previous version of this
proposal was returned for revision based on some significant concerns from
the community. Because this is a continuation of an existing proposal, the
core team is still willing to consider it, despite the fact that the review
period will end after the formal end of Phase 2.

Reviews are an important part of the Swift evolution process. All reviews
should be sent to the swift-evolution mailing list at
https://lists.swift.org/mailman/listinfo/swift-evolution
or, if you would like to keep your feedback private, directly to the
review manager. When replying, please try to keep the proposal link at the
top of the message:

Proposal link: https://github.com/apple/swift-evolution/blob/
master/proposals/0155-normalize-enum-case-representation.md

Reply text

Other replies

*What goes into a review?*

The goal of the review process is to improve the proposal under review
through constructive criticism and, eventually, determine the direction of
Swift. When writing your review, here are some questions you might want to
answer in your review:

• What is your evaluation of the proposal?

The general direction is, of course, correct. However, I continue to be
concerned about the details of pattern matching. I will detail those below.

• Is the problem being addressed significant enough to warrant a change to

Swift?

Yes

• Does this proposal fit well with the feel and direction of Swift?

The "Pattern consistency" section does not align well with the feel and
direction of Swift. Specifically, it does not explore some of the
difficulties that arise from the proposed rules, adopts some of the same
shortcomings that required revision for SE-0111, and deviates from some of
the anticipated fixes for those shortcomings outlined in the core team's
"update and commentary" to SE-0111.

It is not the case that the design proposed is "a consequence of no longer
relying on tuple patterns," in that it is not the inevitable result that
falls out of that decision. I will detail the alternative design that
requires the fewest deviations or special rules, and breaks the least code
extant today, later on. First, the shortcomings:

1.
The proposed rules for pattern matching are a source-breaking change, and
are *not* the most minimal such change given the abandoning of tuples (see
alternative below). However, the proposal does not engage with the core
team's Swift 4 criteria for source-breaking changes with respect to the
proposed "stricter rules" for pattern matching. There is no text at all
about why specifically having the compiler encourage local _variable_ names
to match argument labels resolves an active harm that outweighs the goal of
preserving the greatest possible source compatibility.

OTOH, the proposal does outline a major use case for a local variable name
that does not match the argument label: `param` vs `parameter`.
Widely-respected style guides in various languages encourage unabbreviated
and descriptive API names but much more concise local variable names. This
is a legitimate and good practice being actively discouraged by the sugared
rules.

This would be merely annoying and not harmful if we could guarantee that it
only means the API user will have to use longer local names, but the
natural impulse on the part of thoughtful API authors would be to limit the
expressiveness of their labels to help out their users.

This puts API authors in an impossible bind: they need to choose labels
that are not too short lest it collide frequently with existing local
variable names (`x` and `y` would be suboptimal, for example, but there are
good reasons why an associated value might have arguments labeled `x` and
`y`), but they also need to choose labels that are not too verbose. The
safest bet in this case would be not to label at all, but then they lose
the communicative aspect of argument labels (see point 2 below).

2.
In the "update and commentary" revising SE-0111, it was acknowledged that
"cosmetic" labels have a significant use case. Thus, the rules were changed
to allow `(_ foo: Int, _ bar: Int) -> ()` to communicate to the reader of
code that the first argument serves some purpose "foo" without forcing that
name to be part of the API, pending further revisions.

Because enum cases are currently tuples, labels can be dropped freely, and
therefore these labels are effectively "optional" parts of the API that can
be seen by the user but, at their discretion, not used. That fulfills the
use case of "cosmetic" labels. In this revised proposal, by requiring the
argument label to be actually _written_ somewhere by the API user, it puts
a dent into the legitimate use case of "cosmetic" labels.

That is to say, an API author who wishes to communicate something about a
parameter by using a label must now also consider if that label is also
appropriate as a variable name and must forgo its use if the label is not
so appropriate. This is a very different decision-making process and it is
being applied retroactively to previously designed APIs whose labels would
have been (hopefully thoughtfully) chosen under very different
circumstances.

3.
The first part of the proposal aligns enum case syntax with functions.
Functions often taken prepositions as argument labels, and indeed previous
SE proposals have extended the rules to allow most words. However, `case
foo(index: Int, in: T)` would have a disastrous label, as `in` would be a
very annoying variable name whose use would be actively encouraged by the
proposed sugared pattern matching rules.

The proposed rules for the sugared pattern would also require (well,
greatly encourage) unique labels for each argument. This again is
inconsistent with the naming conventions encouraged by the first part of
the proposal aligning enum case syntax with functions, which have no such
restrictions. If a user names something `case foo(point: T, point: T)`,
then the matching rules would actively encourage an invalid redefinition of
a variable named `point`.

(On the other hand, the API author does not have the luxury of naming the
same case `foo(from point: T, to point: T)`, and even if they did,
prepositions can make lousy local variable names--see first paragraph.)

4.
The proposal does not explore what happens when the proposed prohibition on
"mixing and matching" the proposed sugared and unsugared pattern matching
runs up against associated values that have a mix of labeled and unlabeled
parameters, and pattern matching user cases where the user does not wish to
bind all of the arguments.

Given `case foo(a: Int, String, b: Int, String)`, the only sensible
interpretation of the rules for sugared syntax would allow the user to
choose any name for some but not all of the labels. If the user wishes to
bind only `b`, however, he or she will need to navigate a puzzling set of
rules that are not spelled out in the proposal:

case foo(a: _, _, b: let b, _)
// this is definitely allowed

case foo(a: _, _, b: let myVar, _)
// this is also definitely allowed

// but...
case foo(_, _, b: let myVar, _)
// is this allowed, or must the user explicitly state and not bind `a`?

// ...and with respect to the sugared version...
case foo(_, _, let b, _)
// is this allowed, or must the user explicitly state and not bind `a`?

5.
In the "update and commentary" revising SE-0111, the core team outlined a
preferred path to restoring the full use of argument labels for functions
without giving them type system significance. They gave a non-sugared form
and a sugared form, both of which have met with approval from the community.

Briefly, the non-sugared form allows compound names to be used in variable
names: `func foo(opToUse op(lhs:rhs:) : (Int, Int) -> Int)`. The first part
of this proposal is consistent in that it removes the type system
significance of argument labels from the associated values of enum cases,
and considers them as part of the enum case name. It also stands to reason
that, if a user were to match a case _without_ trying to bind any
variables, the same syntax would have be used if the base name is
ambiguous: `case elet(locals:body:): break`.

However, the proposal makes no provision for using that same compound name
in pattern matching. There appears to be no particular reason for its
isolated omission here, as `case elet(locals:body:)(let a, let b): return a
* b` is readable and presents no syntactic difficulties. (Moreover, it is
consistent with the syntax permitted in this proposal for initializing a
variable: `let foo = Expr.elet(locals:body:)([], anExpr)`.)

···

On Fri, Mar 31, 2017 at 12:33 PM, John McCall via swift-evolution < swift-evolution@swift.org> wrote:

---

In light of these shortcomings, I would argue that the following
alternative scheme is the most intuitive and consistent for pattern
matching given the general agreement that enum case representation should
be "normalized":

Given:

enum S {
  case foo(bar: Int, baz: Int)
  case foo(boo: String)
  case bar(boo: String)
}

a. As in functions after SE-0111, enum cases can be identified
unambiguously, regardless of whether one is initializing a variable or
matching a case, by their compound name, e.g. `bar(boo:)`. Where a case can
be unambiguously identified with only the base name, that is an alternative
spelling, e.g. `bar`. Where a case cannot be identified uniquely with the
base name, then it is an error to try to use the base name alone: `case
foo: break // error: unambiguous`.

b. As in functions after SE-0111, arguments can be passed in either a
sugared form or an unsugared form, and they can be bound in a pattern
matching statement in the same way. That is, `case foo(bar: let a, baz: let
b): break` and `case foo(bar:baz:)(let a, let b): break` are equivalent.

c. As in functions, one cannot supply different or incorrect argument
labels. That is, `case foo(baz: let a, bar: let b)` and `case
foo(baz:bar:)(let a, let b)` are both forbidden. _This recovers the vast
majority of the additional syntactic safety that is outlined in the revised
proposal, but without the use of any special rules for pattern matching._

d. By composing rules (a) and (b), `case bar(let a)` is allowed as it is
today, preserving source compatibility. However `case foo(let b, let c)` is
not allowed, and _not_ because different local variable names are chosen,
but because the enum has two cases named foo.

I believe that this alternative preserves achieves the goals of normalizing
enum case representation, simplifying the rules around pattern matching and
adding safety to pattern matching by preventing reordering/mismatching of
labels that were sometimes permitted with tuples, and preserving almost all
source compatibility, without the use of ad-hoc rules.

• If you have used other languages or libraries with a similar feature, how

do you feel that this proposal compares to those?

N/A

• How much effort did you put into your review? A glance, a quick reading,

or an in-depth study?

In-depth study


(Daniel Duan) #4

Thanks again for a detailed review. I have a few comments inline.

  • Does this proposal fit well with the feel and direction of Swift?

The "Pattern consistency" section does not align well with the feel and direction of Swift. Specifically, it does not explore some of the difficulties that arise from the proposed rules, adopts some of the same shortcomings that required revision for SE-0111, and deviates from some of the anticipated fixes for those shortcomings outlined in the core team's "update and commentary" to SE-0111.

It is not the case that the design proposed is "a consequence of no longer relying on tuple patterns," in that it is not the inevitable result that falls out of that decision.

The text in this revision may be poorly phrased. The connection, as I pointed out in an previous thread, is that we need to define syntax for enum pattern matching because the one we’ve been using in Swift 3 is tuple pattern’s syntax, which is now distinct and separate.

I will detail the alternative design that requires the fewest deviations or special rules, and breaks the least code extant today, later on. First, the shortcomings:

1.
The proposed rules for pattern matching are a source-breaking change, and are *not* the most minimal such change given the abandoning of tuples (see alternative below). However, the proposal does not engage with the core team's Swift 4 criteria for source-breaking changes with respect to the proposed "stricter rules" for pattern matching. There is no text at all about why specifically having the compiler encourage local _variable_ names to match argument labels resolves an active harm that outweighs the goal of preserving the greatest possible source compatibility.

With this proposal, user can still use local variable names. It is true that if there are many ways to achieve the same thing, the compiler would be encouraging user to do that thing. But that puts a cost on the compiler, new users and experienced readers in unfamiliar codebases. This is (albeit not to a satisfactory degree, it seems) pointed out in the motivation section.

As for source compatibility, Swift 3 code should continue to work with warnings. Swift 4 mode would issue errors along with fix-its, which the migrator can leverage. Depends on core team/community’s implementor resource, there’s even a chance that this change would roll out one version later (warning in 4.X, error in 5.Y). In theory, the migration hurdle can be minimized.

OTOH, the proposal does outline a major use case for a local variable name that does not match the argument label: `param` vs `parameter`. Widely-respected style guides in various languages encourage unabbreviated and descriptive API names but much more concise local variable names. This is a legitimate and good practice being actively discouraged by the sugared rules.

This not a counterpoint, but I personally think using shortened names is not something to be encouraged. A (admittedly quirky) practice some of us inherited from the Cocoa style guideline is to use real, complete words for variable names. I’d like to think that The Swift API Design Guidelines are aligned in spirit on this matter - “clarity is more important than brevity”. (incidentally, the guidelines’s code samples don’t contain partial-word variables anywhere).

This would be merely annoying and not harmful if we could guarantee that it only means the API user will have to use longer local names, but the natural impulse on the part of thoughtful API authors would be to limit the expressiveness of their labels to help out their users.

This puts API authors in an impossible bind: they need to choose labels that are not too short lest it collide frequently with existing local variable names (`x` and `y` would be suboptimal, for example, but there are good reasons why an associated value might have arguments labeled `x` and `y`),

API authors are already in this impossible bind: whenever they export a type name, a method signature in an open class or a protocol, risk of collision come up.

When a local variable does collide with a payload label, it would be bad if the user accidentally used the variable _in stead of_ the actual payload value. Forcing users to proactively rebind the variable would make them more mindful for this type of mistake.

but they also need to choose labels that are not too verbose. The safest bet in this case would be not to label at all, but then they lose the communicative aspect of argument labels (see point 2 below).

A more realistic version of the story: API author choose labels that make the most sense for the declaration and user accept the risk of collision as they use the API. Most of those who choose to skip labels would not have given this much thought about their effect at all.

2.
In the "update and commentary" revising SE-0111, it was acknowledged that "cosmetic" labels have a significant use case. Thus, the rules were changed to allow `(_ foo: Int, _ bar: Int) -> ()` to communicate to the reader of code that the first argument serves some purpose "foo" without forcing that name to be part of the API, pending further revisions.

Because enum cases are currently tuples, labels can be dropped freely, and therefore these labels are effectively "optional" parts of the API that can be seen by the user but, at their discretion, not used. That fulfills the use case of "cosmetic" labels. In this revised proposal, by requiring the argument label to be actually _written_ somewhere by the API user, it puts a dent into the legitimate use case of "cosmetic" labels.

That is to say, an API author who wishes to communicate something about a parameter by using a label must now also consider if that label is also appropriate as a variable name and must forgo its use if the label is not so appropriate. This is a very different decision-making process and it is being applied retroactively to previously designed APIs whose labels would have been (hopefully thoughtfully) chosen under very different circumstances.

This is something we never agreed on: SE-0111 is about functions. In some languages, patterns does resemble constructor functions, but that’s as much similarity as one can get anywhere. I still think applying every decision we made about functions to pattern matching is weird. But here’s my analysis anyways: the “cosmetic label” comment is about paving a way to restore expressivity of closures. It talks about the *interaction* between a function/closure’s declaration and use site — if parameter names are provided in a closure’s declaration, they should be required at invocation, similar to pre-SE-0111. IMO this proposal makes enum case and patterns closer to this goal.

3.
The first part of the proposal aligns enum case syntax with functions. Functions often taken prepositions as argument labels, and indeed previous SE proposals have extended the rules to allow most words. However, `case foo(index: Int, in: T)` would have a disastrous label, as `in` would be a very annoying variable name whose use would be actively encouraged by the proposed sugared pattern matching rules.

The proposed rules for the sugared pattern would also require (well, greatly encourage) unique labels for each argument. This again is inconsistent with the naming conventions encouraged by the first part of the proposal aligning enum case syntax with functions, which have no such restrictions. If a user names something `case foo(point: T, point: T)`, then the matching rules would actively encourage an invalid redefinition of a variable named `point`.

(On the other hand, the API author does not have the luxury of naming the same case `foo(from point: T, to point: T)`, and even if they did, prepositions can make lousy local variable names--see first paragraph.)

I don’t see this as a problem for enum case authors. It just means the poor pattern writer needs to provide the positional information to disambiguate.

4.
The proposal does not explore what happens when the proposed prohibition on "mixing and matching" the proposed sugared and unsugared pattern matching runs up against associated values that have a mix of labeled and unlabeled parameters, and pattern matching user cases where the user does not wish to bind all of the arguments.

Given `case foo(a: Int, String, b: Int, String)`, the only sensible interpretation of the rules for sugared syntax would allow the user to choose any name for some but not all of the labels. If the user wishes to bind only `b`, however, he or she will need to navigate a puzzling set of rules that are not spelled out in the proposal:

case foo(a: _, _, b: let b, _)
// this is definitely allowed

case foo(a: _, _, b: let myVar, _)
// this is also definitely allowed

// but...
case foo(_, _, b: let myVar, _)
// is this allowed, or must the user explicitly state and not bind `a`?

// ...and with respect to the sugared version...
case foo(_, _, let b, _)
// is this allowed, or must the user explicitly state and not bind `a`?

Good point. To make up for this: `_` can substitute any sub pattern, which is something that this proposal doesn’t change but definitely worth spelling out.

5.
In the "update and commentary" revising SE-0111, the core team outlined a preferred path to restoring the full use of argument labels for functions without giving them type system significance. They gave a non-sugared form and a sugared form, both of which have met with approval from the community.

Briefly, the non-sugared form allows compound names to be used in variable names: `func foo(opToUse op(lhs:rhs:) : (Int, Int) -> Int)`. The first part of this proposal is consistent in that it removes the type system significance of argument labels from the associated values of enum cases, and considers them as part of the enum case name. It also stands to reason that, if a user were to match a case _without_ trying to bind any variables, the same syntax would have be used if the base name is ambiguous: `case elet(locals:body:): break`.

However, the proposal makes no provision for using that same compound name in pattern matching. There appears to be no particular reason for its isolated omission here, as `case elet(locals:body:)(let a, let b): return a * b` is readable and presents no syntactic difficulties. (Moreover, it is consistent with the syntax permitted in this proposal for initializing a variable: `let foo = Expr.elet(locals:body:)([], anExpr)`.)

Another good point. We can handle this in the purely additional proposal for compound variable names. I consider this not the 5th item in the list, but a separate suggestion, however :stuck_out_tongue:

---

In light of these shortcomings, I would argue that the following alternative scheme is the most intuitive and consistent for pattern matching given the general agreement that enum case representation should be "normalized":

Given:

enum S {
  case foo(bar: Int, baz: Int)
  case foo(boo: String)
  case bar(boo: String)
}

a. As in functions after SE-0111, enum cases can be identified unambiguously, regardless of whether one is initializing a variable or matching a case, by their compound name, e.g. `bar(boo:)`. Where a case can be unambiguously identified with only the base name, that is an alternative spelling, e.g. `bar`. Where a case cannot be identified uniquely with the base name, then it is an error to try to use the base name alone: `case foo: break // error: unambiguous`.

b. As in functions after SE-0111, arguments can be passed in either a sugared form or an unsugared form, and they can be bound in a pattern matching statement in the same way. That is, `case foo(bar: let a, baz: let b): break` and `case foo(bar:baz:)(let a, let b): break` are equivalent.

c. As in functions, one cannot supply different or incorrect argument labels. That is, `case foo(baz: let a, bar: let b)` and `case foo(baz:bar:)(let a, let b)` are both forbidden. _This recovers the vast majority of the additional syntactic safety that is outlined in the revised proposal, but without the use of any special rules for pattern matching._

d. By composing rules (a) and (b), `case bar(let a)` is allowed as it is today, preserving source compatibility. However `case foo(let b, let c)` is not allowed, and _not_ because different local variable names are chosen, but because the enum has two cases named foo.

From a user’s point of view, there’s enough positional information in this pattern for the compiler to figure out which case it should match. This would be very unintuitive IMO.

···

On Apr 1, 2017, at 9:50 AM, Xiaodi Wu via swift-evolution <swift-evolution@swift.org> wrote:

I believe that this alternative preserves achieves the goals of normalizing enum case representation, simplifying the rules around pattern matching and adding safety to pattern matching by preventing reordering/mismatching of labels that were sometimes permitted with tuples, and preserving almost all source compatibility, without the use of ad-hoc rules.

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

N/A

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

In-depth study

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Xiaodi Wu) #5

Thanks again for a detailed review. I have a few comments inline.

• Does this proposal fit well with the feel and direction of Swift?

The "Pattern consistency" section does not align well with the feel and
direction of Swift. Specifically, it does not explore some of the
difficulties that arise from the proposed rules, adopts some of the same
shortcomings that required revision for SE-0111, and deviates from some of
the anticipated fixes for those shortcomings outlined in the core team's
"update and commentary" to SE-0111.

It is not the case that the design proposed is "a consequence of no longer
relying on tuple patterns," in that it is not the inevitable result that
falls out of that decision.

The text in this revision may be poorly phrased. The connection, as I
pointed out in an previous thread, is that we need to define syntax for
enum pattern matching because the one we’ve been using in Swift 3 is tuple
pattern’s syntax, which is now distinct and separate.

What I'm saying here is that, although _some_ change becomes necessary, the
particular changes proposed here are not themselves "a consequence of no
longer relying on tuple patterns."

Put another way, given `enum E { case foo(bar: Int, baz: Int) }`, not being
allowed to write `switch e { case foo(let a, let b): break }` is *not* an
inevitable consequence of moving away from tuple patterns. Since the
particular proposed changes break more existing source code than is
strictly necessary for moving away from tuple-based pattern matching, those
choices require stringent justification.

I will detail the alternative design that requires the fewest deviations or

special rules, and breaks the least code extant today, later on. First, the
shortcomings:

1.
The proposed rules for pattern matching are a source-breaking change, and
are *not* the most minimal such change given the abandoning of tuples (see
alternative below). However, the proposal does not engage with the core
team's Swift 4 criteria for source-breaking changes with respect to the
proposed "stricter rules" for pattern matching. There is no text at all
about why specifically having the compiler encourage local _variable_ names
to match argument labels resolves an active harm that outweighs the goal of
preserving the greatest possible source compatibility.

With this proposal, user can still use local variable names. It is true
that if there are many ways to achieve the same thing, the compiler would
be encouraging user to do that thing. But that puts a cost on the compiler,
new users and experienced readers in unfamiliar codebases. This is (albeit
not to a satisfactory degree, it seems) pointed out in the motivation
section.

As for source compatibility, Swift 3 code should continue to work with
warnings. Swift 4 mode would issue errors along with fix-its, which the
migrator can leverage. Depends on core team/community’s implementor
resource, there’s even a chance that this change would roll out one version
later (warning in 4.X, error in 5.Y). In theory, the migration hurdle can
be minimized.

Many syntactic changes can be migrated in this way, but for Swift 4, that
would only be justified when the existing syntax meets a high bar for being
harmful. Again, the overarching theme of my response is that I don't think
the proposed "stricter rules" offer much more harm mitigation than
significantly less source-breaking designs for pattern matching, and I
don't see anything in the proposal text that discusses the issue or
justifies the particular design over less source-breaking alternatives.

OTOH, the proposal does outline a major use case for a local variable name
that does not match the argument label: `param` vs `parameter`.
Widely-respected style guides in various languages encourage unabbreviated
and descriptive API names but much more concise local variable names. This
is a legitimate and good practice being actively discouraged by the sugared
rules.

This not a counterpoint, but I personally think using shortened names is
not something to be encouraged. A (admittedly quirky) practice some of us
inherited from the Cocoa style guideline is to use real, complete words for
variable names. I’d like to think that The Swift API Design Guidelines are
aligned in spirit on this matter - “clarity is more important than
brevity”. (incidentally, the guidelines’s code samples don’t contain
partial-word variables anywhere).

We're talking _local_ variables: local variables aren't API. There are
many, many examples of single-letter variables in the design guidelines.
For example, `x = y.union(z)` has three of them.

This would be merely annoying and not harmful if we could guarantee that
it only means the API user will have to use longer local names, but the
natural impulse on the part of thoughtful API authors would be to limit the
expressiveness of their labels to help out their users.

This puts API authors in an impossible bind: they need to choose labels
that are not too short lest it collide frequently with existing local
variable names (`x` and `y` would be suboptimal, for example, but there are
good reasons why an associated value might have arguments labeled `x` and
`y`),

API authors are already in this impossible bind: whenever they export a
type name, a method signature in an open class or a protocol, risk of
collision come up.

Again, local variables aren't API. API authors have never been in this bind
with respect to local variables. Nothing in the language has ever caused
API to restrict the consumer's choice of local variable names. I think this
is a highly, highly unusual rule.

When a local variable does collide with a payload label, it would be bad
if the user accidentally used the variable _in stead of_ the actual payload
value. Forcing users to proactively rebind the variable would make them
more mindful for this type of mistake.

What mistake do you have in mind? Currently, labels have nothing to do with
variable names. How does a user accidentally use a label name instead of a
variable name?

but they also need to choose labels that are not too verbose. The safest
bet in this case would be not to label at all, but then they lose the
communicative aspect of argument labels (see point 2 below).

A more realistic version of the story: API author choose labels that make
the most sense for the declaration and user accept the risk of collision as
they use the API. Most of those who choose to skip labels would not have
given this much thought about their effect at all.

2.
In the "update and commentary" revising SE-0111, it was acknowledged that
"cosmetic" labels have a significant use case. Thus, the rules were changed
to allow `(_ foo: Int, _ bar: Int) -> ()` to communicate to the reader of
code that the first argument serves some purpose "foo" without forcing that
name to be part of the API, pending further revisions.

Because enum cases are currently tuples, labels can be dropped freely, and
therefore these labels are effectively "optional" parts of the API that can
be seen by the user but, at their discretion, not used. That fulfills the
use case of "cosmetic" labels. In this revised proposal, by requiring the
argument label to be actually _written_ somewhere by the API user, it puts
a dent into the legitimate use case of "cosmetic" labels.

That is to say, an API author who wishes to communicate something about a
parameter by using a label must now also consider if that label is also
appropriate as a variable name and must forgo its use if the label is not
so appropriate. This is a very different decision-making process and it is
being applied retroactively to previously designed APIs whose labels would
have been (hopefully thoughtfully) chosen under very different
circumstances.

This is something we never agreed on: SE-0111 is about functions. In some
languages, patterns does resemble constructor functions, but that’s as much
similarity as one can get anywhere. I still think applying every decision
we made about functions to pattern matching is weird.

I have to admit, I still don't understand your reticence. The first part of
your proposal aligns enum cases with functions. If we are to look for
patterns in something that is spelled like a function, then it is natural
for the pattern itself to be spelled like a function, no? Currently, in
Swift 3, since we're trying to use pattern matching for a tuple, the
pattern is spelled like a tuple. In my simplistic mind, if we're trying to
use pattern matching for a $foo, the pattern should be spelled like a $foo.
Far from being weird, to me that is the only possible intuitive syntax.

But here’s my analysis anyways: the “cosmetic label” comment is about

paving a way to restore expressivity of closures. It talks about the
*interaction* between a function/closure’s declaration and use site — if
parameter names are provided in a closure’s declaration, they should be
required at invocation, similar to pre-SE-0111. IMO this proposal makes
enum case and patterns closer to this goal.

I agree that your proposal does indeed get us closer to SE-0111. By
requiring argument labels chosen by the API author to be written out by the
user, we get closer to the goals of SE-0111. But SE-0111 also had a large
drawback that required post-approval modification, which was that there
ended up being no way to write "cosmetic labels," which both the community
and core team agreed was an important use case.

With functions, that role can be filled with internal parameter names. This
is what the "update and commentary" restored to SE-0111. With tuples, that
role is filled by the labels themselves, because they can be ergonomically
erased. With enum cases, you have not provided a parallel facility for
cosmetic labels, because in your proposal labels can no longer be easily
erased, but nor are there internal parameter names or some other
substitute. I'm saying that we should learn from the problems discovered
after SE-0111 was approved and fix that shortcoming for enum cases before
this proposal is adopted.

3.
The first part of the proposal aligns enum case syntax with functions.
Functions often taken prepositions as argument labels, and indeed previous
SE proposals have extended the rules to allow most words. However, `case
foo(index: Int, in: T)` would have a disastrous label, as `in` would be a
very annoying variable name whose use would be actively encouraged by the
proposed sugared pattern matching rules.

The proposed rules for the sugared pattern would also require (well,
greatly encourage) unique labels for each argument. This again is
inconsistent with the naming conventions encouraged by the first part of
the proposal aligning enum case syntax with functions, which have no such
restrictions. If a user names something `case foo(point: T, point: T)`,
then the matching rules would actively encourage an invalid redefinition of
a variable named `point`.

(On the other hand, the API author does not have the luxury of naming the
same case `foo(from point: T, to point: T)`, and even if they did,
prepositions can make lousy local variable names--see first paragraph.)

I don’t see this as a problem for enum case authors. It just means the
poor pattern writer needs to provide the positional information to
disambiguate.

What do you mean by "positional information" here?

4.

The proposal does not explore what happens when the proposed prohibition
on "mixing and matching" the proposed sugared and unsugared pattern
matching runs up against associated values that have a mix of labeled and
unlabeled parameters, and pattern matching user cases where the user does
not wish to bind all of the arguments.

Given `case foo(a: Int, String, b: Int, String)`, the only sensible
interpretation of the rules for sugared syntax would allow the user to
choose any name for some but not all of the labels. If the user wishes to
bind only `b`, however, he or she will need to navigate a puzzling set of
rules that are not spelled out in the proposal:

case foo(a: _, _, b: let b, _)
// this is definitely allowed

case foo(a: _, _, b: let myVar, _)
// this is also definitely allowed

// but...
case foo(_, _, b: let myVar, _)
// is this allowed, or must the user explicitly state and not bind `a`?

// ...and with respect to the sugared version...
case foo(_, _, let b, _)
// is this allowed, or must the user explicitly state and not bind `a`?

Good point. To make up for this: `_` can substitute any sub pattern, which
is something that this proposal doesn’t change but definitely worth
spelling out.

5.
In the "update and commentary" revising SE-0111, the core team outlined a
preferred path to restoring the full use of argument labels for functions
without giving them type system significance. They gave a non-sugared form
and a sugared form, both of which have met with approval from the community.

Briefly, the non-sugared form allows compound names to be used in variable
names: `func foo(opToUse op(lhs:rhs:) : (Int, Int) -> Int)`. The first
part of this proposal is consistent in that it removes the type system
significance of argument labels from the associated values of enum cases,
and considers them as part of the enum case name. It also stands to reason
that, if a user were to match a case _without_ trying to bind any
variables, the same syntax would have be used if the base name is
ambiguous: `case elet(locals:body:): break`.

However, the proposal makes no provision for using that same compound name
in pattern matching. There appears to be no particular reason for its
isolated omission here, as `case elet(locals:body:)(let a, let b): return a
* b` is readable and presents no syntactic difficulties. (Moreover, it is
consistent with the syntax permitted in this proposal for initializing a
variable: `let foo = Expr.elet(locals:body:)([], anExpr)`.)

Another good point. We can handle this in the purely additional proposal
for compound variable names. I consider this not the 5th item in the list,
but a separate suggestion, however :stuck_out_tongue:

---

In light of these shortcomings, I would argue that the following
alternative scheme is the most intuitive and consistent for pattern
matching given the general agreement that enum case representation should
be "normalized":

Given:

enum S {
  case foo(bar: Int, baz: Int)
  case foo(boo: String)
  case bar(boo: String)
}

a. As in functions after SE-0111, enum cases can be identified
unambiguously, regardless of whether one is initializing a variable or
matching a case, by their compound name, e.g. `bar(boo:)`. Where a case can
be unambiguously identified with only the base name, that is an alternative
spelling, e.g. `bar`. Where a case cannot be identified uniquely with the
base name, then it is an error to try to use the base name alone: `case
foo: break // error: unambiguous`.

b. As in functions after SE-0111, arguments can be passed in either a
sugared form or an unsugared form, and they can be bound in a pattern
matching statement in the same way. That is, `case foo(bar: let a, baz: let
b): break` and `case foo(bar:baz:)(let a, let b): break` are equivalent.

c. As in functions, one cannot supply different or incorrect argument
labels. That is, `case foo(baz: let a, bar: let b)` and `case
foo(baz:bar:)(let a, let b)` are both forbidden. _This recovers the vast
majority of the additional syntactic safety that is outlined in the revised
proposal, but without the use of any special rules for pattern matching._

d. By composing rules (a) and (b), `case bar(let a)` is allowed as it is
today, preserving source compatibility. However `case foo(let b, let c)` is
not allowed, and _not_ because different local variable names are chosen,
but because the enum has two cases named foo.

From a user’s point of view, there’s enough positional information in this
pattern for the compiler to figure out which case it should match. This
would be very unintuitive IMO.

Wait, the key point of your proposal, with its "stricter rules," is that
labels shouldn't be optional even with sufficient positional information!
That's also the whole thing above about getting us closer to aligning with
SE-0111, etc.

···

On Sat, Apr 1, 2017 at 3:38 PM, Daniel Duan <daniel@duan.org> wrote:

On Apr 1, 2017, at 9:50 AM, Xiaodi Wu via swift-evolution < > swift-evolution@swift.org> wrote:


(Xiaodi Wu) #6

<snip>

4.
The proposal does not explore what happens when the proposed prohibition
on "mixing and matching" the proposed sugared and unsugared pattern
matching runs up against associated values that have a mix of labeled and
unlabeled parameters, and pattern matching user cases where the user does
not wish to bind all of the arguments.

Given `case foo(a: Int, String, b: Int, String)`, the only sensible
interpretation of the rules for sugared syntax would allow the user to
choose any name for some but not all of the labels. If the user wishes to
bind only `b`, however, he or she will need to navigate a puzzling set of
rules that are not spelled out in the proposal:

case foo(a: _, _, b: let b, _)
// this is definitely allowed

case foo(a: _, _, b: let myVar, _)
// this is also definitely allowed

// but...
case foo(_, _, b: let myVar, _)
// is this allowed, or must the user explicitly state and not bind `a`?

// ...and with respect to the sugared version...
case foo(_, _, let b, _)
// is this allowed, or must the user explicitly state and not bind `a`?

Good point. To make up for this: `_` can substitute any sub pattern, which
is something that this proposal doesn’t change but definitely worth
spelling out.

This rule cannot hold. You cannot have the shorthand syntax you propose,
disallow mixing of shorthand syntax and the longer form, *and* allow `_` to
substitute for any pattern without a label.

enum E {
  case foo(a: Int, b: Int, c: Int)
  case foo(a: String, c: String, e: String)
}

let e = /* instance of E */

switch e {
case foo(let a, _, _):
  // what type is `a` here?
  break
default:
  fatalError()
}
···

On Sat, Apr 1, 2017 at 3:38 PM, Daniel Duan <daniel@duan.org> wrote:


(Daniel Duan) #7

Thanks again for a detailed review. I have a few comments inline.

  • Does this proposal fit well with the feel and direction of Swift?

The "Pattern consistency" section does not align well with the feel and direction of Swift. Specifically, it does not explore some of the difficulties that arise from the proposed rules, adopts some of the same shortcomings that required revision for SE-0111, and deviates from some of the anticipated fixes for those shortcomings outlined in the core team's "update and commentary" to SE-0111.

It is not the case that the design proposed is "a consequence of no longer relying on tuple patterns," in that it is not the inevitable result that falls out of that decision.

The text in this revision may be poorly phrased. The connection, as I pointed out in an previous thread, is that we need to define syntax for enum pattern matching because the one we’ve been using in Swift 3 is tuple pattern’s syntax, which is now distinct and separate.

What I'm saying here is that, although _some_ change becomes necessary, the particular changes proposed here are not themselves "a consequence of no longer relying on tuple patterns."

Put another way, given `enum E { case foo(bar: Int, baz: Int) }`, not being allowed to write `switch e { case foo(let a, let b): break }` is *not* an inevitable consequence of moving away from tuple patterns. Since the particular proposed changes break more existing source code than is strictly necessary for moving away from tuple-based pattern matching, those choices require stringent justification.

I will detail the alternative design that requires the fewest deviations or special rules, and breaks the least code extant today, later on. First, the shortcomings:

1.
The proposed rules for pattern matching are a source-breaking change, and are *not* the most minimal such change given the abandoning of tuples (see alternative below). However, the proposal does not engage with the core team's Swift 4 criteria for source-breaking changes with respect to the proposed "stricter rules" for pattern matching. There is no text at all about why specifically having the compiler encourage local _variable_ names to match argument labels resolves an active harm that outweighs the goal of preserving the greatest possible source compatibility.

With this proposal, user can still use local variable names. It is true that if there are many ways to achieve the same thing, the compiler would be encouraging user to do that thing. But that puts a cost on the compiler, new users and experienced readers in unfamiliar codebases. This is (albeit not to a satisfactory degree, it seems) pointed out in the motivation section.

As for source compatibility, Swift 3 code should continue to work with warnings. Swift 4 mode would issue errors along with fix-its, which the migrator can leverage. Depends on core team/community’s implementor resource, there’s even a chance that this change would roll out one version later (warning in 4.X, error in 5.Y). In theory, the migration hurdle can be minimized.

Many syntactic changes can be migrated in this way, but for Swift 4, that would only be justified when the existing syntax meets a high bar for being harmful. Again, the overarching theme of my response is that I don't think the proposed "stricter rules" offer much more harm mitigation than significantly less source-breaking designs for pattern matching, and I don't see anything in the proposal text that discusses the issue or justifies the particular design over less source-breaking alternatives.

OTOH, the proposal does outline a major use case for a local variable name that does not match the argument label: `param` vs `parameter`. Widely-respected style guides in various languages encourage unabbreviated and descriptive API names but much more concise local variable names. This is a legitimate and good practice being actively discouraged by the sugared rules.

This not a counterpoint, but I personally think using shortened names is not something to be encouraged. A (admittedly quirky) practice some of us inherited from the Cocoa style guideline is to use real, complete words for variable names. I’d like to think that The Swift API Design Guidelines are aligned in spirit on this matter - “clarity is more important than brevity”. (incidentally, the guidelines’s code samples don’t contain partial-word variables anywhere).

We're talking _local_ variables: local variables aren't API. There are many, many examples of single-letter variables in the design guidelines. For example, `x = y.union(z)` has three of them.

This would be merely annoying and not harmful if we could guarantee that it only means the API user will have to use longer local names, but the natural impulse on the part of thoughtful API authors would be to limit the expressiveness of their labels to help out their users.

This puts API authors in an impossible bind: they need to choose labels that are not too short lest it collide frequently with existing local variable names (`x` and `y` would be suboptimal, for example, but there are good reasons why an associated value might have arguments labeled `x` and `y`),

API authors are already in this impossible bind: whenever they export a type name, a method signature in an open class or a protocol, risk of collision come up.

Again, local variables aren't API. API authors have never been in this bind with respect to local variables. Nothing in the language has ever caused API to restrict the consumer's choice of local variable names. I think this is a highly, highly unusual rule.

Local variable being the same as argument label, which is API, correct? I’m saying user’s local variables and types can collide with symbols from APIs. To illustrate, imagine implementing a protocol (yes, as simple as that):

protocol A {
    var answer: Int { get }
    func ask(_ question: String)
}

if blah() {
    let answer = 0
    let question = "huh?"

    class B: A {
        let answer = 42
        func ask(_ question: String) {
            // what's question and answer here?
      // what if you want to define a new type here with the name “A”?
        }
    }
}

`A` forced user to shadow their local variable (a collision!), so it’s wise for the user to pick some other variable name here. Why does seem so trivial and natural? Because it’s how API works: someone defines some symbol, you take them into your local scope and use them. The pattern matching rule proposed here is no different.

When a local variable does collide with a payload label, it would be bad if the user accidentally used the variable _in stead of_ the actual payload value. Forcing users to proactively rebind the variable would make them more mindful for this type of mistake.

What mistake do you have in mind? Currently, labels have nothing to do with variable names. How does a user accidentally use a label name instead of a variable name?

Looking at definition of an enum, user sees something like

enum SomeEnum {
    case aCase(veryMundaneName: Type) // substitute “veryMundaneName” with a common label, like “value” or “account"
}

What’s the value of `veryMundaneName` in a pattern matched black for `aCase`? The answer in Swift 3 is: no one knows! User may use this variable expecting it’s bind to the associated value because it’s natural given the context, and later find out that they’ve been using a variable from outside because the associated value is bond to something completely unrelated.

Example:

switch enumValue {
case aCase….:
  // many lines of code later…
  doThings(with: veryMundaneName) // bug!
}

Turns out, the bug is due to

let veryMundaneName: AType = getAMundaneValue()
// many lines of code later
switch enumValue {
case aCase(let randomLabelFreedomYay):
  // many lines of code later
  doThings(with: veryMundaneName) // bug!
}

This mistake seems silly, and is still a problem in the case of rebinding. But we can make it happen less.

but they also need to choose labels that are not too verbose. The safest bet in this case would be not to label at all, but then they lose the communicative aspect of argument labels (see point 2 below).

A more realistic version of the story: API author choose labels that make the most sense for the declaration and user accept the risk of collision as they use the API. Most of those who choose to skip labels would not have given this much thought about their effect at all.

2.
In the "update and commentary" revising SE-0111, it was acknowledged that "cosmetic" labels have a significant use case. Thus, the rules were changed to allow `(_ foo: Int, _ bar: Int) -> ()` to communicate to the reader of code that the first argument serves some purpose "foo" without forcing that name to be part of the API, pending further revisions.

Because enum cases are currently tuples, labels can be dropped freely, and therefore these labels are effectively "optional" parts of the API that can be seen by the user but, at their discretion, not used. That fulfills the use case of "cosmetic" labels. In this revised proposal, by requiring the argument label to be actually _written_ somewhere by the API user, it puts a dent into the legitimate use case of "cosmetic" labels.

That is to say, an API author who wishes to communicate something about a parameter by using a label must now also consider if that label is also appropriate as a variable name and must forgo its use if the label is not so appropriate. This is a very different decision-making process and it is being applied retroactively to previously designed APIs whose labels would have been (hopefully thoughtfully) chosen under very different circumstances.

This is something we never agreed on: SE-0111 is about functions. In some languages, patterns does resemble constructor functions, but that’s as much similarity as one can get anywhere. I still think applying every decision we made about functions to pattern matching is weird.

I have to admit, I still don't understand your reticence. The first part of your proposal aligns enum cases with functions. If we are to look for patterns in something that is spelled like a function, then it is natural for the pattern itself to be spelled like a function, no? Currently, in Swift 3, since we're trying to use pattern matching for a tuple, the pattern is spelled like a tuple. In my simplistic mind, if we're trying to use pattern matching for a $foo, the pattern should be spelled like a $foo. Far from being weird, to me that is the only possible intuitive syntax.

But here’s my analysis anyways: the “cosmetic label” comment is about paving a way to restore expressivity of closures. It talks about the *interaction* between a function/closure’s declaration and use site — if parameter names are provided in a closure’s declaration, they should be required at invocation, similar to pre-SE-0111. IMO this proposal makes enum case and patterns closer to this goal.

I agree that your proposal does indeed get us closer to SE-0111. By requiring argument labels chosen by the API author to be written out by the user, we get closer to the goals of SE-0111. But SE-0111 also had a large drawback that required post-approval modification, which was that there ended up being no way to write "cosmetic labels," which both the community and core team agreed was an important use case.

With functions, that role can be filled with internal parameter names. This is what the "update and commentary" restored to SE-0111. With tuples, that role is filled by the labels themselves, because they can be ergonomically erased. With enum cases, you have not provided a parallel facility for cosmetic labels, because in your proposal labels can no longer be easily erased, but nor are there internal parameter names or some other substitute. I'm saying that we should learn from the problems discovered after SE-0111 was approved and fix that shortcoming for enum cases before this proposal is adopted.

(Link to what we are talking about for the benefit of those reading along: https://lists.swift.org/pipermail/swift-evolution-announce/2016-July/000233.html)

The key distinction we need to decide here is whether case labels are “cosmetic”. We don’t allow declaration of separate parameter name and internal name for associated values. I interpret that as we are enforcing the syntax sugar in function declaration where user can use one symbol to represent both:

func f(x: Int) // is the same as func f(x x: Int)

It’s tempting to treat matching an enum value against a pattern as assigning a function value to a variable. If that’s what we are doing, it makes perfect sense to say we get “ultimate glory” here with patterns. Meaning, as you suggested, we consider the case labels “cosmetic”. It’s really just tho parameter name in a function (the first of the two “x” in code comment above.

But that’s kind of a stretch isn’t it? An enum value is very different compared to a function value. Yes, there happen to be a function that constructs this enum value that’s declared when user declare a case, that function gets as much resemblance as any other. But the enum value it self deserves more consideration. Telling the user “do these things that you do with a function value” just makes pattern matching harder to explain, because we are *not* assigning nor invoking function values.

That’s not to say we need totally distinct syntax. Deconstructing a value should visually relate to constructing it. So here’s how I think these two relate: a constructor is a function. Function signature has these arguments that the function refers to in its body. Pattern matching is the starting point of deconstructing a value. The scope created following it is the equivalent of a “body”, in which the associated values are used as “arguments”. Therefore it make sense to say that these labels are more like internal names (the 2nd “x” in the comment of the above sample).

3.
The first part of the proposal aligns enum case syntax with functions. Functions often taken prepositions as argument labels, and indeed previous SE proposals have extended the rules to allow most words. However, `case foo(index: Int, in: T)` would have a disastrous label, as `in` would be a very annoying variable name whose use would be actively encouraged by the proposed sugared pattern matching rules.

The proposed rules for the sugared pattern would also require (well, greatly encourage) unique labels for each argument. This again is inconsistent with the naming conventions encouraged by the first part of the proposal aligning enum case syntax with functions, which have no such restrictions. If a user names something `case foo(point: T, point: T)`, then the matching rules would actively encourage an invalid redefinition of a variable named `point`.

(On the other hand, the API author does not have the luxury of naming the same case `foo(from point: T, to point: T)`, and even if they did, prepositions can make lousy local variable names--see first paragraph.)

I don’t see this as a problem for enum case authors. It just means the poor pattern writer needs to provide the positional information to disambiguate.

What do you mean by "positional information" here?

4.
The proposal does not explore what happens when the proposed prohibition on "mixing and matching" the proposed sugared and unsugared pattern matching runs up against associated values that have a mix of labeled and unlabeled parameters, and pattern matching user cases where the user does not wish to bind all of the arguments.

Given `case foo(a: Int, String, b: Int, String)`, the only sensible interpretation of the rules for sugared syntax would allow the user to choose any name for some but not all of the labels. If the user wishes to bind only `b`, however, he or she will need to navigate a puzzling set of rules that are not spelled out in the proposal:

case foo(a: _, _, b: let b, _)
// this is definitely allowed

case foo(a: _, _, b: let myVar, _)
// this is also definitely allowed

// but...
case foo(_, _, b: let myVar, _)
// is this allowed, or must the user explicitly state and not bind `a`?

// ...and with respect to the sugared version...
case foo(_, _, let b, _)
// is this allowed, or must the user explicitly state and not bind `a`?

Good point. To make up for this: `_` can substitute any sub pattern, which is something that this proposal doesn’t change but definitely worth spelling out.

5.
In the "update and commentary" revising SE-0111, the core team outlined a preferred path to restoring the full use of argument labels for functions without giving them type system significance. They gave a non-sugared form and a sugared form, both of which have met with approval from the community.

Briefly, the non-sugared form allows compound names to be used in variable names: `func foo(opToUse op(lhs:rhs:) : (Int, Int) -> Int)`. The first part of this proposal is consistent in that it removes the type system significance of argument labels from the associated values of enum cases, and considers them as part of the enum case name. It also stands to reason that, if a user were to match a case _without_ trying to bind any variables, the same syntax would have be used if the base name is ambiguous: `case elet(locals:body:): break`.

However, the proposal makes no provision for using that same compound name in pattern matching. There appears to be no particular reason for its isolated omission here, as `case elet(locals:body:)(let a, let b): return a * b` is readable and presents no syntactic difficulties. (Moreover, it is consistent with the syntax permitted in this proposal for initializing a variable: `let foo = Expr.elet(locals:body:)([], anExpr)`.)

Another good point. We can handle this in the purely additional proposal for compound variable names. I consider this not the 5th item in the list, but a separate suggestion, however :stuck_out_tongue:

---

In light of these shortcomings, I would argue that the following alternative scheme is the most intuitive and consistent for pattern matching given the general agreement that enum case representation should be "normalized":

Given:

enum S {
  case foo(bar: Int, baz: Int)
  case foo(boo: String)
  case bar(boo: String)
}

a. As in functions after SE-0111, enum cases can be identified unambiguously, regardless of whether one is initializing a variable or matching a case, by their compound name, e.g. `bar(boo:)`. Where a case can be unambiguously identified with only the base name, that is an alternative spelling, e.g. `bar`. Where a case cannot be identified uniquely with the base name, then it is an error to try to use the base name alone: `case foo: break // error: unambiguous`.

b. As in functions after SE-0111, arguments can be passed in either a sugared form or an unsugared form, and they can be bound in a pattern matching statement in the same way. That is, `case foo(bar: let a, baz: let b): break` and `case foo(bar:baz:)(let a, let b): break` are equivalent.

c. As in functions, one cannot supply different or incorrect argument labels. That is, `case foo(baz: let a, bar: let b)` and `case foo(baz:bar:)(let a, let b)` are both forbidden. _This recovers the vast majority of the additional syntactic safety that is outlined in the revised proposal, but without the use of any special rules for pattern matching._

d. By composing rules (a) and (b), `case bar(let a)` is allowed as it is today, preserving source compatibility. However `case foo(let b, let c)` is not allowed, and _not_ because different local variable names are chosen, but because the enum has two cases named foo.

From a user’s point of view, there’s enough positional information in this pattern for the compiler to figure out which case it should match. This would be very unintuitive IMO.

Wait, the key point of your proposal, with its "stricter rules," is that labels shouldn't be optional even with sufficient positional information! That's also the whole thing above about getting us closer to aligning with SE-0111, etc.

Fair enough. The argument I invoked leads us to a dark path :stuck_out_tongue:

···

On Apr 1, 2017, at 2:54 PM, Xiaodi Wu via swift-evolution <swift-evolution@swift.org> wrote:
On Sat, Apr 1, 2017 at 3:38 PM, Daniel Duan <daniel@duan.org <mailto:daniel@duan.org>> wrote:

On Apr 1, 2017, at 9:50 AM, Xiaodi Wu via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Daniel Duan) #8

That seems like straight up ambiguity with or without restriction on the label though? This kind of usability problem should and is discouraged by the proposed solution.

···

On Apr 2, 2017, at 11:10 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Sat, Apr 1, 2017 at 3:38 PM, Daniel Duan <daniel@duan.org <mailto:daniel@duan.org>> wrote:

<snip>

4.
The proposal does not explore what happens when the proposed prohibition on "mixing and matching" the proposed sugared and unsugared pattern matching runs up against associated values that have a mix of labeled and unlabeled parameters, and pattern matching user cases where the user does not wish to bind all of the arguments.

Given `case foo(a: Int, String, b: Int, String)`, the only sensible interpretation of the rules for sugared syntax would allow the user to choose any name for some but not all of the labels. If the user wishes to bind only `b`, however, he or she will need to navigate a puzzling set of rules that are not spelled out in the proposal:

case foo(a: _, _, b: let b, _)
// this is definitely allowed

case foo(a: _, _, b: let myVar, _)
// this is also definitely allowed

// but...
case foo(_, _, b: let myVar, _)
// is this allowed, or must the user explicitly state and not bind `a`?

// ...and with respect to the sugared version...
case foo(_, _, let b, _)
// is this allowed, or must the user explicitly state and not bind `a`?

Good point. To make up for this: `_` can substitute any sub pattern, which is something that this proposal doesn’t change but definitely worth spelling out.

This rule cannot hold. You cannot have the shorthand syntax you propose, disallow mixing of shorthand syntax and the longer form, *and* allow `_` to substitute for any pattern without a label.

enum E {
  case foo(a: Int, b: Int, c: Int)
  case foo(a: String, c: String, e: String)
}

let e = /* instance of E */

switch e {
case foo(let a, _, _):
  // what type is `a` here?
  break
default:
  fatalError()
}

(Brent Royal-Gordon) #9

Let's make it a little more reasonable:

  enum Color {
    case color(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat)
    case color(hue: CGFloat, saturation: CGFloat, value: CGFloat, alpha: CGFloat)
  }

  let c: Color = …
  
  switch c {
  case .color(_, _, _, let alpha):
    …
  }

Interestingly, in this example it would probably be appropriate to match both cases. I wonder if that's true in the general case.

···

On Apr 2, 2017, at 11:17 PM, Daniel Duan via swift-evolution <swift-evolution@swift.org> wrote:

On Apr 2, 2017, at 11:10 PM, Xiaodi Wu <xiaodi.wu@gmail.com <mailto:xiaodi.wu@gmail.com>> wrote:

This rule cannot hold. You cannot have the shorthand syntax you propose, disallow mixing of shorthand syntax and the longer form, *and* allow `_` to substitute for any pattern without a label.

enum E {
  case foo(a: Int, b: Int, c: Int)
  case foo(a: String, c: String, e: String)
}

let e = /* instance of E */

switch e {
case foo(let a, _, _):
  // what type is `a` here?
  break
default:
  fatalError()
}

That seems like straight up ambiguity with or without restriction on the label though? This kind of usability problem should and is discouraged by the proposed solution.

--
Brent Royal-Gordon
Architechies


(Daniel Duan) #10

I prefer patterns and matching cases have a many-to-one relation.

Keep in mind, our goal is to make match site more readable/teachable. I’d venture to say that given the definition of “Color”, this pattern is really unreadable. So is this kind of attempt to disambiguate, if we allow mixture of long/short forms:

case .color(hue: _, _, let alpha): // yuck

···

On Apr 2, 2017, at 11:46 PM, Brent Royal-Gordon <brent@architechies.com> wrote:

On Apr 2, 2017, at 11:17 PM, Daniel Duan via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Apr 2, 2017, at 11:10 PM, Xiaodi Wu <xiaodi.wu@gmail.com <mailto:xiaodi.wu@gmail.com>> wrote:

This rule cannot hold. You cannot have the shorthand syntax you propose, disallow mixing of shorthand syntax and the longer form, *and* allow `_` to substitute for any pattern without a label.

enum E {
  case foo(a: Int, b: Int, c: Int)
  case foo(a: String, c: String, e: String)
}

let e = /* instance of E */

switch e {
case foo(let a, _, _):
  // what type is `a` here?
  break
default:
  fatalError()
}

That seems like straight up ambiguity with or without restriction on the label though? This kind of usability problem should and is discouraged by the proposed solution.

Let's make it a little more reasonable:

  enum Color {
    case color(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat)
    case color(hue: CGFloat, saturation: CGFloat, value: CGFloat, alpha: CGFloat)
  }

  let c: Color = …
  
  switch c {
  case .color(_, _, _, let alpha):
    …
  }

Interestingly, in this example it would probably be appropriate to match both cases. I wonder if that's true in the general case.

--
Brent Royal-Gordon
Architechies


(Xiaodi Wu) #11

Thanks again for a detailed review. I have a few comments inline.

• Does this proposal fit well with the feel and direction of Swift?

The "Pattern consistency" section does not align well with the feel and
direction of Swift. Specifically, it does not explore some of the
difficulties that arise from the proposed rules, adopts some of the same
shortcomings that required revision for SE-0111, and deviates from some of
the anticipated fixes for those shortcomings outlined in the core team's
"update and commentary" to SE-0111.

It is not the case that the design proposed is "a consequence of no
longer relying on tuple patterns," in that it is not the inevitable result
that falls out of that decision.

The text in this revision may be poorly phrased. The connection, as I
pointed out in an previous thread, is that we need to define syntax for
enum pattern matching because the one we’ve been using in Swift 3 is tuple
pattern’s syntax, which is now distinct and separate.

What I'm saying here is that, although _some_ change becomes necessary,
the particular changes proposed here are not themselves "a consequence of
no longer relying on tuple patterns."

Put another way, given `enum E { case foo(bar: Int, baz: Int) }`, not
being allowed to write `switch e { case foo(let a, let b): break }` is
*not* an inevitable consequence of moving away from tuple patterns. Since
the particular proposed changes break more existing source code than is
strictly necessary for moving away from tuple-based pattern matching, those
choices require stringent justification.

I will detail the alternative design that requires the fewest deviations

or special rules, and breaks the least code extant today, later on. First,
the shortcomings:

1.
The proposed rules for pattern matching are a source-breaking change, and
are *not* the most minimal such change given the abandoning of tuples (see
alternative below). However, the proposal does not engage with the core
team's Swift 4 criteria for source-breaking changes with respect to the
proposed "stricter rules" for pattern matching. There is no text at all
about why specifically having the compiler encourage local _variable_ names
to match argument labels resolves an active harm that outweighs the goal of
preserving the greatest possible source compatibility.

With this proposal, user can still use local variable names. It is true
that if there are many ways to achieve the same thing, the compiler would
be encouraging user to do that thing. But that puts a cost on the compiler,
new users and experienced readers in unfamiliar codebases. This is (albeit
not to a satisfactory degree, it seems) pointed out in the motivation
section.

As for source compatibility, Swift 3 code should continue to work with
warnings. Swift 4 mode would issue errors along with fix-its, which the
migrator can leverage. Depends on core team/community’s implementor
resource, there’s even a chance that this change would roll out one version
later (warning in 4.X, error in 5.Y). In theory, the migration hurdle can
be minimized.

Many syntactic changes can be migrated in this way, but for Swift 4, that
would only be justified when the existing syntax meets a high bar for being
harmful. Again, the overarching theme of my response is that I don't think
the proposed "stricter rules" offer much more harm mitigation than
significantly less source-breaking designs for pattern matching, and I
don't see anything in the proposal text that discusses the issue or
justifies the particular design over less source-breaking alternatives.

OTOH, the proposal does outline a major use case for a local variable
name that does not match the argument label: `param` vs `parameter`.
Widely-respected style guides in various languages encourage unabbreviated
and descriptive API names but much more concise local variable names. This
is a legitimate and good practice being actively discouraged by the sugared
rules.

This not a counterpoint, but I personally think using shortened names is
not something to be encouraged. A (admittedly quirky) practice some of us
inherited from the Cocoa style guideline is to use real, complete words for
variable names. I’d like to think that The Swift API Design Guidelines are
aligned in spirit on this matter - “clarity is more important than
brevity”. (incidentally, the guidelines’s code samples don’t contain
partial-word variables anywhere).

We're talking _local_ variables: local variables aren't API. There are
many, many examples of single-letter variables in the design guidelines.
For example, `x = y.union(z)` has three of them.

This would be merely annoying and not harmful if we could guarantee that
it only means the API user will have to use longer local names, but the
natural impulse on the part of thoughtful API authors would be to limit the
expressiveness of their labels to help out their users.

This puts API authors in an impossible bind: they need to choose labels
that are not too short lest it collide frequently with existing local
variable names (`x` and `y` would be suboptimal, for example, but there are
good reasons why an associated value might have arguments labeled `x` and
`y`),

API authors are already in this impossible bind: whenever they export a
type name, a method signature in an open class or a protocol, risk of
collision come up.

Again, local variables aren't API. API authors have never been in this
bind with respect to local variables. Nothing in the language has ever
caused API to restrict the consumer's choice of local variable names. I
think this is a highly, highly unusual rule.

Local variable being the same as argument label, which is API, correct?
I’m saying user’s local variables and types can collide with symbols from
APIs. To illustrate, imagine implementing a protocol (yes, as simple as
that):

protocol A {
    var answer: Int { get }
    func ask(_ question: String)
}

if blah() {
    let answer = 0
    let question = "huh?"

    class B: A {
        let answer = 42
        func ask(_ question: String) {
            // what's question and answer here?
    // what if you want to define a new type here with the name “A”?
        }
    }
}

`A` forced user to shadow their local variable (a collision!), so it’s
wise for the user to pick some other variable name here. Why does seem so
trivial and natural? Because it’s how API works: someone defines some
symbol, you take them into your local scope and use them. The pattern
matching rule proposed here is no different.

When a local variable does collide with a payload label, it would be bad

if the user accidentally used the variable _in stead of_ the actual payload
value. Forcing users to proactively rebind the variable would make them
more mindful for this type of mistake.

What mistake do you have in mind? Currently, labels have nothing to do
with variable names. How does a user accidentally use a label name instead
of a variable name?

Looking at definition of an enum, user sees something like

enum SomeEnum {
    case aCase(veryMundaneName: Type) // substitute “veryMundaneName” with
a common label, like “value” or “account"
}

What’s the value of `veryMundaneName` in a pattern matched black for
`aCase`? The answer in Swift 3 is: no one knows! User may use this variable
expecting it’s bind to the associated value because it’s natural given the
context, and later find out that they’ve been using a variable from outside
because the associated value is bond to something completely unrelated.

Example:

switch enumValue {
case aCase….:
  // many lines of code later…
  doThings(with: veryMundaneName) // bug!
}

Turns out, the bug is due to

let veryMundaneName: AType = getAMundaneValue()
// many lines of code later
switch enumValue {
case aCase(let randomLabelFreedomYay):
  // many lines of code later
  doThings(with: veryMundaneName) // bug!
}

This mistake seems silly, and is still a problem in the case of rebinding.
But we can make it happen less.

I am not convinced this is an illustration of a bug related to enum cases
in any real sense. You are invoking a function with one variable when you
meant to invoke it with another. This can happen with any two variables in
any scenario. I see no evidence that argument labels are any more prone to
be confused for variable names than are case names, function names, or any
other name. It is that Swift is strongly typed that makes confusion happen
less, given that `veryMundaneName` is of type `AType` and
`randomLabelFreedomYay` is of type `Type`.

but they also need to choose labels that are not too verbose. The safest
bet in this case would be not to label at all, but then they lose the
communicative aspect of argument labels (see point 2 below).

A more realistic version of the story: API author choose labels that make
the most sense for the declaration and user accept the risk of collision as
they use the API. Most of those who choose to skip labels would not have
given this much thought about their effect at all.

2.
In the "update and commentary" revising SE-0111, it was acknowledged that
"cosmetic" labels have a significant use case. Thus, the rules were changed
to allow `(_ foo: Int, _ bar: Int) -> ()` to communicate to the reader of
code that the first argument serves some purpose "foo" without forcing that
name to be part of the API, pending further revisions.

Because enum cases are currently tuples, labels can be dropped freely,
and therefore these labels are effectively "optional" parts of the API that
can be seen by the user but, at their discretion, not used. That fulfills
the use case of "cosmetic" labels. In this revised proposal, by requiring
the argument label to be actually _written_ somewhere by the API user, it
puts a dent into the legitimate use case of "cosmetic" labels.

That is to say, an API author who wishes to communicate something about a
parameter by using a label must now also consider if that label is also
appropriate as a variable name and must forgo its use if the label is not
so appropriate. This is a very different decision-making process and it is
being applied retroactively to previously designed APIs whose labels would
have been (hopefully thoughtfully) chosen under very different
circumstances.

This is something we never agreed on: SE-0111 is about functions. In some
languages, patterns does resemble constructor functions, but that’s as much
similarity as one can get anywhere. I still think applying every decision
we made about functions to pattern matching is weird.

I have to admit, I still don't understand your reticence. The first part
of your proposal aligns enum cases with functions. If we are to look for
patterns in something that is spelled like a function, then it is natural
for the pattern itself to be spelled like a function, no? Currently, in
Swift 3, since we're trying to use pattern matching for a tuple, the
pattern is spelled like a tuple. In my simplistic mind, if we're trying to
use pattern matching for a $foo, the pattern should be spelled like a $foo.
Far from being weird, to me that is the only possible intuitive syntax.

But here’s my analysis anyways: the “cosmetic label” comment is about

paving a way to restore expressivity of closures. It talks about the
*interaction* between a function/closure’s declaration and use site — if
parameter names are provided in a closure’s declaration, they should be
required at invocation, similar to pre-SE-0111. IMO this proposal makes
enum case and patterns closer to this goal.

I agree that your proposal does indeed get us closer to SE-0111. By
requiring argument labels chosen by the API author to be written out by the
user, we get closer to the goals of SE-0111. But SE-0111 also had a large
drawback that required post-approval modification, which was that there
ended up being no way to write "cosmetic labels," which both the community
and core team agreed was an important use case.

With functions, that role can be filled with internal parameter names.
This is what the "update and commentary" restored to SE-0111. With tuples,
that role is filled by the labels themselves, because they can be
ergonomically erased. With enum cases, you have not provided a parallel
facility for cosmetic labels, because in your proposal labels can no longer
be easily erased, but nor are there internal parameter names or some other
substitute. I'm saying that we should learn from the problems discovered
after SE-0111 was approved and fix that shortcoming for enum cases before
this proposal is adopted.

(Link to what we are talking about for the benefit of those reading along:
https://lists.swift.org/pipermail/swift-evolution-
announce/2016-July/000233.html)

The key distinction we need to decide here is whether case labels are
“cosmetic”. We don’t allow declaration of separate parameter name and
internal name for associated values. I interpret that as we are enforcing
the syntax sugar in function declaration where user can use one symbol to
represent both:

func f(x: Int) // is the same as func f(x x: Int)

It’s tempting to treat matching an enum value against a pattern as
assigning a function value to a variable.

Sorry, I am not sure I understand this sentence.

If that’s what we are doing, it makes perfect sense to say we get
“ultimate glory” here with patterns. Meaning, as you suggested, we consider
the case labels “cosmetic”. It’s really just tho parameter name in a
function (the first of the two “x” in code comment above.

But that’s kind of a stretch isn’t it? An enum value is very different
compared to a function value. Yes, there happen to be a function that
constructs this enum value that’s declared when user declare a case, that
function gets as much resemblance as any other. But the enum value it self
deserves more consideration. Telling the user “do these things that you do
with a function value” just makes pattern matching harder to explain,
because we are *not* assigning nor invoking function values.

Ah, I see. You think of the associated value as something distinct from the
declaration used to initialize it. However, there is no spelling for an
associated value other than what is used to initialize it. Given `case
foo(bar: Int, baz: Int, boo: Int)`, previously, the full name of the case
was `foo` and the associated value was `(bar: Int, baz: Int, boo: Int)`.
Your proposal causes the full name of the case instead to be
`foo(bar:baz:boo:)` and the associated value to be `(Int, Int, Int)`. Is
that not your understanding of it?

Pattern matching is just a matter of (a) indicating what case you want to
identify with the pattern; and (b) what parts of the associated value you
wish to match or to bind to variables. Part (a) is done by writing the
name, either the base name or in full (i.e. either `foo` or
`foo(bar:baz:boo:)`). Part (b) is done by writing `let myVariableName` in
the intended positions.

That’s not to say we need totally distinct syntax. Deconstructing a value

···

On Sun, Apr 2, 2017 at 1:03 AM, Daniel Duan <daniel@duan.org> wrote:

On Apr 1, 2017, at 2:54 PM, Xiaodi Wu via swift-evolution < > swift-evolution@swift.org> wrote:
On Sat, Apr 1, 2017 at 3:38 PM, Daniel Duan <daniel@duan.org> wrote:

On Apr 1, 2017, at 9:50 AM, Xiaodi Wu via swift-evolution < >> swift-evolution@swift.org> wrote:

should visually relate to constructing it. So here’s how I think these two
relate: a constructor is a function. Function signature has these arguments
that the function refers to in its body. Pattern matching is the starting
point of deconstructing a value. The scope created following it is the
equivalent of a “body”, in which the associated values are used as
“arguments”. Therefore it make sense to say that these labels are more like
internal names (the 2nd “x” in the comment of the above sample).

3.

The first part of the proposal aligns enum case syntax with functions.
Functions often taken prepositions as argument labels, and indeed previous
SE proposals have extended the rules to allow most words. However, `case
foo(index: Int, in: T)` would have a disastrous label, as `in` would be a
very annoying variable name whose use would be actively encouraged by the
proposed sugared pattern matching rules.

The proposed rules for the sugared pattern would also require (well,
greatly encourage) unique labels for each argument. This again is
inconsistent with the naming conventions encouraged by the first part of
the proposal aligning enum case syntax with functions, which have no such
restrictions. If a user names something `case foo(point: T, point: T)`,
then the matching rules would actively encourage an invalid redefinition of
a variable named `point`.

(On the other hand, the API author does not have the luxury of naming the
same case `foo(from point: T, to point: T)`, and even if they did,
prepositions can make lousy local variable names--see first paragraph.)

I don’t see this as a problem for enum case authors. It just means the
poor pattern writer needs to provide the positional information to
disambiguate.

What do you mean by "positional information" here?

4.

The proposal does not explore what happens when the proposed prohibition
on "mixing and matching" the proposed sugared and unsugared pattern
matching runs up against associated values that have a mix of labeled and
unlabeled parameters, and pattern matching user cases where the user does
not wish to bind all of the arguments.

Given `case foo(a: Int, String, b: Int, String)`, the only sensible
interpretation of the rules for sugared syntax would allow the user to
choose any name for some but not all of the labels. If the user wishes to
bind only `b`, however, he or she will need to navigate a puzzling set of
rules that are not spelled out in the proposal:

case foo(a: _, _, b: let b, _)
// this is definitely allowed

case foo(a: _, _, b: let myVar, _)
// this is also definitely allowed

// but...
case foo(_, _, b: let myVar, _)
// is this allowed, or must the user explicitly state and not bind `a`?

// ...and with respect to the sugared version...
case foo(_, _, let b, _)
// is this allowed, or must the user explicitly state and not bind `a`?

Good point. To make up for this: `_` can substitute any sub pattern,
which is something that this proposal doesn’t change but definitely worth
spelling out.

5.
In the "update and commentary" revising SE-0111, the core team outlined a
preferred path to restoring the full use of argument labels for functions
without giving them type system significance. They gave a non-sugared form
and a sugared form, both of which have met with approval from the community.

Briefly, the non-sugared form allows compound names to be used in
variable names: `func foo(opToUse op(lhs:rhs:) : (Int, Int) -> Int)`. The
first part of this proposal is consistent in that it removes the type
system significance of argument labels from the associated values of enum
cases, and considers them as part of the enum case name. It also stands to
reason that, if a user were to match a case _without_ trying to bind any
variables, the same syntax would have be used if the base name is
ambiguous: `case elet(locals:body:): break`.

However, the proposal makes no provision for using that same compound
name in pattern matching. There appears to be no particular reason for its
isolated omission here, as `case elet(locals:body:)(let a, let b): return a
* b` is readable and presents no syntactic difficulties. (Moreover, it is
consistent with the syntax permitted in this proposal for initializing a
variable: `let foo = Expr.elet(locals:body:)([], anExpr)`.)

Another good point. We can handle this in the purely additional proposal
for compound variable names. I consider this not the 5th item in the list,
but a separate suggestion, however :stuck_out_tongue:

---

In light of these shortcomings, I would argue that the following
alternative scheme is the most intuitive and consistent for pattern
matching given the general agreement that enum case representation should
be "normalized":

Given:

enum S {
  case foo(bar: Int, baz: Int)
  case foo(boo: String)
  case bar(boo: String)
}

a. As in functions after SE-0111, enum cases can be identified
unambiguously, regardless of whether one is initializing a variable or
matching a case, by their compound name, e.g. `bar(boo:)`. Where a case can
be unambiguously identified with only the base name, that is an alternative
spelling, e.g. `bar`. Where a case cannot be identified uniquely with the
base name, then it is an error to try to use the base name alone: `case
foo: break // error: unambiguous`.

b. As in functions after SE-0111, arguments can be passed in either a
sugared form or an unsugared form, and they can be bound in a pattern
matching statement in the same way. That is, `case foo(bar: let a, baz: let
b): break` and `case foo(bar:baz:)(let a, let b): break` are equivalent.

c. As in functions, one cannot supply different or incorrect argument
labels. That is, `case foo(baz: let a, bar: let b)` and `case
foo(baz:bar:)(let a, let b)` are both forbidden. _This recovers the vast
majority of the additional syntactic safety that is outlined in the revised
proposal, but without the use of any special rules for pattern matching._

d. By composing rules (a) and (b), `case bar(let a)` is allowed as it is
today, preserving source compatibility. However `case foo(let b, let c)` is
not allowed, and _not_ because different local variable names are chosen,
but because the enum has two cases named foo.

From a user’s point of view, there’s enough positional information in
this pattern for the compiler to figure out which case it should match.
This would be very unintuitive IMO.

Wait, the key point of your proposal, with its "stricter rules," is that
labels shouldn't be optional even with sufficient positional information!
That's also the whole thing above about getting us closer to aligning with
SE-0111, etc.

Fair enough. The argument I invoked leads us to a dark path :stuck_out_tongue:

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Daniel Duan) #12

Daniel Duan

Thanks again for a detailed review. I have a few comments inline.

  • Does this proposal fit well with the feel and direction of Swift?

The "Pattern consistency" section does not align well with the feel and direction of Swift. Specifically, it does not explore some of the difficulties that arise from the proposed rules, adopts some of the same shortcomings that required revision for SE-0111, and deviates from some of the anticipated fixes for those shortcomings outlined in the core team's "update and commentary" to SE-0111.

It is not the case that the design proposed is "a consequence of no longer relying on tuple patterns," in that it is not the inevitable result that falls out of that decision.

The text in this revision may be poorly phrased. The connection, as I pointed out in an previous thread, is that we need to define syntax for enum pattern matching because the one we’ve been using in Swift 3 is tuple pattern’s syntax, which is now distinct and separate.

What I'm saying here is that, although _some_ change becomes necessary, the particular changes proposed here are not themselves "a consequence of no longer relying on tuple patterns."

Put another way, given `enum E { case foo(bar: Int, baz: Int) }`, not being allowed to write `switch e { case foo(let a, let b): break }` is *not* an inevitable consequence of moving away from tuple patterns. Since the particular proposed changes break more existing source code than is strictly necessary for moving away from tuple-based pattern matching, those choices require stringent justification.

I will detail the alternative design that requires the fewest deviations or special rules, and breaks the least code extant today, later on. First, the shortcomings:

1.
The proposed rules for pattern matching are a source-breaking change, and are *not* the most minimal such change given the abandoning of tuples (see alternative below). However, the proposal does not engage with the core team's Swift 4 criteria for source-breaking changes with respect to the proposed "stricter rules" for pattern matching. There is no text at all about why specifically having the compiler encourage local _variable_ names to match argument labels resolves an active harm that outweighs the goal of preserving the greatest possible source compatibility.

With this proposal, user can still use local variable names. It is true that if there are many ways to achieve the same thing, the compiler would be encouraging user to do that thing. But that puts a cost on the compiler, new users and experienced readers in unfamiliar codebases. This is (albeit not to a satisfactory degree, it seems) pointed out in the motivation section.

As for source compatibility, Swift 3 code should continue to work with warnings. Swift 4 mode would issue errors along with fix-its, which the migrator can leverage. Depends on core team/community’s implementor resource, there’s even a chance that this change would roll out one version later (warning in 4.X, error in 5.Y). In theory, the migration hurdle can be minimized.

Many syntactic changes can be migrated in this way, but for Swift 4, that would only be justified when the existing syntax meets a high bar for being harmful. Again, the overarching theme of my response is that I don't think the proposed "stricter rules" offer much more harm mitigation than significantly less source-breaking designs for pattern matching, and I don't see anything in the proposal text that discusses the issue or justifies the particular design over less source-breaking alternatives.

OTOH, the proposal does outline a major use case for a local variable name that does not match the argument label: `param` vs `parameter`. Widely-respected style guides in various languages encourage unabbreviated and descriptive API names but much more concise local variable names. This is a legitimate and good practice being actively discouraged by the sugared rules.

This not a counterpoint, but I personally think using shortened names is not something to be encouraged. A (admittedly quirky) practice some of us inherited from the Cocoa style guideline is to use real, complete words for variable names. I’d like to think that The Swift API Design Guidelines are aligned in spirit on this matter - “clarity is more important than brevity”. (incidentally, the guidelines’s code samples don’t contain partial-word variables anywhere).

We're talking _local_ variables: local variables aren't API. There are many, many examples of single-letter variables in the design guidelines. For example, `x = y.union(z)` has three of them.

This would be merely annoying and not harmful if we could guarantee that it only means the API user will have to use longer local names, but the natural impulse on the part of thoughtful API authors would be to limit the expressiveness of their labels to help out their users.

This puts API authors in an impossible bind: they need to choose labels that are not too short lest it collide frequently with existing local variable names (`x` and `y` would be suboptimal, for example, but there are good reasons why an associated value might have arguments labeled `x` and `y`),

API authors are already in this impossible bind: whenever they export a type name, a method signature in an open class or a protocol, risk of collision come up.

Again, local variables aren't API. API authors have never been in this bind with respect to local variables. Nothing in the language has ever caused API to restrict the consumer's choice of local variable names. I think this is a highly, highly unusual rule.

Local variable being the same as argument label, which is API, correct? I’m saying user’s local variables and types can collide with symbols from APIs. To illustrate, imagine implementing a protocol (yes, as simple as that):

protocol A {
    var answer: Int { get }
    func ask(_ question: String)
}

if blah() {
    let answer = 0
    let question = "huh?"

    class B: A {
        let answer = 42
        func ask(_ question: String) {
            // what's question and answer here?
      // what if you want to define a new type here with the name “A”?
        }
    }
}

`A` forced user to shadow their local variable (a collision!), so it’s wise for the user to pick some other variable name here. Why does seem so trivial and natural? Because it’s how API works: someone defines some symbol, you take them into your local scope and use them. The pattern matching rule proposed here is no different.

When a local variable does collide with a payload label, it would be bad if the user accidentally used the variable _in stead of_ the actual payload value. Forcing users to proactively rebind the variable would make them more mindful for this type of mistake.

What mistake do you have in mind? Currently, labels have nothing to do with variable names. How does a user accidentally use a label name instead of a variable name?

Looking at definition of an enum, user sees something like

enum SomeEnum {
    case aCase(veryMundaneName: Type) // substitute “veryMundaneName” with a common label, like “value” or “account"
}

What’s the value of `veryMundaneName` in a pattern matched black for `aCase`? The answer in Swift 3 is: no one knows! User may use this variable expecting it’s bind to the associated value because it’s natural given the context, and later find out that they’ve been using a variable from outside because the associated value is bond to something completely unrelated.

Example:

switch enumValue {
case aCase….:
  // many lines of code later…
  doThings(with: veryMundaneName) // bug!
}

Turns out, the bug is due to

let veryMundaneName: AType = getAMundaneValue()
// many lines of code later
switch enumValue {
case aCase(let randomLabelFreedomYay):
  // many lines of code later
  doThings(with: veryMundaneName) // bug!
}

This mistake seems silly, and is still a problem in the case of rebinding. But we can make it happen less.

I am not convinced this is an illustration of a bug related to enum cases in any real sense. You are invoking a function with one variable when you meant to invoke it with another. This can happen with any two variables in any scenario. I see no evidence that argument labels are any more prone to be confused for variable names than are case names, function names, or any other name. It is that Swift is strongly typed that makes confusion happen less, given that `veryMundaneName` is of type `AType` and `randomLabelFreedomYay` is of type `Type`.

but they also need to choose labels that are not too verbose. The safest bet in this case would be not to label at all, but then they lose the communicative aspect of argument labels (see point 2 below).

A more realistic version of the story: API author choose labels that make the most sense for the declaration and user accept the risk of collision as they use the API. Most of those who choose to skip labels would not have given this much thought about their effect at all.

2.
In the "update and commentary" revising SE-0111, it was acknowledged that "cosmetic" labels have a significant use case. Thus, the rules were changed to allow `(_ foo: Int, _ bar: Int) -> ()` to communicate to the reader of code that the first argument serves some purpose "foo" without forcing that name to be part of the API, pending further revisions.

Because enum cases are currently tuples, labels can be dropped freely, and therefore these labels are effectively "optional" parts of the API that can be seen by the user but, at their discretion, not used. That fulfills the use case of "cosmetic" labels. In this revised proposal, by requiring the argument label to be actually _written_ somewhere by the API user, it puts a dent into the legitimate use case of "cosmetic" labels.

That is to say, an API author who wishes to communicate something about a parameter by using a label must now also consider if that label is also appropriate as a variable name and must forgo its use if the label is not so appropriate. This is a very different decision-making process and it is being applied retroactively to previously designed APIs whose labels would have been (hopefully thoughtfully) chosen under very different circumstances.

This is something we never agreed on: SE-0111 is about functions. In some languages, patterns does resemble constructor functions, but that’s as much similarity as one can get anywhere. I still think applying every decision we made about functions to pattern matching is weird.

I have to admit, I still don't understand your reticence. The first part of your proposal aligns enum cases with functions. If we are to look for patterns in something that is spelled like a function, then it is natural for the pattern itself to be spelled like a function, no? Currently, in Swift 3, since we're trying to use pattern matching for a tuple, the pattern is spelled like a tuple. In my simplistic mind, if we're trying to use pattern matching for a $foo, the pattern should be spelled like a $foo. Far from being weird, to me that is the only possible intuitive syntax.

But here’s my analysis anyways: the “cosmetic label” comment is about paving a way to restore expressivity of closures. It talks about the *interaction* between a function/closure’s declaration and use site — if parameter names are provided in a closure’s declaration, they should be required at invocation, similar to pre-SE-0111. IMO this proposal makes enum case and patterns closer to this goal.

I agree that your proposal does indeed get us closer to SE-0111. By requiring argument labels chosen by the API author to be written out by the user, we get closer to the goals of SE-0111. But SE-0111 also had a large drawback that required post-approval modification, which was that there ended up being no way to write "cosmetic labels," which both the community and core team agreed was an important use case.

With functions, that role can be filled with internal parameter names. This is what the "update and commentary" restored to SE-0111. With tuples, that role is filled by the labels themselves, because they can be ergonomically erased. With enum cases, you have not provided a parallel facility for cosmetic labels, because in your proposal labels can no longer be easily erased, but nor are there internal parameter names or some other substitute. I'm saying that we should learn from the problems discovered after SE-0111 was approved and fix that shortcoming for enum cases before this proposal is adopted.

(Link to what we are talking about for the benefit of those reading along: https://lists.swift.org/pipermail/swift-evolution-announce/2016-July/000233.html)

The key distinction we need to decide here is whether case labels are “cosmetic”. We don’t allow declaration of separate parameter name and internal name for associated values. I interpret that as we are enforcing the syntax sugar in function declaration where user can use one symbol to represent both:

func f(x: Int) // is the same as func f(x x: Int)

It’s tempting to treat matching an enum value against a pattern as assigning a function value to a variable.

Sorry, I am not sure I understand this sentence.

Aka viewing the case pattern as simply an compound variable assignment as envisioned in the SE-0111 commentary. This way of the labels would be "cosmetic".

If that’s what we are doing, it makes perfect sense to say we get “ultimate glory” here with patterns. Meaning, as you suggested, we consider the case labels “cosmetic”. It’s really just tho parameter name in a function (the first of the two “x” in code comment above.

But that’s kind of a stretch isn’t it? An enum value is very different compared to a function value. Yes, there happen to be a function that constructs this enum value that’s declared when user declare a case, that function gets as much resemblance as any other. But the enum value it self deserves more consideration. Telling the user “do these things that you do with a function value” just makes pattern matching harder to explain, because we are *not* assigning nor invoking function values.

Ah, I see. You think of the associated value as something distinct from the declaration used to initialize it. However, there is no spelling for an associated value other than what is used to initialize it. Given `case foo(bar: Int, baz: Int, boo: Int)`, previously, the full name of the case was `foo` and the associated value was `(bar: Int, baz: Int, boo: Int)`. Your proposal causes the full name of the case instead to be `foo(bar:baz:boo:)` and the associated value to be `(Int, Int, Int)`. Is that not your understanding of it?

Yes

Pattern matching is just a matter of (a) indicating what case you want to identify with the pattern; and (b) what parts of the associated value you wish to match or to bind to variables. Part (a) is done by writing the name, either the base name or in full (i.e. either `foo` or `foo(bar:baz:boo:)`). Part (b) is done by writing `let myVariableName` in the intended positions.

What I left out is that the internal/parameter names of a function are non-optional part of its signature (one must use exact parameter names to implement a method in a protocol, for example). I prefer treating labels in case pattern matching the same way we treat parameter names in protocol method implementation (due to the symmetry between constructing/deconstructing body mentioned in my previous comments).

···

Sent from my iPhone

On Apr 1, 2017, at 11:49 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Sun, Apr 2, 2017 at 1:03 AM, Daniel Duan <daniel@duan.org> wrote:

On Apr 1, 2017, at 2:54 PM, Xiaodi Wu via swift-evolution <swift-evolution@swift.org> wrote:

On Sat, Apr 1, 2017 at 3:38 PM, Daniel Duan <daniel@duan.org> wrote:

On Apr 1, 2017, at 9:50 AM, Xiaodi Wu via swift-evolution <swift-evolution@swift.org> wrote:

That’s not to say we need totally distinct syntax. Deconstructing a value should visually relate to constructing it. So here’s how I think these two relate: a constructor is a function. Function signature has these arguments that the function refers to in its body. Pattern matching is the starting point of deconstructing a value. The scope created following it is the equivalent of a “body”, in which the associated values are used as “arguments”. Therefore it make sense to say that these labels are more like internal names (the 2nd “x” in the comment of the above sample).

3.
The first part of the proposal aligns enum case syntax with functions. Functions often taken prepositions as argument labels, and indeed previous SE proposals have extended the rules to allow most words. However, `case foo(index: Int, in: T)` would have a disastrous label, as `in` would be a very annoying variable name whose use would be actively encouraged by the proposed sugared pattern matching rules.

The proposed rules for the sugared pattern would also require (well, greatly encourage) unique labels for each argument. This again is inconsistent with the naming conventions encouraged by the first part of the proposal aligning enum case syntax with functions, which have no such restrictions. If a user names something `case foo(point: T, point: T)`, then the matching rules would actively encourage an invalid redefinition of a variable named `point`.

(On the other hand, the API author does not have the luxury of naming the same case `foo(from point: T, to point: T)`, and even if they did, prepositions can make lousy local variable names--see first paragraph.)

I don’t see this as a problem for enum case authors. It just means the poor pattern writer needs to provide the positional information to disambiguate.

What do you mean by "positional information" here?

4.
The proposal does not explore what happens when the proposed prohibition on "mixing and matching" the proposed sugared and unsugared pattern matching runs up against associated values that have a mix of labeled and unlabeled parameters, and pattern matching user cases where the user does not wish to bind all of the arguments.

Given `case foo(a: Int, String, b: Int, String)`, the only sensible interpretation of the rules for sugared syntax would allow the user to choose any name for some but not all of the labels. If the user wishes to bind only `b`, however, he or she will need to navigate a puzzling set of rules that are not spelled out in the proposal:

case foo(a: _, _, b: let b, _)
// this is definitely allowed

case foo(a: _, _, b: let myVar, _)
// this is also definitely allowed

// but...
case foo(_, _, b: let myVar, _)
// is this allowed, or must the user explicitly state and not bind `a`?

// ...and with respect to the sugared version...
case foo(_, _, let b, _)
// is this allowed, or must the user explicitly state and not bind `a`?

Good point. To make up for this: `_` can substitute any sub pattern, which is something that this proposal doesn’t change but definitely worth spelling out.

5.
In the "update and commentary" revising SE-0111, the core team outlined a preferred path to restoring the full use of argument labels for functions without giving them type system significance. They gave a non-sugared form and a sugared form, both of which have met with approval from the community.

Briefly, the non-sugared form allows compound names to be used in variable names: `func foo(opToUse op(lhs:rhs:) : (Int, Int) -> Int)`. The first part of this proposal is consistent in that it removes the type system significance of argument labels from the associated values of enum cases, and considers them as part of the enum case name. It also stands to reason that, if a user were to match a case _without_ trying to bind any variables, the same syntax would have be used if the base name is ambiguous: `case elet(locals:body:): break`.

However, the proposal makes no provision for using that same compound name in pattern matching. There appears to be no particular reason for its isolated omission here, as `case elet(locals:body:)(let a, let b): return a * b` is readable and presents no syntactic difficulties. (Moreover, it is consistent with the syntax permitted in this proposal for initializing a variable: `let foo = Expr.elet(locals:body:)([], anExpr)`.)

Another good point. We can handle this in the purely additional proposal for compound variable names. I consider this not the 5th item in the list, but a separate suggestion, however :stuck_out_tongue:

---

In light of these shortcomings, I would argue that the following alternative scheme is the most intuitive and consistent for pattern matching given the general agreement that enum case representation should be "normalized":

Given:

enum S {
  case foo(bar: Int, baz: Int)
  case foo(boo: String)
  case bar(boo: String)
}

a. As in functions after SE-0111, enum cases can be identified unambiguously, regardless of whether one is initializing a variable or matching a case, by their compound name, e.g. `bar(boo:)`. Where a case can be unambiguously identified with only the base name, that is an alternative spelling, e.g. `bar`. Where a case cannot be identified uniquely with the base name, then it is an error to try to use the base name alone: `case foo: break // error: unambiguous`.

b. As in functions after SE-0111, arguments can be passed in either a sugared form or an unsugared form, and they can be bound in a pattern matching statement in the same way. That is, `case foo(bar: let a, baz: let b): break` and `case foo(bar:baz:)(let a, let b): break` are equivalent.

c. As in functions, one cannot supply different or incorrect argument labels. That is, `case foo(baz: let a, bar: let b)` and `case foo(baz:bar:)(let a, let b)` are both forbidden. _This recovers the vast majority of the additional syntactic safety that is outlined in the revised proposal, but without the use of any special rules for pattern matching._

d. By composing rules (a) and (b), `case bar(let a)` is allowed as it is today, preserving source compatibility. However `case foo(let b, let c)` is not allowed, and _not_ because different local variable names are chosen, but because the enum has two cases named foo.

From a user’s point of view, there’s enough positional information in this pattern for the compiler to figure out which case it should match. This would be very unintuitive IMO.

Wait, the key point of your proposal, with its "stricter rules," is that labels shouldn't be optional even with sufficient positional information! That's also the whole thing above about getting us closer to aligning with SE-0111, etc.

Fair enough. The argument I invoked leads us to a dark path :stuck_out_tongue:

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Xiaodi Wu) #13

<snip>

The key distinction we need to decide here is whether case labels are

“cosmetic”. We don’t allow declaration of separate parameter name and
internal name for associated values. I interpret that as we are enforcing
the syntax sugar in function declaration where user can use one symbol to
represent both:

func f(x: Int) // is the same as func f(x x: Int)

It’s tempting to treat matching an enum value against a pattern as
assigning a function value to a variable.

Sorry, I am not sure I understand this sentence.

Aka viewing the case pattern as simply an compound variable assignment as
envisioned in the SE-0111 commentary. This way of the labels would be
"cosmetic".

I'm sorry. Still don't understand :frowning:

I have a very simple understanding of what makes something "cosmetic."
Simply, it's something that the API author can write, which the API
consumer can choose to read but is never (or, at least, rarely) required to
write.

If that’s what we are doing, it makes perfect sense to say we get

“ultimate glory” here with patterns. Meaning, as you suggested, we consider
the case labels “cosmetic”. It’s really just tho parameter name in a
function (the first of the two “x” in code comment above.

But that’s kind of a stretch isn’t it? An enum value is very different
compared to a function value. Yes, there happen to be a function that
constructs this enum value that’s declared when user declare a case, that
function gets as much resemblance as any other. But the enum value it self
deserves more consideration. Telling the user “do these things that you do
with a function value” just makes pattern matching harder to explain,
because we are *not* assigning nor invoking function values.

Ah, I see. You think of the associated value as something distinct from
the declaration used to initialize it. However, there is no spelling for an
associated value other than what is used to initialize it. Given `case
foo(bar: Int, baz: Int, boo: Int)`, previously, the full name of the case
was `foo` and the associated value was `(bar: Int, baz: Int, boo: Int)`.
Your proposal causes the full name of the case instead to be
`foo(bar:baz:boo:)` and the associated value to be `(Int, Int, Int)`. Is
that not your understanding of it?

Yes

Pattern matching is just a matter of (a) indicating what case you want to
identify with the pattern; and (b) what parts of the associated value you
wish to match or to bind to variables. Part (a) is done by writing the
name, either the base name or in full (i.e. either `foo` or
`foo(bar:baz:boo:)`). Part (b) is done by writing `let myVariableName` in
the intended positions.

What I left out is that the internal/parameter names of a function are
non-optional part of its signature (one must use exact parameter names to
implement a method in a protocol, for example).

That's not true, and if we change Swift's rules to make it true, my own
code would become pervasively broken, and I suspect many others' code too.

protocol P {
func foo(_ bar: Int, _ baz: Int)
}

extension P {
func foo(_ a: Int, _ b: Int) {
print("Hello from P!")
}
}

struct S : P {
func foo(_ x: Int, _ y: Int) {
print("Hello world!")
}
}

struct T : P { }

let p: P = S()
p.foo(42, 42) // "Hello world!"
let q: P = T()
q.foo(42, 42) // "Hello from P!"

I prefer treating labels in case pattern matching the same way we treat

parameter names in protocol method implementation (due to the symmetry
between constructing/deconstructing body mentioned in my previous comments).

Independent of the how internal names of a function are handled, I would
strongly disagree with this idea as well. Implementing a protocol is the
act of an API author--i.e., you are opting a type into a contract about its
API. Pattern matching is the act of an API user. Swift has always observed
a sharp differentiation between these two activities; it is, in essence,
the dividing line between internal and not internal. We absolutely should
not design pattern matching to parallel rules for API contracts.

···

On Sun, Apr 2, 2017 at 6:30 PM, Daniel Duan <daniel@duan.org> wrote:

That’s not to say we need totally distinct syntax. Deconstructing a value

should visually relate to constructing it. So here’s how I think these two
relate: a constructor is a function. Function signature has these arguments
that the function refers to in its body. Pattern matching is the starting
point of deconstructing a value. The scope created following it is the
equivalent of a “body”, in which the associated values are used as
“arguments”. Therefore it make sense to say that these labels are more like
internal names (the 2nd “x” in the comment of the above sample).

3.

The first part of the proposal aligns enum case syntax with functions.
Functions often taken prepositions as argument labels, and indeed previous
SE proposals have extended the rules to allow most words. However, `case
foo(index: Int, in: T)` would have a disastrous label, as `in` would be a
very annoying variable name whose use would be actively encouraged by the
proposed sugared pattern matching rules.

The proposed rules for the sugared pattern would also require (well,
greatly encourage) unique labels for each argument. This again is
inconsistent with the naming conventions encouraged by the first part of
the proposal aligning enum case syntax with functions, which have no such
restrictions. If a user names something `case foo(point: T, point: T)`,
then the matching rules would actively encourage an invalid redefinition of
a variable named `point`.

(On the other hand, the API author does not have the luxury of naming
the same case `foo(from point: T, to point: T)`, and even if they did,
prepositions can make lousy local variable names--see first paragraph.)

I don’t see this as a problem for enum case authors. It just means the
poor pattern writer needs to provide the positional information to
disambiguate.

What do you mean by "positional information" here?

4.

The proposal does not explore what happens when the proposed prohibition
on "mixing and matching" the proposed sugared and unsugared pattern
matching runs up against associated values that have a mix of labeled and
unlabeled parameters, and pattern matching user cases where the user does
not wish to bind all of the arguments.

Given `case foo(a: Int, String, b: Int, String)`, the only sensible
interpretation of the rules for sugared syntax would allow the user to
choose any name for some but not all of the labels. If the user wishes to
bind only `b`, however, he or she will need to navigate a puzzling set of
rules that are not spelled out in the proposal:

case foo(a: _, _, b: let b, _)
// this is definitely allowed

case foo(a: _, _, b: let myVar, _)
// this is also definitely allowed

// but...
case foo(_, _, b: let myVar, _)
// is this allowed, or must the user explicitly state and not bind `a`?

// ...and with respect to the sugared version...
case foo(_, _, let b, _)
// is this allowed, or must the user explicitly state and not bind `a`?

Good point. To make up for this: `_` can substitute any sub pattern,
which is something that this proposal doesn’t change but definitely worth
spelling out.

5.
In the "update and commentary" revising SE-0111, the core team outlined
a preferred path to restoring the full use of argument labels for functions
without giving them type system significance. They gave a non-sugared form
and a sugared form, both of which have met with approval from the community.

Briefly, the non-sugared form allows compound names to be used in
variable names: `func foo(opToUse op(lhs:rhs:) : (Int, Int) -> Int)`. The
first part of this proposal is consistent in that it removes the type
system significance of argument labels from the associated values of enum
cases, and considers them as part of the enum case name. It also stands to
reason that, if a user were to match a case _without_ trying to bind any
variables, the same syntax would have be used if the base name is
ambiguous: `case elet(locals:body:): break`.

However, the proposal makes no provision for using that same compound
name in pattern matching. There appears to be no particular reason for its
isolated omission here, as `case elet(locals:body:)(let a, let b): return a
* b` is readable and presents no syntactic difficulties. (Moreover, it is
consistent with the syntax permitted in this proposal for initializing a
variable: `let foo = Expr.elet(locals:body:)([], anExpr)`.)

Another good point. We can handle this in the purely additional proposal
for compound variable names. I consider this not the 5th item in the list,
but a separate suggestion, however :stuck_out_tongue:

---

In light of these shortcomings, I would argue that the following
alternative scheme is the most intuitive and consistent for pattern
matching given the general agreement that enum case representation should
be "normalized":

Given:

enum S {
  case foo(bar: Int, baz: Int)
  case foo(boo: String)
  case bar(boo: String)
}

a. As in functions after SE-0111, enum cases can be identified
unambiguously, regardless of whether one is initializing a variable or
matching a case, by their compound name, e.g. `bar(boo:)`. Where a case can
be unambiguously identified with only the base name, that is an alternative
spelling, e.g. `bar`. Where a case cannot be identified uniquely with the
base name, then it is an error to try to use the base name alone: `case
foo: break // error: unambiguous`.

b. As in functions after SE-0111, arguments can be passed in either a
sugared form or an unsugared form, and they can be bound in a pattern
matching statement in the same way. That is, `case foo(bar: let a, baz: let
b): break` and `case foo(bar:baz:)(let a, let b): break` are equivalent.

c. As in functions, one cannot supply different or incorrect argument
labels. That is, `case foo(baz: let a, bar: let b)` and `case
foo(baz:bar:)(let a, let b)` are both forbidden. _This recovers the vast
majority of the additional syntactic safety that is outlined in the revised
proposal, but without the use of any special rules for pattern matching._

d. By composing rules (a) and (b), `case bar(let a)` is allowed as it is
today, preserving source compatibility. However `case foo(let b, let c)` is
not allowed, and _not_ because different local variable names are chosen,
but because the enum has two cases named foo.

From a user’s point of view, there’s enough positional information in
this pattern for the compiler to figure out which case it should match.
This would be very unintuitive IMO.

Wait, the key point of your proposal, with its "stricter rules," is that
labels shouldn't be optional even with sufficient positional information!
That's also the whole thing above about getting us closer to aligning with
SE-0111, etc.

Fair enough. The argument I invoked leads us to a dark path :stuck_out_tongue:

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Daniel Duan) #14

<snip>

The key distinction we need to decide here is whether case labels are “cosmetic”. We don’t allow declaration of separate parameter name and internal name for associated values. I interpret that as we are enforcing the syntax sugar in function declaration where user can use one symbol to represent both:

func f(x: Int) // is the same as func f(x x: Int)

It’s tempting to treat matching an enum value against a pattern as assigning a function value to a variable.

Sorry, I am not sure I understand this sentence.

Aka viewing the case pattern as simply an compound variable assignment as envisioned in the SE-0111 commentary. This way of the labels would be "cosmetic".

I'm sorry. Still don't understand :frowning:

I have a very simple understanding of what makes something "cosmetic." Simply, it's something that the API author can write, which the API consumer can choose to read but is never (or, at least, rarely) required to write.

If that’s what we are doing, it makes perfect sense to say we get “ultimate glory” here with patterns. Meaning, as you suggested, we consider the case labels “cosmetic”. It’s really just tho parameter name in a function (the first of the two “x” in code comment above.

But that’s kind of a stretch isn’t it? An enum value is very different compared to a function value. Yes, there happen to be a function that constructs this enum value that’s declared when user declare a case, that function gets as much resemblance as any other. But the enum value it self deserves more consideration. Telling the user “do these things that you do with a function value” just makes pattern matching harder to explain, because we are *not* assigning nor invoking function values.

Ah, I see. You think of the associated value as something distinct from the declaration used to initialize it. However, there is no spelling for an associated value other than what is used to initialize it. Given `case foo(bar: Int, baz: Int, boo: Int)`, previously, the full name of the case was `foo` and the associated value was `(bar: Int, baz: Int, boo: Int)`. Your proposal causes the full name of the case instead to be `foo(bar:baz:boo:)` and the associated value to be `(Int, Int, Int)`. Is that not your understanding of it?

Yes

Pattern matching is just a matter of (a) indicating what case you want to identify with the pattern; and (b) what parts of the associated value you wish to match or to bind to variables. Part (a) is done by writing the name, either the base name or in full (i.e. either `foo` or `foo(bar:baz:boo:)`). Part (b) is done by writing `let myVariableName` in the intended positions.

What I left out is that the internal/parameter names of a function are non-optional part of its signature (one must use exact parameter names to implement a method in a protocol, for example).

That's not true, and if we change Swift's rules to make it true, my own code would become pervasively broken, and I suspect many others' code too.

My apologies, not sure where I got that impression.

protocol P {
	func foo(_ bar: Int, _ baz: Int)
}

extension P {
	func foo(_ a: Int, _ b: Int) {
		print("Hello from P!")
	}
}

struct S : P {
	func foo(_ x: Int, _ y: Int) {
		print("Hello world!")
	}
}

struct T : P { }

let p: P = S()
p.foo(42, 42) // "Hello world!"
let q: P = T()
q.foo(42, 42) // "Hello from P!"

I prefer treating labels in case pattern matching the same way we treat parameter names in protocol method implementation (due to the symmetry between constructing/deconstructing body mentioned in my previous comments).

Independent of the how internal names of a function are handled, I would strongly disagree with this idea as well. Implementing a protocol is the act of an API author--i.e., you are opting a type into a contract about its API. Pattern matching is the act of an API user. Swift has always observed a sharp differentiation between these two activities; it is, in essence, the dividing line between internal and not internal. We absolutely should not design pattern matching to parallel rules for API contracts.

I’m not satisfied by your definition of API author/user. When I conform my type to, say, Equitable, or UITableViewDataSource, I consider myself *user* of an API in the form of a contract: to use functionality provided by this code, implement some stuff that meets some requirements associated with it. The author of these protocols specify these interfaces including what method name the user should conform with, what’s the best semantic for them, etc.

···

On Apr 2, 2017, at 5:12 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:
On Sun, Apr 2, 2017 at 6:30 PM, Daniel Duan <daniel@duan.org <mailto:daniel@duan.org>> wrote:

That’s not to say we need totally distinct syntax. Deconstructing a value should visually relate to constructing it. So here’s how I think these two relate: a constructor is a function. Function signature has these arguments that the function refers to in its body. Pattern matching is the starting point of deconstructing a value. The scope created following it is the equivalent of a “body”, in which the associated values are used as “arguments”. Therefore it make sense to say that these labels are more like internal names (the 2nd “x” in the comment of the above sample).

3.
The first part of the proposal aligns enum case syntax with functions. Functions often taken prepositions as argument labels, and indeed previous SE proposals have extended the rules to allow most words. However, `case foo(index: Int, in: T)` would have a disastrous label, as `in` would be a very annoying variable name whose use would be actively encouraged by the proposed sugared pattern matching rules.

The proposed rules for the sugared pattern would also require (well, greatly encourage) unique labels for each argument. This again is inconsistent with the naming conventions encouraged by the first part of the proposal aligning enum case syntax with functions, which have no such restrictions. If a user names something `case foo(point: T, point: T)`, then the matching rules would actively encourage an invalid redefinition of a variable named `point`.

(On the other hand, the API author does not have the luxury of naming the same case `foo(from point: T, to point: T)`, and even if they did, prepositions can make lousy local variable names--see first paragraph.)

I don’t see this as a problem for enum case authors. It just means the poor pattern writer needs to provide the positional information to disambiguate.

What do you mean by "positional information" here?

4.
The proposal does not explore what happens when the proposed prohibition on "mixing and matching" the proposed sugared and unsugared pattern matching runs up against associated values that have a mix of labeled and unlabeled parameters, and pattern matching user cases where the user does not wish to bind all of the arguments.

Given `case foo(a: Int, String, b: Int, String)`, the only sensible interpretation of the rules for sugared syntax would allow the user to choose any name for some but not all of the labels. If the user wishes to bind only `b`, however, he or she will need to navigate a puzzling set of rules that are not spelled out in the proposal:

case foo(a: _, _, b: let b, _)
// this is definitely allowed

case foo(a: _, _, b: let myVar, _)
// this is also definitely allowed

// but...
case foo(_, _, b: let myVar, _)
// is this allowed, or must the user explicitly state and not bind `a`?

// ...and with respect to the sugared version...
case foo(_, _, let b, _)
// is this allowed, or must the user explicitly state and not bind `a`?

Good point. To make up for this: `_` can substitute any sub pattern, which is something that this proposal doesn’t change but definitely worth spelling out.

5.
In the "update and commentary" revising SE-0111, the core team outlined a preferred path to restoring the full use of argument labels for functions without giving them type system significance. They gave a non-sugared form and a sugared form, both of which have met with approval from the community.

Briefly, the non-sugared form allows compound names to be used in variable names: `func foo(opToUse op(lhs:rhs:) : (Int, Int) -> Int)`. The first part of this proposal is consistent in that it removes the type system significance of argument labels from the associated values of enum cases, and considers them as part of the enum case name. It also stands to reason that, if a user were to match a case _without_ trying to bind any variables, the same syntax would have be used if the base name is ambiguous: `case elet(locals:body:): break`.

However, the proposal makes no provision for using that same compound name in pattern matching. There appears to be no particular reason for its isolated omission here, as `case elet(locals:body:)(let a, let b): return a * b` is readable and presents no syntactic difficulties. (Moreover, it is consistent with the syntax permitted in this proposal for initializing a variable: `let foo = Expr.elet(locals:body:)([], anExpr)`.)

Another good point. We can handle this in the purely additional proposal for compound variable names. I consider this not the 5th item in the list, but a separate suggestion, however :stuck_out_tongue:

---

In light of these shortcomings, I would argue that the following alternative scheme is the most intuitive and consistent for pattern matching given the general agreement that enum case representation should be "normalized":

Given:

enum S {
  case foo(bar: Int, baz: Int)
  case foo(boo: String)
  case bar(boo: String)
}

a. As in functions after SE-0111, enum cases can be identified unambiguously, regardless of whether one is initializing a variable or matching a case, by their compound name, e.g. `bar(boo:)`. Where a case can be unambiguously identified with only the base name, that is an alternative spelling, e.g. `bar`. Where a case cannot be identified uniquely with the base name, then it is an error to try to use the base name alone: `case foo: break // error: unambiguous`.

b. As in functions after SE-0111, arguments can be passed in either a sugared form or an unsugared form, and they can be bound in a pattern matching statement in the same way. That is, `case foo(bar: let a, baz: let b): break` and `case foo(bar:baz:)(let a, let b): break` are equivalent.

c. As in functions, one cannot supply different or incorrect argument labels. That is, `case foo(baz: let a, bar: let b)` and `case foo(baz:bar:)(let a, let b)` are both forbidden. _This recovers the vast majority of the additional syntactic safety that is outlined in the revised proposal, but without the use of any special rules for pattern matching._

d. By composing rules (a) and (b), `case bar(let a)` is allowed as it is today, preserving source compatibility. However `case foo(let b, let c)` is not allowed, and _not_ because different local variable names are chosen, but because the enum has two cases named foo.

From a user’s point of view, there’s enough positional information in this pattern for the compiler to figure out which case it should match. This would be very unintuitive IMO.

Wait, the key point of your proposal, with its "stricter rules," is that labels shouldn't be optional even with sufficient positional information! That's also the whole thing above about getting us closer to aligning with SE-0111, etc.

Fair enough. The argument I invoked leads us to a dark path :stuck_out_tongue:

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Xiaodi Wu) #15

The contract is not merely or principally between you, the author of the
type, and the author of the protocol. In fact, the author of the protocol
doesn't have to know or care that your type exists.

The key and salient contract is between you, author of the type, and the
users of the type. You are the vendor of an API, and by stating a
conformance, you are making a guarantee to your users about the API of your
type.

···

On Sun, Apr 2, 2017 at 19:58 Daniel Duan <daniel@duan.org> wrote:

On Apr 2, 2017, at 5:12 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Sun, Apr 2, 2017 at 6:30 PM, Daniel Duan <daniel@duan.org> wrote:

<snip>

The key distinction we need to decide here is whether case labels are
“cosmetic”. We don’t allow declaration of separate parameter name and
internal name for associated values. I interpret that as we are enforcing
the syntax sugar in function declaration where user can use one symbol to
represent both:

func f(x: Int) // is the same as func f(x x: Int)

It’s tempting to treat matching an enum value against a pattern as
assigning a function value to a variable.

Sorry, I am not sure I understand this sentence.

Aka viewing the case pattern as simply an compound variable assignment as
envisioned in the SE-0111 commentary. This way of the labels would be
"cosmetic".

I'm sorry. Still don't understand :frowning:

I have a very simple understanding of what makes something "cosmetic."
Simply, it's something that the API author can write, which the API
consumer can choose to read but is never (or, at least, rarely) required to
write.

If that’s what we are doing, it makes perfect sense to say we get
“ultimate glory” here with patterns. Meaning, as you suggested, we consider
the case labels “cosmetic”. It’s really just tho parameter name in a
function (the first of the two “x” in code comment above.

But that’s kind of a stretch isn’t it? An enum value is very different
compared to a function value. Yes, there happen to be a function that
constructs this enum value that’s declared when user declare a case, that
function gets as much resemblance as any other. But the enum value it self
deserves more consideration. Telling the user “do these things that you do
with a function value” just makes pattern matching harder to explain,
because we are *not* assigning nor invoking function values.

Ah, I see. You think of the associated value as something distinct from
the declaration used to initialize it. However, there is no spelling for an
associated value other than what is used to initialize it. Given `case
foo(bar: Int, baz: Int, boo: Int)`, previously, the full name of the case
was `foo` and the associated value was `(bar: Int, baz: Int, boo: Int)`.
Your proposal causes the full name of the case instead to be
`foo(bar:baz:boo:)` and the associated value to be `(Int, Int, Int)`. Is
that not your understanding of it?

Yes

Pattern matching is just a matter of (a) indicating what case you want to
identify with the pattern; and (b) what parts of the associated value you
wish to match or to bind to variables. Part (a) is done by writing the
name, either the base name or in full (i.e. either `foo` or
`foo(bar:baz:boo:)`). Part (b) is done by writing `let myVariableName` in
the intended positions.

What I left out is that the internal/parameter names of a function are
non-optional part of its signature (one must use exact parameter names to
implement a method in a protocol, for example).

That's not true, and if we change Swift's rules to make it true, my own
code would become pervasively broken, and I suspect many others' code too.

My apologies, not sure where I got that impression.

protocol P {
func foo(_ bar: Int, _ baz: Int)
}

extension P {
func foo(_ a: Int, _ b: Int) {
print("Hello from P!")
}
}

struct S : P {
func foo(_ x: Int, _ y: Int) {
print("Hello world!")
}
}

struct T : P { }

let p: P = S()
p.foo(42, 42) // "Hello world!"
let q: P = T()
q.foo(42, 42) // "Hello from P!"

I prefer treating labels in case pattern matching the same way we treat
parameter names in protocol method implementation (due to the symmetry
between constructing/deconstructing body mentioned in my previous comments).

Independent of the how internal names of a function are handled, I would
strongly disagree with this idea as well. Implementing a protocol is the
act of an API author--i.e., you are opting a type into a contract about its
API. Pattern matching is the act of an API user. Swift has always observed
a sharp differentiation between these two activities; it is, in essence,
the dividing line between internal and not internal. We absolutely should
not design pattern matching to parallel rules for API contracts.

I’m not satisfied by your definition of API author/user. When I conform my
type to, say, Equitable, or UITableViewDataSource, I consider myself *user*
of an API in the form of a contract: to use functionality provided by this
code, implement some stuff that meets some requirements associated with
it. The author of these protocols specify these interfaces including what
method name the user should conform with, what’s the best semantic for
them, etc.

That’s not to say we need totally distinct syntax. Deconstructing a value
should visually relate to constructing it. So here’s how I think these two
relate: a constructor is a function. Function signature has these arguments
that the function refers to in its body. Pattern matching is the starting
point of deconstructing a value. The scope created following it is the
equivalent of a “body”, in which the associated values are used as
“arguments”. Therefore it make sense to say that these labels are more like
internal names (the 2nd “x” in the comment of the above sample).

3.
The first part of the proposal aligns enum case syntax with functions.
Functions often taken prepositions as argument labels, and indeed previous
SE proposals have extended the rules to allow most words. However, `case
foo(index: Int, in: T)` would have a disastrous label, as `in` would be a
very annoying variable name whose use would be actively encouraged by the
proposed sugared pattern matching rules.

The proposed rules for the sugared pattern would also require (well,
greatly encourage) unique labels for each argument. This again is
inconsistent with the naming conventions encouraged by the first part of
the proposal aligning enum case syntax with functions, which have no such
restrictions. If a user names something `case foo(point: T, point: T)`,
then the matching rules would actively encourage an invalid redefinition of
a variable named `point`.

(On the other hand, the API author does not have the luxury of naming the
same case `foo(from point: T, to point: T)`, and even if they did,
prepositions can make lousy local variable names--see first paragraph.)

I don’t see this as a problem for enum case authors. It just means the
poor pattern writer needs to provide the positional information to
disambiguate.

What do you mean by "positional information" here?

4.
The proposal does not explore what happens when the proposed prohibition
on "mixing and matching" the proposed sugared and unsugared pattern
matching runs up against associated values that have a mix of labeled and
unlabeled parameters, and pattern matching user cases where the user does
not wish to bind all of the arguments.

Given `case foo(a: Int, String, b: Int, String)`, the only sensible
interpretation of the rules for sugared syntax would allow the user to
choose any name for some but not all of the labels. If the user wishes to
bind only `b`, however, he or she will need to navigate a puzzling set of
rules that are not spelled out in the proposal:

case foo(a: _, _, b: let b, _)
// this is definitely allowed

case foo(a: _, _, b: let myVar, _)
// this is also definitely allowed

// but...
case foo(_, _, b: let myVar, _)
// is this allowed, or must the user explicitly state and not bind `a`?

// ...and with respect to the sugared version...
case foo(_, _, let b, _)
// is this allowed, or must the user explicitly state and not bind `a`?

Good point. To make up for this: `_` can substitute any sub pattern, which
is something that this proposal doesn’t change but definitely worth
spelling out.

5.
In the "update and commentary" revising SE-0111, the core team outlined a
preferred path to restoring the full use of argument labels for functions
without giving them type system significance. They gave a non-sugared form
and a sugared form, both of which have met with approval from the community.

Briefly, the non-sugared form allows compound names to be used in variable
names: `func foo(opToUse op(lhs:rhs:) : (Int, Int) -> Int)`. The first
part of this proposal is consistent in that it removes the type system
significance of argument labels from the associated values of enum cases,
and considers them as part of the enum case name. It also stands to reason
that, if a user were to match a case _without_ trying to bind any
variables, the same syntax would have be used if the base name is
ambiguous: `case elet(locals:body:): break`.

However, the proposal makes no provision for using that same compound name
in pattern matching. There appears to be no particular reason for its
isolated omission here, as `case elet(locals:body:)(let a, let b): return a
* b` is readable and presents no syntactic difficulties. (Moreover, it is
consistent with the syntax permitted in this proposal for initializing a
variable: `let foo = Expr.elet(locals:body:)([], anExpr)`.)

Another good point. We can handle this in the purely additional proposal
for compound variable names. I consider this not the 5th item in the list,
but a separate suggestion, however :stuck_out_tongue:

---

In light of these shortcomings, I would argue that the following
alternative scheme is the most intuitive and consistent for pattern
matching given the general agreement that enum case representation should
be "normalized":

Given:

enum S {
  case foo(bar: Int, baz: Int)
  case foo(boo: String)
  case bar(boo: String)
}

a. As in functions after SE-0111, enum cases can be identified
unambiguously, regardless of whether one is initializing a variable or
matching a case, by their compound name, e.g. `bar(boo:)`. Where a case can
be unambiguously identified with only the base name, that is an alternative
spelling, e.g. `bar`. Where a case cannot be identified uniquely with the
base name, then it is an error to try to use the base name alone: `case
foo: break // error: unambiguous`.

b. As in functions after SE-0111, arguments can be passed in either a
sugared form or an unsugared form, and they can be bound in a pattern
matching statement in the same way. That is, `case foo(bar: let a, baz: let
b): break` and `case foo(bar:baz:)(let a, let b): break` are equivalent.

c. As in functions, one cannot supply different or incorrect argument
labels. That is, `case foo(baz: let a, bar: let b)` and `case
foo(baz:bar:)(let a, let b)` are both forbidden. _This recovers the vast
majority of the additional syntactic safety that is outlined in the revised
proposal, but without the use of any special rules for pattern matching._

d. By composing rules (a) and (b), `case bar(let a)` is allowed as it is
today, preserving source compatibility. However `case foo(let b, let c)` is
not allowed, and _not_ because different local variable names are chosen,
but because the enum has two cases named foo.

From a user’s point of view, there’s enough positional information in this
pattern for the compiler to figure out which case it should match. This
would be very unintuitive IMO.

Wait, the key point of your proposal, with its "stricter rules," is that
labels shouldn't be optional even with sufficient positional information!
That's also the whole thing above about getting us closer to aligning with
SE-0111, etc.

Fair enough. The argument I invoked leads us to a dark path :stuck_out_tongue:

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution