[swift-evolution] [Review] SE-0155: Normalize Enum Case Representation

Xiaodi Wu xiaodi.wu at gmail.com
Sun Apr 2 19:12:07 CDT 2017


On Sun, Apr 2, 2017 at 6:30 PM, Daniel Duan <daniel at 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 :(

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.


>
> 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 :P
>>>
>>>
>>> ---
>>>
>>> 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 :P
>>
>>
>> _______________________________________________
>> swift-evolution mailing list
>> swift-evolution at swift.org
>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>
>>
>>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170402/5c70e591/attachment.html>


More information about the swift-evolution mailing list