Normalize Enum Case Representation (rev. 2)


(Daniel Duan) #1

Hi everyone,

Here’s revision 2 of SE-0155. I’d love some feedback before going into re-review.

Note the “anonymous case” feature is not in this proposal. I found the motivation section difficult to write when it’s included. I’ve drafted a separate proposal that adds it.

Rendered version: https://github.com/dduan/swift-evolution/blob/SE0155-rev2/proposals/0155-normalize-enum-case-representation.md

···

# Normalize Enum Case Representation

* Proposal: [SE-0155][]
* Authors: [Daniel Duan][], [Joe Groff][]
* Review Manager: [John McCall][]

## Introduction

In Swift 3, associated values for an enum case are represented by
a labeled-tuple. This has several undesired effects: inconsistency in enum value
construction syntax, many forms of pattern matching, missing features such as
specifying default value and missed opportunity for layout improvements.

This proposal aims to make enums more "regular" by replacing tuple as the
representation of associated values, making declaration and construction of enum
cases more function-like.

Swift-evolution thread: [Compound Names For Enum Cases][SE Thread]

## Motivation

**Each enum case declares a function that can be used to create a corresponding
value. To users who expect these functions to behave "normally", surprises
await.**

1. Associated value labels aren't part of the function name.

    After [SE-0111][] Swift function's fully qualified name consists of its
    base-name and all argument labels. As an illustration, one can invoke
    a function with its full name:

    ```swift
    func f(x: Int, y: Int) {}
    f(x: y:)(0, 0) // Okay, this is equivalent to f(x: 0, y: 0)
    ```

    This, however, cannot be done when enum cases with associated value were
    constructed:

    ```swift
    enum Foo {
        case bar(x: Int, y: Int)
    }
    Foo.bar(x: y:)(0, 0) // Does not compile as of Swift 3
    ```

    Here, `x` and `y` are labels of bar's payload (a tuple), as opposed to being
    part of the case's formal name. This is inconsistent with rest of the
    language.

2. Default value for parameters isn't available in case declarations.

    ```swift
    enum Animation {
        case fadeIn(duration: TimeInterval = 0.3) // Nope!
    }
    let anim = Animation.fadeIn() // Would be nice, too bad!
    ```

**Associated values being a tuple complicates pattern matching.**

The least unexpected pattern to match a `bar` value is the following:

if case let .bar(x: p, y: q) = Foo.bar(x: 0, y: 1) {
    print(p, q) // 0 1
}

In Swift 3, there are a few alternatives that may not be obvious to new users.

1. A pattern with a single value would match and result in a tuple:

    ```swift
    if case let .bar(wat) = Foo.bar(x: 0, y: 1) {
        print(wat.y) // 1
    }
    ```

2. Labels in patterns are not enforced:

    ```swift
    // note: there's no label in the following pattern
    if case let .bar(p, q) = Foo.bar(x: 0, y: 1) {
        print(p, q) // 0 1
    }
    ```

These complex rules makes pattern matching difficult to teach and to expand to
other types.

**Moving away from tuple-as-associated-value also give us opportunity to improve
enum's memory layout** since each associated value would no longer play double
duty as part of the tuple's memory layout.

## Proposed Solution

When a enum case has associated values, they will no longer form a tuple. Their
labels will become part of the case's declared name. Patterns matching such
values must include labels.

This proposal also introduce the ability to include a default value for each
associated value in the declaration.

## Detailed Design

### Make associated value labels part of case's name
When labels are present in enum case's payload, they will become part of case's
declared name instead of being labels for fields in a tuple. In details, when
constructing an enum value with the case name, label names must either be
supplied in the argument list it self, or as part of the full name.

Foo.bar(x: 0, y: 0) // Okay, the Swift 3 way.
Foo.bar(x: y:)(0, 0) // Equivalent to the previous line.
Foo.bar(x: y:)(x: 0, y: 0) // This would be an error, however.

Note that since the labels aren't part of a tuple, they no longer participate in
type checking, similar to functions:

let f = Foo.bar // f has type (Int, Int) -> Foo
f(0, 0) // Okay!
f(x: 0, y: 0) // Won't compile.

Enum cases should have distinct *full* names. Therefore, shared base name will be allowed:

enum Expr {
    case literal(bool: Bool)
    case literal(int: Int)
}

### Add default value in enum case declarations
From a user's point view, declaring an enum case should remain the same as Swift
3 except now it's possible to add `= expression` after the type of an
associated value to convey a default value for that field. Updated syntax:

union-style-enum-case = enum-case-name [enum-case-associated-value-clause];
enum-case-associated-value-clause = "(" ")"
                                  > "(" enum-case-associated-value-list ")";
enum-case-associated-value-list = enum-associated-value-element
                                > enum-associated-value-element ","
                                  enum-case-associated-value-list;
enum-case-associated-value-element = element-name type-annotation
                                     [enum-case-element-default-value-clause]
                                   > type [enum-case-element-default-value-clause];
element-name = identifier;
enum-case-element-default-value-clause = "=" expression;

### Simplify pattern matching rules on enums
Syntax for enum case patterns will be the following:

enum-case-pattern = [type-identifier] "." enum-case-name [enum-case-associated-value-pattern];
enum-case-associated-value-pattern = "(" [enum-case-associated-value-list-pattern] ")";
enum-case-associated-value-list-pattern = enum-case-associated-value-list-pattern-element
                                        > enum-case-associated-value-list-pattern-element ","
                                          enum-case-associated-value-list-pattern;
enum-case-associated-value-list-element = pattern | identifier ":" pattern;

… and `case-associated-value-pattern` will be added to the list of various
`pattern`s.

Note that `enum-case-associated-value-pattern` is identical to `tuple-pattern`
except in names. It is introduced here to denote semantic difference between the
two. Whereas the syntax in Swift 3 allows a single `tuple-pattern-element` to
match the entire case payload, the number of
`enum-case-associated-value-list-pattern-element`s must be equal to that of
associated value of the case in order to be a match. This means code in the next
example will be deprecated under this proposal:

if case let .bar(wat) = Foo.bar(x: 0, y: 1) { // syntax error
    // …
}

Further, `identifier` in `enum-case-associated-value-list-pattern-element` must
be the same as the label of corresponding associated value intended for the
match. So this will be deprecated as well:

if case let .bar(p, q) = Foo.bar(x: 0, y: 1) { // missing `x:` and `y:`
    // …
}

## Source compatibility

As detailed in the previous section, this proposal deprecates certain pattern
matching syntax.

Other changes to the syntax are additive and source-compatible with Swift 3. For
example, matching a case with associated value solely by its name should still
work:

switch Foo.bar(x: 0, y: 1) {
case .bar: // matches.
    print("bar!")
}

## Effect on ABI stability and resilience

After this proposal, enum cases may have compound names, which would be mangled
differently than Swift 3.

The compiler may also layout enums differently now that payloads are not
constrained by having to be part of a tuple.

## Alternative Considered

To maintain maximum source compatibility, we could introduce a rule that matches
all associated values to a labeled tuple. As T.J. Usiyan
[pointed out][TJs comment], implementation of the equality protocol would be
simplified due to tuple's conformance to `Equatable`. This feature may still be
introduced with alternative syntax (perhaps related to splats) later without
source-breakage. And the need to implement `Equatable` may also disappear with
auto-deriving for `Equatable` conformance.

A syntax that did stay for source compatibility is allowing `()` in patterns
that match enum cases without associated values:

if let case .x() = Foo.baz { // … }

We could remove this syntax as it would make the pattern look more consistent to
the case's declaration.

[SE-0111]: https://github.com/apple/swift-evolution/blob/master/proposals/0111-remove-arg-label-type-significance.md
[Daniel Duan]: https://github.com/dduan
[Joe Groff]: https://github.com/jckarter
[SE-0155]: 0155-normalize-enum-case-representation.md
[TJs comment]: https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170116/030614.html
[SE Thread]: https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170116/030477.html
[John McCall]: https://github.com/rjmccall


(Xiaodi Wu) #2

The rendered version differs from the text appended to your message. I'll
assume the more fully fleshed out version is what you intend to submit.
Three comments/questions:

Enum Case "Overloading"

An enum may contain cases with the same full name but with associated
values of different types. For example:

enum Expr {
    case literal(Bool)
    case literal(Int)
}

The above cases have overloaded constructors, which follow the same rules
as functions at call site for disambiguation:

// It's clear which case is being constructed in the following.let
aBool: Expr = .literal(false)let anInt: Expr = .literal(42)

User must specify an as expression in sub-patterns in pattern matching, in
order to match with such cases:

case .literal(let value) // this is ambiguouscase .literal(let value
as Bool) // matches `case literal(Bool)`

Comment/question 1: Here, why aren't you proposing to allow `case
.literal(let value: Bool)`? For one, it would seem to be more consistent.
Second, since we still have some use cases where there's Obj-C bridging
magic with `as`, using `as` in this way may run into ambiguity issues if
(for example) you have two cases, one with associated value of type
`String` and the other of type `NSString`. Also, since enum cases are to be
like functions, I assume that the more verbose `as` version would work for
free: `case .literal(let value) as (Bool) -> Expr`?

<https://github.com/dduan/swift-evolution/blob/SE0155-rev2/proposals/0155-normalize-enum-case-representation.md#alternative-payload-less-case-declaration>Alternative
Payload-less Case Declaration

In Swift 3, the following syntax is valid:

enum Tree {
    case leaf() // the type of this constructor is confusing!}

Tree.leaf has a very unexpected type to most Swift users: (()) -> Tree

We propose this syntax declare the "bare" case instead. So it's going to be
the equivalent of

enum Tree {
    case leaf // `()` is optional and does the same thing.}

Comment/question 2: First, if associated values are not to be modeled as
tuples, for backwards compatibility the rare uses of `case leaf()` should
be migrated to `case leaf(())`. Second, to be clear, you are _not_
proposing additional sugar so that a case without an associated value be
equivalent to a case that has an associated value of type `Void`, correct?
You are saying that, with your proposal, both `case leaf()` and `case leaf`
would be regarded as being of type `() -> Tree` instead of the current
`(()) -> Tree`?[The latter (i.e. `() -> Tree`) seems entirely fine. The
former (i.e. additional sugar for `(()) -> Tree`) seems mostly fine, except
that it would introduce an inconsistency with raw values that IMO is
awkward. That is, if I have `enum Foo { case bar }`, it would make case
`bar` have implied associated type `Void`; but, if I have `enum Foo: Int {
case bar }`, would case `bar` have raw value `0` of type `Int` as well as
associated value `()` of type `Void`?]

<https://github.com/dduan/swift-evolution/blob/SE0155-rev2/proposals/0155-normalize-enum-case-representation.md#pattern-consistency>Pattern
Consistency

*(The following enum will be used throughout code snippets in this
section).*

indirect enum Expr {
    case variable(name: String)
    case lambda(parameters: [String], body: Expr)
}

Compared to patterns in Swift 3, matching against enum cases will follow
stricter rules. This is a consequence of no longer relying on tuple
patterns.

When an associated value has a label, the sub-pattern must include the
label exactly as declared. There are two variants that should look familiar
to Swift 3 users. Variant 1 allows user to bind the associated value to
arbitrary name in the pattern by requiring the label:

case .variable(name: let x) // okaycase .variable(x: let x) // compile
error; there's no label `x`case .lambda(parameters: let params, body:
let body) // Okaycase .lambda(params: let params, body: let body) //
error: 1st label mismatches

User may choose not to use binding names that differ from labels. In this
variant, the corresponding value will bind to the label, resulting in this
shorter form:

case .variable(let name) // okay, because the name is the same as the
labelcase .lambda(let parameters, let body) // this is okay too, same
reason.case .variable(let x) // compiler error. label must appear one
way or another.case .lambda(let params, let body) // compiler error,
same reason as above.

Comment/question 3: Being a source-breaking change, that requires extreme
justification, and I just don't think there is one for this rule. The
perceived problem being addressed (that one might try to bind `parameters`
to `body` and `body` to `parameters`) is unchanged whether enum cases are
modeled as tuples or functions, so aligning enum cases to functions is not
in and of itself justification to revisit the issue of whether to try to
prohibit this. In fact, I think the proposed solution suffers from two
great weaknesses. First, it seems ad-hoc. Consider this: if enum cases are
to be modeled as functions, then I should be able to write something
intermediate between the options above; namely: `case .variable(name:)(let
x)`. Since `.variable` unambiguously refers to `.variable(name:)`, I should
also be allowed to write `.variable(let x)` just as I am now. Second, it seems
unduly restrictive. If, in the containing scope, I have a variable named
`body` that I don't want to shadow, this rule would force me to either
write the more verbose form or deal with shadowing `body`. If a person opts
for the shorter form, they are choosing not to use the label.

Only one of these variants may appear in a single pattern. Swift compiler
will raise a compile error for mixed usage.

case .lambda(parameters: let params, let body) // error, can not mix the two.

Some patterns will no longer match enum cases. For example, all associated
values can bind as a tuple in Swift 3, this will no longer work after this
proposal:

// deprecated: matching all associated values as a tupleif case let
.lambda(f) = anLambdaExpr {
    evaluateLambda(parameters: f.parameters, body: f.body)
}

<https://github.com/dduan/swift-evolution/blob/SE0155-rev2/proposals/0155-normalize-enum-case-representation.md#source-compatibility>


(Joe Groff) #3

You appear to have pasted a different version below from what's rendered at that link.

Some comments:

- Overloading seems like unnecessarily distracting scope creep. I would leave it out.

- Having `case leaf()` have type Tree is IMO more surprising than making it equivalent to `case leaf`. It should either behave as if it were a method, or be banned outright. We don't need two ways to spell the same thing.

-Joe

···

On Mar 8, 2017, at 7:09 PM, Daniel Duan via swift-evolution <swift-evolution@swift.org> wrote:

Hi everyone,

Here’s revision 2 of SE-0155. I’d love some feedback before going into re-review.

Note the “anonymous case” feature is not in this proposal. I found the motivation section difficult to write when it’s included. I’ve drafted a separate proposal that adds it.

Rendered version: https://github.com/dduan/swift-evolution/blob/SE0155-rev2/proposals/0155-normalize-enum-case-representation.md


(Daniel Duan) #4

Here’s an updated version with hopefully the correct content. The “overloaded” case feature has been removed and is discussed in the "alternatives considered" section.

# Normalize Enum Case Representation

* Proposal: [SE-0155][]
* Authors: [Daniel Duan][], [Joe Groff][]
* Review Manager: [John McCall][]
* Status: **Awaiting review**
* Previous Revision: [1][Revision 1]

## Introduction

In Swift 3, associated values of an enum case are represented by a tuple. This
implementation causes inconsistencies in case declaration, construction and
pattern matching in several places.

Enums, therefore, can be made more "regular" when we replace tuple as the
representation of associated case values. This proposal aims to define the
effect of doings so on various parts of the language.

Swift-evolution thread: [Normalize Enum Case Representation (rev. 2)][]

## Motivation

When user declares a case for an enum, a function which constructs the
corresponding case value is declared. We'll refer to such functions as _case
constructors_ in this proposal.

enum Expr {
    // this case declares the case constructor `Expr.elet(_:_:)`
    indirect case elet(locals: [(String, Expr)], body: Expr)
}

// f's signature is f(_: _), type is ([(String, Expr)], Expr) -> Expr
let f = Expr.elet

// `f` is just a function
f([], someExpr) // construct a `Expr.elet`

There are many surprising aspects of enum constructors, however:

1. After [SE-0111][], Swift function's fully qualified name consists of its base
   name and all of its argument labels. User can use the full name of the
   function at use site. In the example above, `locals` and `body` are currently
   not part of the case constructors name, therefore the expected syntax is
   invalid.

   func f(x: Int, y: Int) {}
   f(x: y:)(0, 0) // Okay, this is equivalent to f(x: 0, y: 0)
   Expr.elet(locals: body:)([], someExpr) // this doesn't work in Swift 3

2. Case constructors cannot include a default value for each parameter. This
   is yet another feature available to functions.

As previous mentioned, these are symptoms of associated values being a tuple
instead of having its own distinct semantics. This problem manifests more in
Swift 3's pattern matching:

1. A pattern with a single value would match and result in a tuple:

    ```swift
    // this works for reasons most user probably don't expect!
    if case .elet(let wat) = anExpr {
        eval(wat.body)
    }
    ```

2. Labels in patterns are not enforced:

    ```swift
    // note: there's no label in the first sub-pattern
    if case .elet(let p, let body: q) = anExpr {
        // code
    }
    ```

These extra rules makes pattern matching difficult to teach and to expand to
other types.

## Proposed Solution

We'll add first class syntax (which largely resemble the syntax in Swift 3) for
declaring associated values with labels. Tuple will no longer be used to
represent the aggregate of associated values for an enum case. This means
pattern matching for enum cases needs its own syntax as well (as opposed to
piggybacking on tuple patterns, which remains in the language for tuples.).

## Detailed Design

### Compound Names For Enum Constructors

Associated values' labels should be part of the enum case's constructor name.
When constructing an enum value with the case name, label names must either be
supplied in the argument list it self, or as part of the full name.

Expr.elet(locals: [], body: anExpr) // Okay, the Swift 3 way.
Expr.elet(locals: body:)([], anExpr) // Okay, equivalent to the previous line.
Expr.elet(locals: body:)(locals: 0, body: 0) // This would be an error, however.

Note that since the labels aren't part of a tuple, they no longer participate in
type checking, behaving consistently with functions.

let f = Expr.elet // f has type ([(String, Expr)], Expr) -> Expr
f([], anExpr) // Okay!
f(locals: [], body: anExpr) // Won't compile.

Enum cases should have distinct *full* names. Therefore, shared base name will
be allowed:

enum SyntaxTree {
    case type(variables: [TypeVariable])
    case type(instantiated: [Type])
}

Using only the base name in pattern matching for the previous example would be
ambiguous and result in an compile error. In this case, the full name must be
supplied to disambiguate.

case .type // error: ambiguous
case .type(variables: let variables) // Okay

### Default Parameter Values For Enum Constructors

From a user's point view, declaring an enum case should remain the same as Swift
3 except now it's possible to add `= expression` after the type of an
associated value to convey a default value for that field.

enum Animation {
    case fadeIn(duration: TimeInterval = 0.3) // Okay!
}
let anim = Animation.fadeIn() // Great!

Updated syntax:

union-style-enum-case = enum-case-name [enum-case-associated-value-clause];
enum-case-associated-value-clause = "(" ")"
                                  > "(" enum-case-associated-value-list ")";
enum-case-associated-value-list = enum-associated-value-element
                                > enum-associated-value-element ","
                                  enum-case-associated-value-list;
enum-case-associated-value-element = element-name type-annotation
                                     [enum-case-element-default-value-clause]
                                   > type
                                     [enum-case-element-default-value-clause];
element-name = identifier;
enum-case-element-default-value-clause = "=" expression;

### Alternative Payload-less Case Declaration

In Swift 3, the following syntax is valid:

enum Tree {
    case leaf() // the type of this constructor is confusing!
}

`Tree.leaf` has a very unexpected type to most Swift users: `(()) -> Tree`

We propose this syntax become illegal. User must explicitly declare
associated value of type `Void` if needed:

enum Tree {
    case leaf(Void)
}

### Pattern Consistency

*(The following enum will be used throughout code snippets in this section).*

indirect enum Expr {
    case variable(name: String)
    case lambda(parameters: [String], body: Expr)
}

Compared to patterns in Swift 3, matching against enum cases will follow
stricter rules. This is a consequence of no longer relying on tuple patterns.

When an associated value has a label, the sub-pattern must include the label
exactly as declared. There are two variants that should look familiar to Swift
3 users. Variant 1 allows user to bind the associated value to arbitrary name in
the pattern by requiring the label:

case .variable(name: let x) // okay
case .variable(x: let x) // compile error; there's no label `x`
case .lambda(parameters: let params, body: let body) // Okay
case .lambda(params: let params, body: let body) // error: 1st label mismatches

User may choose not to use binding names that differ from labels. In this
variant, the corresponding value will bind to the label, resulting in this
shorter form:

case .variable(let name) // okay, because the name is the same as the label
case .lambda(let parameters, let body) // this is okay too, same reason.
case .variable(let x) // compiler error. label must appear one way or another.
case .lambda(let params, let body) // compiler error, same reason as above.

Only one of these variants may appear in a single pattern. Swift compiler will
raise a compile error for mixed usage.

case .lambda(parameters: let params, let body) // error, can not mix the two.

Some patterns will no longer match enum cases. For example, all associated
values can bind as a tuple in Swift 3, this will no longer work after this
proposal:

// deprecated: matching all associated values as a tuple
if case let .lambda(f) = anLambdaExpr {
    evaluateLambda(parameters: f.parameters, body: f.body)
}

## Source compatibility

Despite a few additions, case declaration remain mostly source-compatible with
Swift 3, with the exception of the change detailed in "Alternative Payload-less
Case Declaration".

Syntax for case constructor at use site remain source-compatible.

A large portion of pattern matching syntax for enum cases with associated values
remain unchanged. But patterns for matching all values as a tuple, patterns that
elide the label and binds to names that differ from the labels, patterns that
include labels for some sub-patterns but the rest of them are deprecated by this
proposal. Therefore this is a source breaking change.

## Effect on ABI stability and resilience

After this proposal, enum cases may have compound names. This means the standard
library will expose different symbols for enum constructors. The name mangling
rules should also change accordingly.

## Alternative Considered

Between case declaration and pattern matching, there exist many reasonable
combinations of improvement. On one hand, we can optimize for consistency,
simplicity and teachability by bringing in as much similarity between enum and
other part of the language as possible. Many decisions in the first revision
were made in favor if doing so. Through the feedbacks from swift-evolution, we
found that some of the changes impedes the ergonomics of these features too much
. In this section, we describe some of the alternatives that were raised and
rejected in hope to strike a balance between the two end of the goals.

We discussed allowing user to declare a *parameter name* ("internal names")
for each associated value. Such names may be used in various rules in pattern
matching. Some feedback suggested they maybe used as property names when we
make enum case subtypes of the enum and resembles a struct. This feature is not
included in this proposal because parameter names are not very useful *today*.
Using them in patterns actually don't improve consistency as users don't use
them outside normal function definitions at all. If enum case gains a function
body in a future proposal, it'd be better to define the semantics of parameter
names then, as opposed to locking it down now.

To maintain ergonomics/source compatibility, we could allow user to choose
arbitrary bindings for each associated value. The problem is it makes the
pattern deviate a lot from declaration and makes it hard for beginners to
understand. This also decrease readability for seasoned users.

Along the same line, a pattern that gets dropped is binding all associated
values as a labeled tuple, which tuple pattern allowed in Swift 3. As T.J.
Usiyan [pointed out][TJs comment], implementation of the equality protocol would
be simplified due to tuple's conformance to `Equatable`. This feature may still
be introduced with alternative syntax (perhaps related to splats) later without
source-breakage. And the need to implement `Equatable` may also disappear with
auto-deriving for `Equatable` conformance.

The previous revision of this proposal mandated that the labeled form of
sub-pattern (`case .elet(locals: let x, body: let y)`) be the only acceptable
pattern. Turns out the community considers this to be too verbose in some cases.

A drafted version of this proposal considered allowing "overloaded" declaration
of enum cases (same full-name, but with associated values with different types).
We ultimately decided that this feature is out of the scope of this proposal.

[SE-0155]: 0155-normalize-enum-case-representation.md
[SE-0111]: 0111-remove-arg-label-type-significance.md
[Daniel Duan]: https://github.com/dduan
[Joe Groff]: https://github.com/jckarter
[John McCall]: https://github.com/rjmccall
[TJs comment]: https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170116/030614.html
[Revision 1]: https://github.com/apple/swift-evolution/blob/43ca098355762014f53e1b54e02d2f6a01253385/proposals/0155-normalize-enum-case-representation.md
[Normalize Enum Case Representation (rev. 2)]: https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170306/033626.html


(Daniel Duan) #5

Thanks for the thoughtful feed Xiaodi! Replies are inline. I'm going to incorporate some of the responses into the proposal.

The rendered version differs from the text appended to your message. I'll assume the more fully fleshed out version is what you intend to submit. Three comments/questions:

Enum Case "Overloading"

An enum may contain cases with the same full name but with associated values of different types. For example:

enum Expr {
    case literal(Bool)
    case literal(Int)
}
The above cases have overloaded constructors, which follow the same rules as functions at call site for disambiguation:

// It's clear which case is being constructed in the following.
let aBool: Expr = .literal(false)
let anInt: Expr = .literal(42)
User must specify an as expression in sub-patterns in pattern matching, in order to match with such cases:

case .literal(let value) // this is ambiguous
case .literal(let value as Bool) // matches `case literal(Bool)`

Comment/question 1: Here, why aren't you proposing to allow `case .literal(let value: Bool)`? For one, it would seem to be more consistent.

The example in proposal doesn't include any labels. Are you suggesting two colons for sub-patterns with labels? Like `case .literal(value: let value: Bool)`? This looks jarring. But I'm definitely open to other suggestions.

Second, since we still have some use cases where there's Obj-C bridging magic with `as`, using `as` in this way may run into ambiguity issues if (for example) you have two cases, one with associated value of type `String` and the other of type `NSString`.

Either this should be rejected at declaration, or we need a way to accept a "pre-magic" resolution at pattern matching, when this scenarios is at hand. I'm on the phone so I can't verify. Wouldn't function overloading face a similar problem?

Also, since enum cases are to be like functions, I assume that the more verbose `as` version would work for free: `case .literal(let value) as (Bool) -> Expr`?

This is not being proposed. When a user sees/authors a case, their expectation for the declared case constructor should resemble that of a function. Pattern matching was considered separately since it's not relatable syntactically.

Alternative Payload-less Case Declaration

In Swift 3, the following syntax is valid:

enum Tree {
    case leaf() // the type of this constructor is confusing!
}
Tree.leaf has a very unexpected type to most Swift users: (()) -> Tree

We propose this syntax declare the "bare" case instead. So it's going to be the equivalent of

enum Tree {
    case leaf // `()` is optional and does the same thing.
}

Comment/question 2: First, if associated values are not to be modeled as tuples, for backwards compatibility the rare uses of `case leaf()` should be migrated to `case leaf(())`.

Yes, and when user uses a arbitrary name when they should have used a label, or when labels are misspelled, the compiler should suggest the correct labels. I wasn't sure how much of migrator related thing should go into a proposal. Perhaps there should be more.

Second, to be clear, you are _not_ proposing additional sugar so that a case without an associated value be equivalent to a case that has an associated value of type `Void`, correct? You are saying that, with your proposal, both `case leaf()` and `case leaf` would be regarded as being of type `() -> Tree` instead of the current `(()) -> Tree`?

Correct. I'm _not_ proposing implicit `Void`.

[The latter (i.e. `() -> Tree`) seems entirely fine. The former (i.e. additional sugar for `(()) -> Tree`) seems mostly fine, except that it would introduce an inconsistency with raw values that IMO is awkward. That is, if I have `enum Foo { case bar }`, it would make case `bar` have implied associated type `Void`; but, if I have `enum Foo: Int { case bar }`, would case `bar` have raw value `0` of type `Int` as well as associated value `()` of type `Void`?]

Pattern Consistency

(The following enum will be used throughout code snippets in this section).

indirect enum Expr {
    case variable(name: String)
    case lambda(parameters: [String], body: Expr)
}
Compared to patterns in Swift 3, matching against enum cases will follow stricter rules. This is a consequence of no longer relying on tuple patterns.

When an associated value has a label, the sub-pattern must include the label exactly as declared. There are two variants that should look familiar to Swift 3 users. Variant 1 allows user to bind the associated value to arbitrary name in the pattern by requiring the label:

case .variable(name: let x) // okay
case .variable(x: let x) // compile error; there's no label `x`
case .lambda(parameters: let params, body: let body) // Okay
case .lambda(params: let params, body: let body) // error: 1st label mismatches
User may choose not to use binding names that differ from labels. In this variant, the corresponding value will bind to the label, resulting in this shorter form:

case .variable(let name) // okay, because the name is the same as the label
case .lambda(let parameters, let body) // this is okay too, same reason.
case .variable(let x) // compiler error. label must appear one way or another.
case .lambda(let params, let body) // compiler error, same reason as above.
Comment/question 3: Being a source-breaking change, that requires extreme justification, and I just don't think there is one for this rule. The perceived problem being addressed (that one might try to bind `parameters` to `body` and `body` to `parameters`) is unchanged whether enum cases are modeled as tuples or functions, so aligning enum cases to functions is not in and of itself justification to revisit the issue of whether to try to prohibit this.

To reiterate, here patterns are changed not for any kind of "alignment" with function syntax. It changed because we dropped the tuple pattern (which remains available for matching with tuple values, btw), therefore we need to consider what a first-class syntax for enum case would look like.

The justification for this breaking change is this: with tuples, labels in pattern is not well enforced. User can skip them, bind value to totally arbitrary names, etc. I personally think emulating such rule prevents us from making pattern matching easier to read for experienced devs and easier to learn for new comers.

It's reasonable to expect existing patterns in the wild either already use the labels found in declaration or they are matching against label-less cases. In other words, existing code with good style won't be affected much. For the code that actually would break, I think the migrator and the compiler can provide sufficient help in form of migration/fixits/warnings.

Ultimately I think requiring appearance of labels one way or another in patterns will improve both the readability of the pattern matching site as well as forcing the author of case declaration consider the use site more.

In fact, I think the proposed solution suffers from two great weaknesses. First, it seems ad-hoc. Consider this: if enum cases are to be modeled as functions, then I should be able to write something intermediate between the options above; namely: `case .variable(name:)(let x)`. Since `.variable` unambiguously refers to `.variable(name:)`, I should also be allowed to write `.variable(let x)` just as I am now.

Again, patterns are not to be modeled after functions. Only the declaration and usage of case constructors are.

Second, it seems unduly restrictive. If, in the containing scope, I have a variable named `body` that I don't want to shadow, this rule would force me to either write the more verbose form or deal with shadowing `body`. If a person opts for the shorter form, they are choosing not to use the label.

In fact this (to avoid label conflict in nesting) is the only reason the longer form allows rebinding to other names at all! You say "unduly restrictive", I say "necessarily flexible" :slight_smile:

···

On Mar 8, 2017, at 9:56 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

Only one of these variants may appear in a single pattern. Swift compiler will raise a compile error for mixed usage.

case .lambda(parameters: let params, let body) // error, can not mix the two.
Some patterns will no longer match enum cases. For example, all associated values can bind as a tuple in Swift 3, this will no longer work after this proposal:

// deprecated: matching all associated values as a tuple
if case let .lambda(f) = anLambdaExpr {
    evaluateLambda(parameters: f.parameters, body: f.body)
}


(Xiaodi Wu) #6

Thanks for the thoughtful feed Xiaodi! Replies are inline. I'm going to
incorporate some of the responses into the proposal.

The rendered version differs from the text appended to your message. I'll
assume the more fully fleshed out version is what you intend to submit.
Three comments/questions:

Enum Case "Overloading"

An enum may contain cases with the same full name but with associated
values of different types. For example:

enum Expr {
    case literal(Bool)
    case literal(Int)
}

The above cases have overloaded constructors, which follow the same rules
as functions at call site for disambiguation:

// It's clear which case is being constructed in the following.let aBool: Expr = .literal(false)let anInt: Expr = .literal(42)

User must specify an as expression in sub-patterns in pattern matching,
in order to match with such cases:

case .literal(let value) // this is ambiguouscase .literal(let value as Bool) // matches `case literal(Bool)`

Comment/question 1: Here, why aren't you proposing to allow `case
.literal(let value: Bool)`? For one, it would seem to be more consistent.

The example in proposal doesn't include any labels. Are you suggesting two
colons for sub-patterns with labels? Like `case .literal(value: let value:
Bool)`? This looks jarring. But I'm definitely open to other suggestions.

That does look jarring. But hmm.

Second, since we still have some use cases where there's Obj-C bridging
magic with `as`, using `as` in this way may run into ambiguity issues if
(for example) you have two cases, one with associated value of type
`String` and the other of type `NSString`.

Either this should be rejected at declaration, or we need a way to accept
a "pre-magic" resolution at pattern matching, when this scenarios is at
hand.

Or we align pattern matching to function syntax and have such cases
disambiguated in that way (see below).

I'm on the phone so I can't verify. Wouldn't function overloading face a
similar problem?

Also, since enum cases are to be like functions, I assume that the more
verbose `as` version would work for free: `case .literal(let value) as
(Bool) -> Expr`?

This is not being proposed. When a user sees/authors a case, their
expectation for the declared case constructor should resemble that of a
function. Pattern matching was considered separately since it's not
relatable syntactically.

This requires justification. If enum cases are to be like functions, then
the logical expectation is that pattern matching should work in that way
too. I see no rationale to undergird your claim that pattern matching is
"not relatable syntactically." Allowing `case .literal(let value) as (Bool)
-> Expr` would solve the issue above, as well as provide more flexibility
with the issues below.

<https://github.com/dduan/swift-evolution/blob/SE0155-rev2/proposals/0155-normalize-enum-case-representation.md#alternative-payload-less-case-declaration>Alternative
Payload-less Case Declaration

In Swift 3, the following syntax is valid:

enum Tree {
    case leaf() // the type of this constructor is confusing!}

Tree.leaf has a very unexpected type to most Swift users: (()) -> Tree

We propose this syntax declare the "bare" case instead. So it's going to
be the equivalent of

enum Tree {
    case leaf // `()` is optional and does the same thing.}

Comment/question 2: First, if associated values are not to be modeled as
tuples, for backwards compatibility the rare uses of `case leaf()` should
be migrated to `case leaf(())`.

Yes,

Cool.

and when user uses a arbitrary name when they should have used a label, or
when labels are misspelled, the compiler should suggest the correct labels.

As below, I disagree with this restriction very strongly.

I wasn't sure how much of migrator related thing should go into a
proposal. Perhaps there should be more.

Second, to be clear, you are _not_ proposing additional sugar so that a
case without an associated value be equivalent to a case that has an
associated value of type `Void`, correct? You are saying that, with your
proposal, both `case leaf()` and `case leaf` would be regarded as being of
type `() -> Tree` instead of the current `(()) -> Tree`?

Correct. I'm _not_ proposing implicit `Void`.

[The latter (i.e. `() -> Tree`) seems entirely fine. The former (i.e.
additional sugar for `(()) -> Tree`) seems mostly fine, except that it
would introduce an inconsistency with raw values that IMO is awkward. That
is, if I have `enum Foo { case bar }`, it would make case `bar` have
implied associated type `Void`; but, if I have `enum Foo: Int { case bar
}`, would case `bar` have raw value `0` of type `Int` as well as associated
value `()` of type `Void`?]

<https://github.com/dduan/swift-evolution/blob/SE0155-rev2/proposals/0155-normalize-enum-case-representation.md#pattern-consistency>Pattern
Consistency

*(The following enum will be used throughout code snippets in this
section).*

indirect enum Expr {
    case variable(name: String)
    case lambda(parameters: [String], body: Expr)
}

Compared to patterns in Swift 3, matching against enum cases will follow
stricter rules. This is a consequence of no longer relying on tuple
patterns.

When an associated value has a label, the sub-pattern must include the
label exactly as declared. There are two variants that should look familiar
to Swift 3 users. Variant 1 allows user to bind the associated value to
arbitrary name in the pattern by requiring the label:

case .variable(name: let x) // okaycase .variable(x: let x) // compile error; there's no label `x`case .lambda(parameters: let params, body: let body) // Okaycase .lambda(params: let params, body: let body) // error: 1st label mismatches

User may choose not to use binding names that differ from labels. In this
variant, the corresponding value will bind to the label, resulting in this
shorter form:

case .variable(let name) // okay, because the name is the same as the labelcase .lambda(let parameters, let body) // this is okay too, same reason.case .variable(let x) // compiler error. label must appear one way or another.case .lambda(let params, let body) // compiler error, same reason as above.

Comment/question 3: Being a source-breaking change, that requires extreme
justification, and I just don't think there is one for this rule. The
perceived problem being addressed (that one might try to bind `parameters`
to `body` and `body` to `parameters`) is unchanged whether enum cases are
modeled as tuples or functions, so aligning enum cases to functions is not
in and of itself justification to revisit the issue of whether to try to
prohibit this.

To reiterate, here patterns are changed not for any kind of "alignment"
with function syntax. It changed because we dropped the tuple pattern
(which remains available for matching with tuple values, btw), therefore we
need to consider what a first-class syntax for enum case would look like.

Since the rationale for this proposal is to "normalize" enum cases by
making them more function-like, again you will need to justify why pattern
matching should break from that overarching goal.

This is a source-breaking change, so it's not enough that a "first-class
syntax" from the ground up would be different from the status quo (which
was the Swift 3 evolution standard--if we were to do it again from scratch,
would we still do it this way?). The Swift 4 evolution expectation is that
a source-breaking change should require "extreme" justification.

The justification for this breaking change is this: with tuples, labels in

pattern is not well enforced. User can skip them, bind value to totally
arbitrary names, etc. I personally think emulating such rule prevents us
from making pattern matching easier to read for experienced devs and easier
to learn for new comers.

Perhaps, but this is an argument that tuple pattern binding is inferior. It
has nothing to do with enum cases in particular. In fact, several threads
have touched on this topic holistically. The conclusions there have been
that allowing (a: Int, b: Int) to bind (Int, Int) or vice versa is healthy
and useful, but allowing (a: Int, b: Int) to bind (b: Int, c: Int) is not
so good, and (a: Int, b: Int) binding (b: Int, a: Int) is counterintuitive
and should be removed.

Emulating tuple rules is the backwards-compatible way. Therefore, it ought
to be the default unless the source-breaking alternative is not merely
easier but _overwhelmingly_ easier. If tuples are dangerously broken, then
that calls for a proposal to change tuples.

It's reasonable to expect existing patterns in the wild either already use

the labels found in declaration or they are matching against label-less
cases.

I don't think that is reasonable to expect.

In other words, existing code with good style won't be affected much.

You're defining your preference as "good style" and saying that a proposal
to enforce your preference by the compiler won't affect code with "good
style" (i.e. adhering to your preference), which is tautologically true and
also not a valid argument.

For the code that actually would break, I think the migrator and the
compiler can provide sufficient help in form of migration/fixits/warnings.

Ultimately I think requiring appearance of labels one way or another in
patterns will improve both the readability of the pattern matching site as
well as forcing the author of case declaration consider the use site more.

In fact, I think the proposed solution suffers from two great weaknesses. First,
it seems ad-hoc. Consider this: if enum cases are to be modeled as
functions, then I should be able to write something intermediate between
the options above; namely: `case .variable(name:)(let x)`. Since
`.variable` unambiguously refers to `.variable(name:)`, I should also be
allowed to write `.variable(let x)` just as I am now.

Again, patterns are not to be modeled after functions. Only the
declaration and usage of case constructors are.

This requires justification. Why are they not to be? IMO, they ought to be.

Second, it seems unduly restrictive. If, in the containing scope, I have a

variable named `body` that I don't want to shadow, this rule would force me
to either write the more verbose form or deal with shadowing `body`. If a
person opts for the shorter form, they are choosing not to use the label.

In fact this (to avoid label conflict in nesting) is the only reason the
longer form allows rebinding to other names at all! You say "unduly
restrictive", I say "necessarily flexible" :slight_smile:

The great majority of Swift users don't read this list, nor care about the
starting point from which a proposal added "flexibility." They compare
Swift N to Swift N + 1, and in this case, there is a new restriction. That
is the only comparison which matters. So make no mistake, you are proposing
a source-breaking restriction. For the arguments I gave in my earlier
reply, I believe it is _unduly_ restrictive.

···

On Thu, Mar 9, 2017 at 1:07 AM, Daniel Duan <daniel@duan.org> wrote:

On Mar 8, 2017, at 9:56 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

Only one of these variants may appear in a single pattern. Swift compiler
will raise a compile error for mixed usage.

case .lambda(parameters: let params, let body) // error, can not mix the two.

Some patterns will no longer match enum cases. For example, all associated
values can bind as a tuple in Swift 3, this will no longer work after this
proposal:

// deprecated: matching all associated values as a tupleif case let .lambda(f) = anLambdaExpr {
    evaluateLambda(parameters: f.parameters, body: f.body)
}

<https://github.com/dduan/swift-evolution/blob/SE0155-rev2/proposals/0155-normalize-enum-case-representation.md#source-compatibility>


(David Hart) #7

Thanks for the thoughtful feed Xiaodi! Replies are inline. I'm going to incorporate some of the responses into the proposal.

The rendered version differs from the text appended to your message. I'll assume the more fully fleshed out version is what you intend to submit. Three comments/questions:

Enum Case "Overloading"

An enum may contain cases with the same full name but with associated values of different types. For example:

enum Expr {
    case literal(Bool)
    case literal(Int)
}
The above cases have overloaded constructors, which follow the same rules as functions at call site for disambiguation:

// It's clear which case is being constructed in the following.
let aBool: Expr = .literal(false)
let anInt: Expr = .literal(42)
User must specify an as expression in sub-patterns in pattern matching, in order to match with such cases:

case .literal(let value) // this is ambiguous
case .literal(let value as Bool) // matches `case literal(Bool)`

Comment/question 1: Here, why aren't you proposing to allow `case .literal(let value: Bool)`? For one, it would seem to be more consistent.

The example in proposal doesn't include any labels. Are you suggesting two colons for sub-patterns with labels? Like `case .literal(value: let value: Bool)`? This looks jarring. But I'm definitely open to other suggestions.

No, I think he was proposing replicating variable declaration syntax, where the colon is used preceding the type (instead of using as):

let value: Bool = ...

···

On 9 Mar 2017, at 08:07, Daniel Duan via swift-evolution <swift-evolution@swift.org> wrote:
On Mar 8, 2017, at 9:56 PM, Xiaodi Wu <xiaodi.wu@gmail.com <mailto:xiaodi.wu@gmail.com>> wrote:

Second, since we still have some use cases where there's Obj-C bridging magic with `as`, using `as` in this way may run into ambiguity issues if (for example) you have two cases, one with associated value of type `String` and the other of type `NSString`.

Either this should be rejected at declaration, or we need a way to accept a "pre-magic" resolution at pattern matching, when this scenarios is at hand. I'm on the phone so I can't verify. Wouldn't function overloading face a similar problem?

Also, since enum cases are to be like functions, I assume that the more verbose `as` version would work for free: `case .literal(let value) as (Bool) -> Expr`?

This is not being proposed. When a user sees/authors a case, their expectation for the declared case constructor should resemble that of a function. Pattern matching was considered separately since it's not relatable syntactically.

<https://github.com/dduan/swift-evolution/blob/SE0155-rev2/proposals/0155-normalize-enum-case-representation.md#alternative-payload-less-case-declaration>Alternative Payload-less Case Declaration

In Swift 3, the following syntax is valid:

enum Tree {
    case leaf() // the type of this constructor is confusing!
}
Tree.leaf has a very unexpected type to most Swift users: (()) -> Tree

We propose this syntax declare the "bare" case instead. So it's going to be the equivalent of

enum Tree {
    case leaf // `()` is optional and does the same thing.
}

Comment/question 2: First, if associated values are not to be modeled as tuples, for backwards compatibility the rare uses of `case leaf()` should be migrated to `case leaf(())`.

Yes, and when user uses a arbitrary name when they should have used a label, or when labels are misspelled, the compiler should suggest the correct labels. I wasn't sure how much of migrator related thing should go into a proposal. Perhaps there should be more.

Second, to be clear, you are _not_ proposing additional sugar so that a case without an associated value be equivalent to a case that has an associated value of type `Void`, correct? You are saying that, with your proposal, both `case leaf()` and `case leaf` would be regarded as being of type `() -> Tree` instead of the current `(()) -> Tree`?

Correct. I'm _not_ proposing implicit `Void`.

[The latter (i.e. `() -> Tree`) seems entirely fine. The former (i.e. additional sugar for `(()) -> Tree`) seems mostly fine, except that it would introduce an inconsistency with raw values that IMO is awkward. That is, if I have `enum Foo { case bar }`, it would make case `bar` have implied associated type `Void`; but, if I have `enum Foo: Int { case bar }`, would case `bar` have raw value `0` of type `Int` as well as associated value `()` of type `Void`?]

<https://github.com/dduan/swift-evolution/blob/SE0155-rev2/proposals/0155-normalize-enum-case-representation.md#pattern-consistency>Pattern Consistency

(The following enum will be used throughout code snippets in this section).

indirect enum Expr {
    case variable(name: String)
    case lambda(parameters: [String], body: Expr)
}
Compared to patterns in Swift 3, matching against enum cases will follow stricter rules. This is a consequence of no longer relying on tuple patterns.

When an associated value has a label, the sub-pattern must include the label exactly as declared. There are two variants that should look familiar to Swift 3 users. Variant 1 allows user to bind the associated value to arbitrary name in the pattern by requiring the label:

case .variable(name: let x) // okay
case .variable(x: let x) // compile error; there's no label `x`
case .lambda(parameters: let params, body: let body) // Okay
case .lambda(params: let params, body: let body) // error: 1st label mismatches
User may choose not to use binding names that differ from labels. In this variant, the corresponding value will bind to the label, resulting in this shorter form:

case .variable(let name) // okay, because the name is the same as the label
case .lambda(let parameters, let body) // this is okay too, same reason.
case .variable(let x) // compiler error. label must appear one way or another.
case .lambda(let params, let body) // compiler error, same reason as above.
Comment/question 3: Being a source-breaking change, that requires extreme justification, and I just don't think there is one for this rule. The perceived problem being addressed (that one might try to bind `parameters` to `body` and `body` to `parameters`) is unchanged whether enum cases are modeled as tuples or functions, so aligning enum cases to functions is not in and of itself justification to revisit the issue of whether to try to prohibit this.

To reiterate, here patterns are changed not for any kind of "alignment" with function syntax. It changed because we dropped the tuple pattern (which remains available for matching with tuple values, btw), therefore we need to consider what a first-class syntax for enum case would look like.

The justification for this breaking change is this: with tuples, labels in pattern is not well enforced. User can skip them, bind value to totally arbitrary names, etc. I personally think emulating such rule prevents us from making pattern matching easier to read for experienced devs and easier to learn for new comers.

It's reasonable to expect existing patterns in the wild either already use the labels found in declaration or they are matching against label-less cases. In other words, existing code with good style won't be affected much. For the code that actually would break, I think the migrator and the compiler can provide sufficient help in form of migration/fixits/warnings.

Ultimately I think requiring appearance of labels one way or another in patterns will improve both the readability of the pattern matching site as well as forcing the author of case declaration consider the use site more.

In fact, I think the proposed solution suffers from two great weaknesses. First, it seems ad-hoc. Consider this: if enum cases are to be modeled as functions, then I should be able to write something intermediate between the options above; namely: `case .variable(name:)(let x)`. Since `.variable` unambiguously refers to `.variable(name:)`, I should also be allowed to write `.variable(let x)` just as I am now.

Again, patterns are not to be modeled after functions. Only the declaration and usage of case constructors are.

Second, it seems unduly restrictive. If, in the containing scope, I have a variable named `body` that I don't want to shadow, this rule would force me to either write the more verbose form or deal with shadowing `body`. If a person opts for the shorter form, they are choosing not to use the label.

In fact this (to avoid label conflict in nesting) is the only reason the longer form allows rebinding to other names at all! You say "unduly restrictive", I say "necessarily flexible" :slight_smile:

Only one of these variants may appear in a single pattern. Swift compiler will raise a compile error for mixed usage.

case .lambda(parameters: let params, let body) // error, can not mix the two.
Some patterns will no longer match enum cases. For example, all associated values can bind as a tuple in Swift 3, this will no longer work after this proposal:

// deprecated: matching all associated values as a tuple
if case let .lambda(f) = anLambdaExpr {
    evaluateLambda(parameters: f.parameters, body: f.body)
}

<https://github.com/dduan/swift-evolution/blob/SE0155-rev2/proposals/0155-normalize-enum-case-representation.md#source-compatibility>_______________________________________________

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


(Daniel Duan) #8

cc’ing Xiaodi

Hi everyone,

Here’s revision 2 of SE-0155. I’d love some feedback before going into re-review.

Note the “anonymous case” feature is not in this proposal. I found the motivation section difficult to write when it’s included. I’ve drafted a separate proposal that adds it.

Rendered version: https://github.com/dduan/swift-evolution/blob/SE0155-rev2/proposals/0155-normalize-enum-case-representation.md

You appear to have pasted a different version below from what's rendered at that link.

Some comments:

- Overloading seems like unnecessarily distracting scope creep. I would leave it out.

Great, this certainly solves a lot of unresolved problems being discussed here! :stuck_out_tongue:

- Having `case leaf()` have type Tree is IMO more surprising than making it equivalent to `case leaf`. It should either behave as if it were a method, or be banned outright. We don't need two ways to spell the same thing.

Sounds good!

···

On Mar 9, 2017, at 1:53 PM, Joe Groff <jgroff@apple.com> wrote:

On Mar 8, 2017, at 7:09 PM, Daniel Duan via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

-Joe


(David Hart) #9

Is there any plan to get this proposal reviewed? It would *really* improve the SwiftPM Package API.

···

On 9 Mar 2017, at 23:42, Daniel Duan via swift-evolution <swift-evolution@swift.org> wrote:

Here’s an updated version with hopefully the correct content. The “overloaded” case feature has been removed and is discussed in the "alternatives considered" section.

# Normalize Enum Case Representation

* Proposal: [SE-0155][]
* Authors: [Daniel Duan][], [Joe Groff][]
* Review Manager: [John McCall][]
* Status: **Awaiting review**
* Previous Revision: [1][Revision 1]

## Introduction

In Swift 3, associated values of an enum case are represented by a tuple. This
implementation causes inconsistencies in case declaration, construction and
pattern matching in several places.

Enums, therefore, can be made more "regular" when we replace tuple as the
representation of associated case values. This proposal aims to define the
effect of doings so on various parts of the language.

Swift-evolution thread: [Normalize Enum Case Representation (rev. 2)][]

## Motivation

When user declares a case for an enum, a function which constructs the
corresponding case value is declared. We'll refer to such functions as _case
constructors_ in this proposal.

enum Expr {
   // this case declares the case constructor `Expr.elet(_:_:)`
   indirect case elet(locals: [(String, Expr)], body: Expr)
}

// f's signature is f(_: _), type is ([(String, Expr)], Expr) -> Expr
let f = Expr.elet

// `f` is just a function
f([], someExpr) // construct a `Expr.elet`

There are many surprising aspects of enum constructors, however:

1. After [SE-0111][], Swift function's fully qualified name consists of its base
  name and all of its argument labels. User can use the full name of the
  function at use site. In the example above, `locals` and `body` are currently
  not part of the case constructors name, therefore the expected syntax is
  invalid.

  func f(x: Int, y: Int) {}
  f(x: y:)(0, 0) // Okay, this is equivalent to f(x: 0, y: 0)
  Expr.elet(locals: body:)([], someExpr) // this doesn't work in Swift 3

2. Case constructors cannot include a default value for each parameter. This
  is yet another feature available to functions.

As previous mentioned, these are symptoms of associated values being a tuple
instead of having its own distinct semantics. This problem manifests more in
Swift 3's pattern matching:

1. A pattern with a single value would match and result in a tuple:

   // this works for reasons most user probably don't expect!
   if case .elet(let wat) = anExpr {
       eval(wat.body)
   }

2. Labels in patterns are not enforced:

   // note: there's no label in the first sub-pattern
   if case .elet(let p, let body: q) = anExpr {
       // code
   }

These extra rules makes pattern matching difficult to teach and to expand to
other types.

## Proposed Solution

We'll add first class syntax (which largely resemble the syntax in Swift 3) for
declaring associated values with labels. Tuple will no longer be used to
represent the aggregate of associated values for an enum case. This means
pattern matching for enum cases needs its own syntax as well (as opposed to
piggybacking on tuple patterns, which remains in the language for tuples.).

## Detailed Design

### Compound Names For Enum Constructors

Associated values' labels should be part of the enum case's constructor name.
When constructing an enum value with the case name, label names must either be
supplied in the argument list it self, or as part of the full name.

Expr.elet(locals: [], body: anExpr) // Okay, the Swift 3 way.
Expr.elet(locals: body:)([], anExpr) // Okay, equivalent to the previous line.
Expr.elet(locals: body:)(locals: 0, body: 0) // This would be an error, however.

Note that since the labels aren't part of a tuple, they no longer participate in
type checking, behaving consistently with functions.

let f = Expr.elet // f has type ([(String, Expr)], Expr) -> Expr
f([], anExpr) // Okay!
f(locals: [], body: anExpr) // Won't compile.

Enum cases should have distinct *full* names. Therefore, shared base name will
be allowed:

enum SyntaxTree {
   case type(variables: [TypeVariable])
   case type(instantiated: [Type])
}

Using only the base name in pattern matching for the previous example would be
ambiguous and result in an compile error. In this case, the full name must be
supplied to disambiguate.

case .type // error: ambiguous
case .type(variables: let variables) // Okay

### Default Parameter Values For Enum Constructors

From a user's point view, declaring an enum case should remain the same as Swift
3 except now it's possible to add `= expression` after the type of an
associated value to convey a default value for that field.

enum Animation {
   case fadeIn(duration: TimeInterval = 0.3) // Okay!
}
let anim = Animation.fadeIn() // Great!

Updated syntax:

union-style-enum-case = enum-case-name [enum-case-associated-value-clause];
enum-case-associated-value-clause = "(" ")"
                                 > "(" enum-case-associated-value-list ")";
enum-case-associated-value-list = enum-associated-value-element
                               > enum-associated-value-element ","
                                 enum-case-associated-value-list;
enum-case-associated-value-element = element-name type-annotation
                                    [enum-case-element-default-value-clause]
                                  > type
                                    [enum-case-element-default-value-clause];
element-name = identifier;
enum-case-element-default-value-clause = "=" expression;

### Alternative Payload-less Case Declaration

In Swift 3, the following syntax is valid:

enum Tree {
   case leaf() // the type of this constructor is confusing!
}

`Tree.leaf` has a very unexpected type to most Swift users: `(()) -> Tree`

We propose this syntax become illegal. User must explicitly declare
associated value of type `Void` if needed:

enum Tree {
   case leaf(Void)
}

### Pattern Consistency

*(The following enum will be used throughout code snippets in this section).*

indirect enum Expr {
   case variable(name: String)
   case lambda(parameters: [String], body: Expr)
}

Compared to patterns in Swift 3, matching against enum cases will follow
stricter rules. This is a consequence of no longer relying on tuple patterns.

When an associated value has a label, the sub-pattern must include the label
exactly as declared. There are two variants that should look familiar to Swift
3 users. Variant 1 allows user to bind the associated value to arbitrary name in
the pattern by requiring the label:

case .variable(name: let x) // okay
case .variable(x: let x) // compile error; there's no label `x`
case .lambda(parameters: let params, body: let body) // Okay
case .lambda(params: let params, body: let body) // error: 1st label mismatches

User may choose not to use binding names that differ from labels. In this
variant, the corresponding value will bind to the label, resulting in this
shorter form:

case .variable(let name) // okay, because the name is the same as the label
case .lambda(let parameters, let body) // this is okay too, same reason.
case .variable(let x) // compiler error. label must appear one way or another.
case .lambda(let params, let body) // compiler error, same reason as above.

Only one of these variants may appear in a single pattern. Swift compiler will
raise a compile error for mixed usage.

case .lambda(parameters: let params, let body) // error, can not mix the two.

Some patterns will no longer match enum cases. For example, all associated
values can bind as a tuple in Swift 3, this will no longer work after this
proposal:

// deprecated: matching all associated values as a tuple
if case let .lambda(f) = anLambdaExpr {
   evaluateLambda(parameters: f.parameters, body: f.body)
}

## Source compatibility

Despite a few additions, case declaration remain mostly source-compatible with
Swift 3, with the exception of the change detailed in "Alternative Payload-less
Case Declaration".

Syntax for case constructor at use site remain source-compatible.

A large portion of pattern matching syntax for enum cases with associated values
remain unchanged. But patterns for matching all values as a tuple, patterns that
elide the label and binds to names that differ from the labels, patterns that
include labels for some sub-patterns but the rest of them are deprecated by this
proposal. Therefore this is a source breaking change.

## Effect on ABI stability and resilience

After this proposal, enum cases may have compound names. This means the standard
library will expose different symbols for enum constructors. The name mangling
rules should also change accordingly.

## Alternative Considered

Between case declaration and pattern matching, there exist many reasonable
combinations of improvement. On one hand, we can optimize for consistency,
simplicity and teachability by bringing in as much similarity between enum and
other part of the language as possible. Many decisions in the first revision
were made in favor if doing so. Through the feedbacks from swift-evolution, we
found that some of the changes impedes the ergonomics of these features too much
. In this section, we describe some of the alternatives that were raised and
rejected in hope to strike a balance between the two end of the goals.

We discussed allowing user to declare a *parameter name* ("internal names")
for each associated value. Such names may be used in various rules in pattern
matching. Some feedback suggested they maybe used as property names when we
make enum case subtypes of the enum and resembles a struct. This feature is not
included in this proposal because parameter names are not very useful *today*.
Using them in patterns actually don't improve consistency as users don't use
them outside normal function definitions at all. If enum case gains a function
body in a future proposal, it'd be better to define the semantics of parameter
names then, as opposed to locking it down now.

To maintain ergonomics/source compatibility, we could allow user to choose
arbitrary bindings for each associated value. The problem is it makes the
pattern deviate a lot from declaration and makes it hard for beginners to
understand. This also decrease readability for seasoned users.

Along the same line, a pattern that gets dropped is binding all associated
values as a labeled tuple, which tuple pattern allowed in Swift 3. As T.J.
Usiyan [pointed out][TJs comment], implementation of the equality protocol would
be simplified due to tuple's conformance to `Equatable`. This feature may still
be introduced with alternative syntax (perhaps related to splats) later without
source-breakage. And the need to implement `Equatable` may also disappear with
auto-deriving for `Equatable` conformance.

The previous revision of this proposal mandated that the labeled form of
sub-pattern (`case .elet(locals: let x, body: let y)`) be the only acceptable
pattern. Turns out the community considers this to be too verbose in some cases.

A drafted version of this proposal considered allowing "overloaded" declaration
of enum cases (same full-name, but with associated values with different types).
We ultimately decided that this feature is out of the scope of this proposal.

[SE-0155]: 0155-normalize-enum-case-representation.md
[SE-0111]: 0111-remove-arg-label-type-significance.md
[Daniel Duan]: https://github.com/dduan
[Joe Groff]: https://github.com/jckarter
[John McCall]: https://github.com/rjmccall
[TJs comment]: https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170116/030614.html
[Revision 1]: https://github.com/apple/swift-evolution/blob/43ca098355762014f53e1b54e02d2f6a01253385/proposals/0155-normalize-enum-case-representation.md
[Normalize Enum Case Representation (rev. 2)]: https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170306/033626.html

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


(Daniel Duan) #10

Thanks for the thoughtful feed Xiaodi! Replies are inline. I'm going to incorporate some of the responses into the proposal.

The rendered version differs from the text appended to your message. I'll assume the more fully fleshed out version is what you intend to submit. Three comments/questions:

Enum Case "Overloading"

An enum may contain cases with the same full name but with associated values of different types. For example:

enum Expr {
    case literal(Bool)
    case literal(Int)
}
The above cases have overloaded constructors, which follow the same rules as functions at call site for disambiguation:

// It's clear which case is being constructed in the following.
let aBool: Expr = .literal(false)
let anInt: Expr = .literal(42)
User must specify an as expression in sub-patterns in pattern matching, in order to match with such cases:

case .literal(let value) // this is ambiguous
case .literal(let value as Bool) // matches `case literal(Bool)`

Comment/question 1: Here, why aren't you proposing to allow `case .literal(let value: Bool)`? For one, it would seem to be more consistent.

The example in proposal doesn't include any labels. Are you suggesting two colons for sub-patterns with labels? Like `case .literal(value: let value: Bool)`? This looks jarring. But I'm definitely open to other suggestions.

No, I think he was proposing replicating variable declaration syntax, where the colon is used preceding the type (instead of using as):

let value: Bool = ...

We'd still need a way for user to bind a matching value to a name other than the label... I do realize that this would not be a problem if we drop the mandate that label must appear _somewhere_ in the pattern. Hmmm

···

On Mar 9, 2017, at 12:06 AM, David Hart <david@hartbit.com> wrote:

On 9 Mar 2017, at 08:07, Daniel Duan via swift-evolution <swift-evolution@swift.org> wrote:

On Mar 8, 2017, at 9:56 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

Second, since we still have some use cases where there's Obj-C bridging magic with `as`, using `as` in this way may run into ambiguity issues if (for example) you have two cases, one with associated value of type `String` and the other of type `NSString`.

Either this should be rejected at declaration, or we need a way to accept a "pre-magic" resolution at pattern matching, when this scenarios is at hand. I'm on the phone so I can't verify. Wouldn't function overloading face a similar problem?

Also, since enum cases are to be like functions, I assume that the more verbose `as` version would work for free: `case .literal(let value) as (Bool) -> Expr`?

This is not being proposed. When a user sees/authors a case, their expectation for the declared case constructor should resemble that of a function. Pattern matching was considered separately since it's not relatable syntactically.

Alternative Payload-less Case Declaration

In Swift 3, the following syntax is valid:

enum Tree {
    case leaf() // the type of this constructor is confusing!
}
Tree.leaf has a very unexpected type to most Swift users: (()) -> Tree

We propose this syntax declare the "bare" case instead. So it's going to be the equivalent of

enum Tree {
    case leaf // `()` is optional and does the same thing.
}

Comment/question 2: First, if associated values are not to be modeled as tuples, for backwards compatibility the rare uses of `case leaf()` should be migrated to `case leaf(())`.

Yes, and when user uses a arbitrary name when they should have used a label, or when labels are misspelled, the compiler should suggest the correct labels. I wasn't sure how much of migrator related thing should go into a proposal. Perhaps there should be more.

Second, to be clear, you are _not_ proposing additional sugar so that a case without an associated value be equivalent to a case that has an associated value of type `Void`, correct? You are saying that, with your proposal, both `case leaf()` and `case leaf` would be regarded as being of type `() -> Tree` instead of the current `(()) -> Tree`?

Correct. I'm _not_ proposing implicit `Void`.

[The latter (i.e. `() -> Tree`) seems entirely fine. The former (i.e. additional sugar for `(()) -> Tree`) seems mostly fine, except that it would introduce an inconsistency with raw values that IMO is awkward. That is, if I have `enum Foo { case bar }`, it would make case `bar` have implied associated type `Void`; but, if I have `enum Foo: Int { case bar }`, would case `bar` have raw value `0` of type `Int` as well as associated value `()` of type `Void`?]

Pattern Consistency

(The following enum will be used throughout code snippets in this section).

indirect enum Expr {
    case variable(name: String)
    case lambda(parameters: [String], body: Expr)
}
Compared to patterns in Swift 3, matching against enum cases will follow stricter rules. This is a consequence of no longer relying on tuple patterns.

When an associated value has a label, the sub-pattern must include the label exactly as declared. There are two variants that should look familiar to Swift 3 users. Variant 1 allows user to bind the associated value to arbitrary name in the pattern by requiring the label:

case .variable(name: let x) // okay
case .variable(x: let x) // compile error; there's no label `x`
case .lambda(parameters: let params, body: let body) // Okay
case .lambda(params: let params, body: let body) // error: 1st label mismatches
User may choose not to use binding names that differ from labels. In this variant, the corresponding value will bind to the label, resulting in this shorter form:

case .variable(let name) // okay, because the name is the same as the label
case .lambda(let parameters, let body) // this is okay too, same reason.
case .variable(let x) // compiler error. label must appear one way or another.
case .lambda(let params, let body) // compiler error, same reason as above.
Comment/question 3: Being a source-breaking change, that requires extreme justification, and I just don't think there is one for this rule. The perceived problem being addressed (that one might try to bind `parameters` to `body` and `body` to `parameters`) is unchanged whether enum cases are modeled as tuples or functions, so aligning enum cases to functions is not in and of itself justification to revisit the issue of whether to try to prohibit this.

To reiterate, here patterns are changed not for any kind of "alignment" with function syntax. It changed because we dropped the tuple pattern (which remains available for matching with tuple values, btw), therefore we need to consider what a first-class syntax for enum case would look like.

The justification for this breaking change is this: with tuples, labels in pattern is not well enforced. User can skip them, bind value to totally arbitrary names, etc. I personally think emulating such rule prevents us from making pattern matching easier to read for experienced devs and easier to learn for new comers.

It's reasonable to expect existing patterns in the wild either already use the labels found in declaration or they are matching against label-less cases. In other words, existing code with good style won't be affected much. For the code that actually would break, I think the migrator and the compiler can provide sufficient help in form of migration/fixits/warnings.

Ultimately I think requiring appearance of labels one way or another in patterns will improve both the readability of the pattern matching site as well as forcing the author of case declaration consider the use site more.

In fact, I think the proposed solution suffers from two great weaknesses. First, it seems ad-hoc. Consider this: if enum cases are to be modeled as functions, then I should be able to write something intermediate between the options above; namely: `case .variable(name:)(let x)`. Since `.variable` unambiguously refers to `.variable(name:)`, I should also be allowed to write `.variable(let x)` just as I am now.

Again, patterns are not to be modeled after functions. Only the declaration and usage of case constructors are.

Second, it seems unduly restrictive. If, in the containing scope, I have a variable named `body` that I don't want to shadow, this rule would force me to either write the more verbose form or deal with shadowing `body`. If a person opts for the shorter form, they are choosing not to use the label.

In fact this (to avoid label conflict in nesting) is the only reason the longer form allows rebinding to other names at all! You say "unduly restrictive", I say "necessarily flexible" :slight_smile:

Only one of these variants may appear in a single pattern. Swift compiler will raise a compile error for mixed usage.

case .lambda(parameters: let params, let body) // error, can not mix the two.
Some patterns will no longer match enum cases. For example, all associated values can bind as a tuple in Swift 3, this will no longer work after this proposal:

// deprecated: matching all associated values as a tuple
if case let .lambda(f) = anLambdaExpr {
    evaluateLambda(parameters: f.parameters, body: f.body)
}

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


(Matthew Johnson) #11

Thanks for the thoughtful feed Xiaodi! Replies are inline. I'm going to incorporate some of the responses into the proposal.

The rendered version differs from the text appended to your message. I'll assume the more fully fleshed out version is what you intend to submit. Three comments/questions:

Enum Case "Overloading"

An enum may contain cases with the same full name but with associated values of different types. For example:

enum Expr {
    case literal(Bool)
    case literal(Int)
}
The above cases have overloaded constructors, which follow the same rules as functions at call site for disambiguation:

// It's clear which case is being constructed in the following.
let aBool: Expr = .literal(false)
let anInt: Expr = .literal(42)
User must specify an as expression in sub-patterns in pattern matching, in order to match with such cases:

case .literal(let value) // this is ambiguous
case .literal(let value as Bool) // matches `case literal(Bool)`

Comment/question 1: Here, why aren't you proposing to allow `case .literal(let value: Bool)`? For one, it would seem to be more consistent.

The example in proposal doesn't include any labels. Are you suggesting two colons for sub-patterns with labels? Like `case .literal(value: let value: Bool)`? This looks jarring. But I'm definitely open to other suggestions.

That does look jarring. But hmm.

Second, since we still have some use cases where there's Obj-C bridging magic with `as`, using `as` in this way may run into ambiguity issues if (for example) you have two cases, one with associated value of type `String` and the other of type `NSString`.

Either this should be rejected at declaration, or we need a way to accept a "pre-magic" resolution at pattern matching, when this scenarios is at hand.

Or we align pattern matching to function syntax and have such cases disambiguated in that way (see below).

I'm on the phone so I can't verify. Wouldn't function overloading face a similar problem?

Also, since enum cases are to be like functions, I assume that the more verbose `as` version would work for free: `case .literal(let value) as (Bool) -> Expr`?

This is not being proposed. When a user sees/authors a case, their expectation for the declared case constructor should resemble that of a function. Pattern matching was considered separately since it's not relatable syntactically.

This requires justification. If enum cases are to be like functions, then the logical expectation is that pattern matching should work in that way too. I see no rationale to undergird your claim that pattern matching is "not relatable syntactically." Allowing `case .literal(let value) as (Bool) -> Expr` would solve the issue above, as well as provide more flexibility with the issues below.

Alternative Payload-less Case Declaration

In Swift 3, the following syntax is valid:

enum Tree {
    case leaf() // the type of this constructor is confusing!
}
Tree.leaf has a very unexpected type to most Swift users: (()) -> Tree

We propose this syntax declare the "bare" case instead. So it's going to be the equivalent of

enum Tree {
    case leaf // `()` is optional and does the same thing.
}

Comment/question 2: First, if associated values are not to be modeled as tuples, for backwards compatibility the rare uses of `case leaf()` should be migrated to `case leaf(())`.

Yes,

Cool.

and when user uses a arbitrary name when they should have used a label, or when labels are misspelled, the compiler should suggest the correct labels.

As below, I disagree with this restriction very strongly.

I wasn't sure how much of migrator related thing should go into a proposal. Perhaps there should be more.

Second, to be clear, you are _not_ proposing additional sugar so that a case without an associated value be equivalent to a case that has an associated value of type `Void`, correct? You are saying that, with your proposal, both `case leaf()` and `case leaf` would be regarded as being of type `() -> Tree` instead of the current `(()) -> Tree`?

Correct. I'm _not_ proposing implicit `Void`.

[The latter (i.e. `() -> Tree`) seems entirely fine. The former (i.e. additional sugar for `(()) -> Tree`) seems mostly fine, except that it would introduce an inconsistency with raw values that IMO is awkward. That is, if I have `enum Foo { case bar }`, it would make case `bar` have implied associated type `Void`; but, if I have `enum Foo: Int { case bar }`, would case `bar` have raw value `0` of type `Int` as well as associated value `()` of type `Void`?]

Pattern Consistency

(The following enum will be used throughout code snippets in this section).

indirect enum Expr {
    case variable(name: String)
    case lambda(parameters: [String], body: Expr)
}
Compared to patterns in Swift 3, matching against enum cases will follow stricter rules. This is a consequence of no longer relying on tuple patterns.

When an associated value has a label, the sub-pattern must include the label exactly as declared. There are two variants that should look familiar to Swift 3 users. Variant 1 allows user to bind the associated value to arbitrary name in the pattern by requiring the label:

case .variable(name: let x) // okay
case .variable(x: let x) // compile error; there's no label `x`
case .lambda(parameters: let params, body: let body) // Okay
case .lambda(params: let params, body: let body) // error: 1st label mismatches
User may choose not to use binding names that differ from labels. In this variant, the corresponding value will bind to the label, resulting in this shorter form:

case .variable(let name) // okay, because the name is the same as the label
case .lambda(let parameters, let body) // this is okay too, same reason.
case .variable(let x) // compiler error. label must appear one way or another.
case .lambda(let params, let body) // compiler error, same reason as above.
Comment/question 3: Being a source-breaking change, that requires extreme justification, and I just don't think there is one for this rule. The perceived problem being addressed (that one might try to bind `parameters` to `body` and `body` to `parameters`) is unchanged whether enum cases are modeled as tuples or functions, so aligning enum cases to functions is not in and of itself justification to revisit the issue of whether to try to prohibit this.

To reiterate, here patterns are changed not for any kind of "alignment" with function syntax. It changed because we dropped the tuple pattern (which remains available for matching with tuple values, btw), therefore we need to consider what a first-class syntax for enum case would look like.

Since the rationale for this proposal is to "normalize" enum cases by making them more function-like, again you will need to justify why pattern matching should break from that overarching goal.

I have argued repeatedly that enum cases play two distinct roles (and sometimes a third in the future). We need to embrace all of these roles. This proposal primarily normalizes the role of case-as-factory-method.

Pattern matching is a second role enum cases play. We already have syntax for patterns that match based on type. I see no reason we should introduce new matching syntax to disambiguate overloaded cases. The existing patterns work well for this purpose. Matching should be considered a role played by enum cases that is completely orthogonal to their role as a factory method.

The third role some cases will (hopefully) play in the future is as a subtype of the enum itself. All three roles are distinct and cases should align with other parts of the language when playing that role.

This is a source-breaking change, so it's not enough that a "first-class syntax" from the ground up would be different from the status quo (which was the Swift 3 evolution standard--if we were to do it again from scratch, would we still do it this way?). The Swift 4 evolution expectation is that a source-breaking change should require "extreme" justification.

The justification for this breaking change is this: with tuples, labels in pattern is not well enforced. User can skip them, bind value to totally arbitrary names, etc. I personally think emulating such rule prevents us from making pattern matching easier to read for experienced devs and easier to learn for new comers.

Perhaps, but this is an argument that tuple pattern binding is inferior. It has nothing to do with enum cases in particular. In fact, several threads have touched on this topic holistically. The conclusions there have been that allowing (a: Int, b: Int) to bind (Int, Int) or vice versa is healthy and useful, but allowing (a: Int, b: Int) to bind (b: Int, c: Int) is not so good, and (a: Int, b: Int) binding (b: Int, a: Int) is counterintuitive and should be removed.

Emulating tuple rules is the backwards-compatible way. Therefore, it ought to be the default unless the source-breaking alternative is not merely easier but _overwhelmingly_ easier. If tuples are dangerously broken, then that calls for a proposal to change tuples.

It's reasonable to expect existing patterns in the wild either already use the labels found in declaration or they are matching against label-less cases.

I don't think that is reasonable to expect.

I agree. I have code that will break.

In other words, existing code with good style won't be affected much.

You're defining your preference as "good style" and saying that a proposal to enforce your preference by the compiler won't affect code with "good style" (i.e. adhering to your preference), which is tautologically true and also not a valid argument.

I agree here as well. Labels in patterns can end up creating a lot of verbosity that makes code less clear. This is why I have argued for the internal / external name distinction and allowing the label to be elided by binding a name that matches the label or the "internal" name if it exists. This would likely break a lot less code as many existing bindings that omit labels probably qualify under this rule.

I don't consider this problematic enough to oppose the proposal. I like everything else about it and this topic could be revisited in the future.

For the code that actually would break, I think the migrator and the compiler can provide sufficient help in form of migration/fixits/warnings.

Ultimately I think requiring appearance of labels one way or another in patterns will improve both the readability of the pattern matching site as well as forcing the author of case declaration consider the use site more.

In fact, I think the proposed solution suffers from two great weaknesses. First, it seems ad-hoc. Consider this: if enum cases are to be modeled as functions, then I should be able to write something intermediate between the options above; namely: `case .variable(name:)(let x)`. Since `.variable` unambiguously refers to `.variable(name:)`, I should also be allowed to write `.variable(let x)` just as I am now.

Again, patterns are not to be modeled after functions. Only the declaration and usage of case constructors are.

This requires justification. Why are they not to be? IMO, they ought to be.

See above about the distinct roles enum cases play in the language. Why should we try to force the perspective of one of these roles (factory method) onto another (pattern matching)? That is what requires a lot of justification IMO.

···

Sent from my iPad

On Mar 9, 2017, at 2:31 AM, Xiaodi Wu via swift-evolution <swift-evolution@swift.org> wrote:

On Thu, Mar 9, 2017 at 1:07 AM, Daniel Duan <daniel@duan.org> wrote:

On Mar 8, 2017, at 9:56 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

Second, it seems unduly restrictive. If, in the containing scope, I have a variable named `body` that I don't want to shadow, this rule would force me to either write the more verbose form or deal with shadowing `body`. If a person opts for the shorter form, they are choosing not to use the label.

In fact this (to avoid label conflict in nesting) is the only reason the longer form allows rebinding to other names at all! You say "unduly restrictive", I say "necessarily flexible" :slight_smile:

The great majority of Swift users don't read this list, nor care about the starting point from which a proposal added "flexibility." They compare Swift N to Swift N + 1, and in this case, there is a new restriction. That is the only comparison which matters. So make no mistake, you are proposing a source-breaking restriction. For the arguments I gave in my earlier reply, I believe it is _unduly_ restrictive.

Only one of these variants may appear in a single pattern. Swift compiler will raise a compile error for mixed usage.

case .lambda(parameters: let params, let body) // error, can not mix the two.
Some patterns will no longer match enum cases. For example, all associated values can bind as a tuple in Swift 3, this will no longer work after this proposal:

// deprecated: matching all associated values as a tuple
if case let .lambda(f) = anLambdaExpr {
    evaluateLambda(parameters: f.parameters, body: f.body)
}

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


(Daniel Duan) #12

I have concerns about the verbosity this syntax introduces. Example:

enum A { case v(Int) }
enum B { case v(A); case v(Int) }

To disambiguate a value of type B, it would be

case .v(A.v(let xValue)) as ((Int -> A) -> B)

This scales poorly for cases with deeper recursions and/or more associated values.

Disambiguate at the sub-pattern level doesn’t have this scalability problem.

···

On Mar 9, 2017, at 12:31 AM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Thu, Mar 9, 2017 at 1:07 AM, Daniel Duan <daniel@duan.org <mailto:daniel@duan.org>> wrote:
Thanks for the thoughtful feed Xiaodi! Replies are inline. I'm going to incorporate some of the responses into the proposal.

On Mar 8, 2017, at 9:56 PM, Xiaodi Wu <xiaodi.wu@gmail.com <mailto:xiaodi.wu@gmail.com>> wrote:

The rendered version differs from the text appended to your message. I'll assume the more fully fleshed out version is what you intend to submit. Three comments/questions:

Enum Case "Overloading"

An enum may contain cases with the same full name but with associated values of different types. For example:

enum Expr {
    case literal(Bool)
    case literal(Int)
}
The above cases have overloaded constructors, which follow the same rules as functions at call site for disambiguation:

// It's clear which case is being constructed in the following.
let aBool: Expr = .literal(false)
let anInt: Expr = .literal(42)
User must specify an as expression in sub-patterns in pattern matching, in order to match with such cases:

case .literal(let value) // this is ambiguous
case .literal(let value as Bool) // matches `case literal(Bool)`

Comment/question 1: Here, why aren't you proposing to allow `case .literal(let value: Bool)`? For one, it would seem to be more consistent.

The example in proposal doesn't include any labels. Are you suggesting two colons for sub-patterns with labels? Like `case .literal(value: let value: Bool)`? This looks jarring. But I'm definitely open to other suggestions.

That does look jarring. But hmm.

Second, since we still have some use cases where there's Obj-C bridging magic with `as`, using `as` in this way may run into ambiguity issues if (for example) you have two cases, one with associated value of type `String` and the other of type `NSString`.

Either this should be rejected at declaration, or we need a way to accept a "pre-magic" resolution at pattern matching, when this scenarios is at hand.

Or we align pattern matching to function syntax and have such cases disambiguated in that way (see below).

I'm on the phone so I can't verify. Wouldn't function overloading face a similar problem?

Also, since enum cases are to be like functions, I assume that the more verbose `as` version would work for free: `case .literal(let value) as (Bool) -> Expr`?

This is not being proposed. When a user sees/authors a case, their expectation for the declared case constructor should resemble that of a function. Pattern matching was considered separately since it's not relatable syntactically.

This requires justification. If enum cases are to be like functions, then the logical expectation is that pattern matching should work in that way too. I see no rationale to undergird your claim that pattern matching is "not relatable syntactically." Allowing `case .literal(let value) as (Bool) -> Expr` would solve the issue above, as well as provide more flexibility with the issues below.

We’ve encountered a bigger question that it initial seems. Let’s zoom out.

There are 2 popular kinds of patterns for value deconstruction in PLs: patterns for trees and sequences. The former deconstructs value who’s prominently recursive: enum, struct, tuple; the latter deals with list-like (grows in 1 direction indefinitely) things. We are now investigating the syntax that can potentially be used for all tree patterns. Whereas the “shape” alone isn’t enough information, user must use the type to supplement the pattern for a successful match. If we introduce patterns for structs in the future, whatever we came up here for type disambiguation should work there.

<https://github.com/dduan/swift-evolution/blob/SE0155-rev2/proposals/0155-normalize-enum-case-representation.md#alternative-payload-less-case-declaration>Alternative Payload-less Case Declaration

In Swift 3, the following syntax is valid:

enum Tree {
    case leaf() // the type of this constructor is confusing!
}
Tree.leaf has a very unexpected type to most Swift users: (()) -> Tree

We propose this syntax declare the "bare" case instead. So it's going to be the equivalent of

enum Tree {
    case leaf // `()` is optional and does the same thing.
}

Comment/question 2: First, if associated values are not to be modeled as tuples, for backwards compatibility the rare uses of `case leaf()` should be migrated to `case leaf(())`.

Yes,

Cool.

and when user uses a arbitrary name when they should have used a label, or when labels are misspelled, the compiler should suggest the correct labels.

As below, I disagree with this restriction very strongly.

I wasn't sure how much of migrator related thing should go into a proposal. Perhaps there should be more.

Second, to be clear, you are _not_ proposing additional sugar so that a case without an associated value be equivalent to a case that has an associated value of type `Void`, correct? You are saying that, with your proposal, both `case leaf()` and `case leaf` would be regarded as being of type `() -> Tree` instead of the current `(()) -> Tree`?

Correct. I'm _not_ proposing implicit `Void`.

[The latter (i.e. `() -> Tree`) seems entirely fine. The former (i.e. additional sugar for `(()) -> Tree`) seems mostly fine, except that it would introduce an inconsistency with raw values that IMO is awkward. That is, if I have `enum Foo { case bar }`, it would make case `bar` have implied associated type `Void`; but, if I have `enum Foo: Int { case bar }`, would case `bar` have raw value `0` of type `Int` as well as associated value `()` of type `Void`?]

<https://github.com/dduan/swift-evolution/blob/SE0155-rev2/proposals/0155-normalize-enum-case-representation.md#pattern-consistency>Pattern Consistency

(The following enum will be used throughout code snippets in this section).

indirect enum Expr {
    case variable(name: String)
    case lambda(parameters: [String], body: Expr)
}
Compared to patterns in Swift 3, matching against enum cases will follow stricter rules. This is a consequence of no longer relying on tuple patterns.

When an associated value has a label, the sub-pattern must include the label exactly as declared. There are two variants that should look familiar to Swift 3 users. Variant 1 allows user to bind the associated value to arbitrary name in the pattern by requiring the label:

case .variable(name: let x) // okay
case .variable(x: let x) // compile error; there's no label `x`
case .lambda(parameters: let params, body: let body) // Okay
case .lambda(params: let params, body: let body) // error: 1st label mismatches
User may choose not to use binding names that differ from labels. In this variant, the corresponding value will bind to the label, resulting in this shorter form:

case .variable(let name) // okay, because the name is the same as the label
case .lambda(let parameters, let body) // this is okay too, same reason.
case .variable(let x) // compiler error. label must appear one way or another.
case .lambda(let params, let body) // compiler error, same reason as above.
Comment/question 3: Being a source-breaking change, that requires extreme justification, and I just don't think there is one for this rule. The perceived problem being addressed (that one might try to bind `parameters` to `body` and `body` to `parameters`) is unchanged whether enum cases are modeled as tuples or functions, so aligning enum cases to functions is not in and of itself justification to revisit the issue of whether to try to prohibit this.

To reiterate, here patterns are changed not for any kind of "alignment" with function syntax. It changed because we dropped the tuple pattern (which remains available for matching with tuple values, btw), therefore we need to consider what a first-class syntax for enum case would look like.

Since the rationale for this proposal is to "normalize" enum cases by making them more function-like, again you will need to justify why pattern matching should break from that overarching goal.

This is a source-breaking change, so it's not enough that a "first-class syntax" from the ground up would be different from the status quo (which was the Swift 3 evolution standard--if we were to do it again from scratch, would we still do it this way?). The Swift 4 evolution expectation is that a source-breaking change should require "extreme" justification.

Fair enough. I think the Swift 3 criteria is met. As for Swift 4, I used the word “deprecated” in the source compatibility section. I imagine this means that only deprecation warnings and fix-its are issued in Swift 4 and the warning becomes an error in Swift 5. Obviously, that’s not a justification…

What do you think Joe?

The justification for this breaking change is this: with tuples, labels in pattern is not well enforced. User can skip them, bind value to totally arbitrary names, etc. I personally think emulating such rule prevents us from making pattern matching easier to read for experienced devs and easier to learn for new comers.

Perhaps, but this is an argument that tuple pattern binding is inferior. It has nothing to do with enum cases in particular. In fact, several threads have touched on this topic holistically. The conclusions there have been that allowing (a: Int, b: Int) to bind (Int, Int) or vice versa is healthy and useful, but allowing (a: Int, b: Int) to bind (b: Int, c: Int) is not so good, and (a: Int, b: Int) binding (b: Int, a: Int) is counterintuitive and should be removed.

Fantastic! Really appreciate this summary.

Emulating tuple rules is the backwards-compatible way. Therefore, it ought to be the default unless the source-breaking alternative is not merely easier but _overwhelmingly_ easier.

I assume by “easier” you mean impacting existing codebase less by design or tooling?

If tuples are dangerously broken, then that calls for a proposal to change tuples.

It's reasonable to expect existing patterns in the wild either already use the labels found in declaration or they are matching against label-less cases.

I don't think that is reasonable to expect.

In other words, existing code with good style won't be affected much.

You're defining your preference as "good style" and saying that a proposal to enforce your preference by the compiler won't affect code with "good style" (i.e. adhering to your preference), which is tautologically true and also not a valid argument.

This is a combination of personal experience and some of my wishful thinking. I work on a few large Swift projects and observed that most of the time labels matches the names chosen in the pattern. The selection bias here is strong, of course. I wish I could qualify it as “most of the code in the wild” in place of “good style” :stuck_out_tongue:

For the code that actually would break, I think the migrator and the compiler can provide sufficient help in form of migration/fixits/warnings.

Ultimately I think requiring appearance of labels one way or another in patterns will improve both the readability of the pattern matching site as well as forcing the author of case declaration consider the use site more.

In fact, I think the proposed solution suffers from two great weaknesses. First, it seems ad-hoc. Consider this: if enum cases are to be modeled as functions, then I should be able to write something intermediate between the options above; namely: `case .variable(name:)(let x)`. Since `.variable` unambiguously refers to `.variable(name:)`, I should also be allowed to write `.variable(let x)` just as I am now.

Again, patterns are not to be modeled after functions. Only the declaration and usage of case constructors are.

This requires justification. Why are they not to be? IMO, they ought to be.

Second, it seems unduly restrictive. If, in the containing scope, I have a variable named `body` that I don't want to shadow, this rule would force me to either write the more verbose form or deal with shadowing `body`. If a person opts for the shorter form, they are choosing not to use the label.

In fact this (to avoid label conflict in nesting) is the only reason the longer form allows rebinding to other names at all! You say "unduly restrictive", I say "necessarily flexible" :slight_smile:

The great majority of Swift users don't read this list, nor care about the starting point from which a proposal added "flexibility." They compare Swift N to Swift N + 1, and in this case, there is a new restriction. That is the only comparison which matters. So make no mistake, you are proposing a source-breaking restriction. For the arguments I gave in my earlier reply, I believe it is _unduly_ restrictive.

Only one of these variants may appear in a single pattern. Swift compiler will raise a compile error for mixed usage.

case .lambda(parameters: let params, let body) // error, can not mix the two.
Some patterns will no longer match enum cases. For example, all associated values can bind as a tuple in Swift 3, this will no longer work after this proposal:

// deprecated: matching all associated values as a tuple
if case let .lambda(f) = anLambdaExpr {
    evaluateLambda(parameters: f.parameters, body: f.body)
}


(John McCall) #13

Is there any plan to get this proposal reviewed? It would *really* improve the SwiftPM Package API.

The core team talked about it and have decided to allow it. I've put it back up for review until the 10th.

Has someone volunteered to work on an implementation for this? I don't think we at Apple have scheduled any time for it, and if we don't have an implementation within the next few weeks, it's not likely to happen in Swift 4 even if the proposal is approved.

John.

···

On Mar 31, 2017, at 3:10 AM, David Hart via swift-evolution <swift-evolution@swift.org> wrote:

On 9 Mar 2017, at 23:42, Daniel Duan via swift-evolution <swift-evolution@swift.org> wrote:

Here’s an updated version with hopefully the correct content. The “overloaded” case feature has been removed and is discussed in the "alternatives considered" section.

# Normalize Enum Case Representation

* Proposal: [SE-0155][]
* Authors: [Daniel Duan][], [Joe Groff][]
* Review Manager: [John McCall][]
* Status: **Awaiting review**
* Previous Revision: [1][Revision 1]

## Introduction

In Swift 3, associated values of an enum case are represented by a tuple. This
implementation causes inconsistencies in case declaration, construction and
pattern matching in several places.

Enums, therefore, can be made more "regular" when we replace tuple as the
representation of associated case values. This proposal aims to define the
effect of doings so on various parts of the language.

Swift-evolution thread: [Normalize Enum Case Representation (rev. 2)][]

## Motivation

When user declares a case for an enum, a function which constructs the
corresponding case value is declared. We'll refer to such functions as _case
constructors_ in this proposal.

enum Expr {
  // this case declares the case constructor `Expr.elet(_:_:)`
  indirect case elet(locals: [(String, Expr)], body: Expr)
}

// f's signature is f(_: _), type is ([(String, Expr)], Expr) -> Expr
let f = Expr.elet

// `f` is just a function
f([], someExpr) // construct a `Expr.elet`

There are many surprising aspects of enum constructors, however:

1. After [SE-0111][], Swift function's fully qualified name consists of its base
name and all of its argument labels. User can use the full name of the
function at use site. In the example above, `locals` and `body` are currently
not part of the case constructors name, therefore the expected syntax is
invalid.

 func f(x: Int, y: Int) {}
 f(x: y:)(0, 0) // Okay, this is equivalent to f(x: 0, y: 0)
 Expr.elet(locals: body:)([], someExpr) // this doesn't work in Swift 3

2. Case constructors cannot include a default value for each parameter. This
is yet another feature available to functions.

As previous mentioned, these are symptoms of associated values being a tuple
instead of having its own distinct semantics. This problem manifests more in
Swift 3's pattern matching:

1. A pattern with a single value would match and result in a tuple:

  // this works for reasons most user probably don't expect!
  if case .elet(let wat) = anExpr {
      eval(wat.body)
  }

2. Labels in patterns are not enforced:

  // note: there's no label in the first sub-pattern
  if case .elet(let p, let body: q) = anExpr {
      // code
  }

These extra rules makes pattern matching difficult to teach and to expand to
other types.

## Proposed Solution

We'll add first class syntax (which largely resemble the syntax in Swift 3) for
declaring associated values with labels. Tuple will no longer be used to
represent the aggregate of associated values for an enum case. This means
pattern matching for enum cases needs its own syntax as well (as opposed to
piggybacking on tuple patterns, which remains in the language for tuples.).

## Detailed Design

### Compound Names For Enum Constructors

Associated values' labels should be part of the enum case's constructor name.
When constructing an enum value with the case name, label names must either be
supplied in the argument list it self, or as part of the full name.

Expr.elet(locals: [], body: anExpr) // Okay, the Swift 3 way.
Expr.elet(locals: body:)([], anExpr) // Okay, equivalent to the previous line.
Expr.elet(locals: body:)(locals: 0, body: 0) // This would be an error, however.

Note that since the labels aren't part of a tuple, they no longer participate in
type checking, behaving consistently with functions.

let f = Expr.elet // f has type ([(String, Expr)], Expr) -> Expr
f([], anExpr) // Okay!
f(locals: [], body: anExpr) // Won't compile.

Enum cases should have distinct *full* names. Therefore, shared base name will
be allowed:

enum SyntaxTree {
  case type(variables: [TypeVariable])
  case type(instantiated: [Type])
}

Using only the base name in pattern matching for the previous example would be
ambiguous and result in an compile error. In this case, the full name must be
supplied to disambiguate.

case .type // error: ambiguous
case .type(variables: let variables) // Okay

### Default Parameter Values For Enum Constructors

From a user's point view, declaring an enum case should remain the same as Swift
3 except now it's possible to add `= expression` after the type of an
associated value to convey a default value for that field.

enum Animation {
  case fadeIn(duration: TimeInterval = 0.3) // Okay!
}
let anim = Animation.fadeIn() // Great!

Updated syntax:

union-style-enum-case = enum-case-name [enum-case-associated-value-clause];
enum-case-associated-value-clause = "(" ")"
                                > "(" enum-case-associated-value-list ")";
enum-case-associated-value-list = enum-associated-value-element
                              > enum-associated-value-element ","
                                enum-case-associated-value-list;
enum-case-associated-value-element = element-name type-annotation
                                   [enum-case-element-default-value-clause]
                                 > type
                                   [enum-case-element-default-value-clause];
element-name = identifier;
enum-case-element-default-value-clause = "=" expression;

### Alternative Payload-less Case Declaration

In Swift 3, the following syntax is valid:

enum Tree {
  case leaf() // the type of this constructor is confusing!
}

`Tree.leaf` has a very unexpected type to most Swift users: `(()) -> Tree`

We propose this syntax become illegal. User must explicitly declare
associated value of type `Void` if needed:

enum Tree {
  case leaf(Void)
}

### Pattern Consistency

*(The following enum will be used throughout code snippets in this section).*

indirect enum Expr {
  case variable(name: String)
  case lambda(parameters: [String], body: Expr)
}

Compared to patterns in Swift 3, matching against enum cases will follow
stricter rules. This is a consequence of no longer relying on tuple patterns.

When an associated value has a label, the sub-pattern must include the label
exactly as declared. There are two variants that should look familiar to Swift
3 users. Variant 1 allows user to bind the associated value to arbitrary name in
the pattern by requiring the label:

case .variable(name: let x) // okay
case .variable(x: let x) // compile error; there's no label `x`
case .lambda(parameters: let params, body: let body) // Okay
case .lambda(params: let params, body: let body) // error: 1st label mismatches

User may choose not to use binding names that differ from labels. In this
variant, the corresponding value will bind to the label, resulting in this
shorter form:

case .variable(let name) // okay, because the name is the same as the label
case .lambda(let parameters, let body) // this is okay too, same reason.
case .variable(let x) // compiler error. label must appear one way or another.
case .lambda(let params, let body) // compiler error, same reason as above.

Only one of these variants may appear in a single pattern. Swift compiler will
raise a compile error for mixed usage.

case .lambda(parameters: let params, let body) // error, can not mix the two.

Some patterns will no longer match enum cases. For example, all associated
values can bind as a tuple in Swift 3, this will no longer work after this
proposal:

// deprecated: matching all associated values as a tuple
if case let .lambda(f) = anLambdaExpr {
  evaluateLambda(parameters: f.parameters, body: f.body)
}

## Source compatibility

Despite a few additions, case declaration remain mostly source-compatible with
Swift 3, with the exception of the change detailed in "Alternative Payload-less
Case Declaration".

Syntax for case constructor at use site remain source-compatible.

A large portion of pattern matching syntax for enum cases with associated values
remain unchanged. But patterns for matching all values as a tuple, patterns that
elide the label and binds to names that differ from the labels, patterns that
include labels for some sub-patterns but the rest of them are deprecated by this
proposal. Therefore this is a source breaking change.

## Effect on ABI stability and resilience

After this proposal, enum cases may have compound names. This means the standard
library will expose different symbols for enum constructors. The name mangling
rules should also change accordingly.

## Alternative Considered

Between case declaration and pattern matching, there exist many reasonable
combinations of improvement. On one hand, we can optimize for consistency,
simplicity and teachability by bringing in as much similarity between enum and
other part of the language as possible. Many decisions in the first revision
were made in favor if doing so. Through the feedbacks from swift-evolution, we
found that some of the changes impedes the ergonomics of these features too much
. In this section, we describe some of the alternatives that were raised and
rejected in hope to strike a balance between the two end of the goals.

We discussed allowing user to declare a *parameter name* ("internal names")
for each associated value. Such names may be used in various rules in pattern
matching. Some feedback suggested they maybe used as property names when we
make enum case subtypes of the enum and resembles a struct. This feature is not
included in this proposal because parameter names are not very useful *today*.
Using them in patterns actually don't improve consistency as users don't use
them outside normal function definitions at all. If enum case gains a function
body in a future proposal, it'd be better to define the semantics of parameter
names then, as opposed to locking it down now.

To maintain ergonomics/source compatibility, we could allow user to choose
arbitrary bindings for each associated value. The problem is it makes the
pattern deviate a lot from declaration and makes it hard for beginners to
understand. This also decrease readability for seasoned users.

Along the same line, a pattern that gets dropped is binding all associated
values as a labeled tuple, which tuple pattern allowed in Swift 3. As T.J.
Usiyan [pointed out][TJs comment], implementation of the equality protocol would
be simplified due to tuple's conformance to `Equatable`. This feature may still
be introduced with alternative syntax (perhaps related to splats) later without
source-breakage. And the need to implement `Equatable` may also disappear with
auto-deriving for `Equatable` conformance.

The previous revision of this proposal mandated that the labeled form of
sub-pattern (`case .elet(locals: let x, body: let y)`) be the only acceptable
pattern. Turns out the community considers this to be too verbose in some cases.

A drafted version of this proposal considered allowing "overloaded" declaration
of enum cases (same full-name, but with associated values with different types).
We ultimately decided that this feature is out of the scope of this proposal.

[SE-0155]: 0155-normalize-enum-case-representation.md
[SE-0111]: 0111-remove-arg-label-type-significance.md
[Daniel Duan]: https://github.com/dduan
[Joe Groff]: https://github.com/jckarter
[John McCall]: https://github.com/rjmccall
[TJs comment]: https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170116/030614.html
[Revision 1]: https://github.com/apple/swift-evolution/blob/43ca098355762014f53e1b54e02d2f6a01253385/proposals/0155-normalize-enum-case-representation.md
[Normalize Enum Case Representation (rev. 2)]: https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170306/033626.html

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

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


(Joe Groff) #14

Thanks for the thoughtful feed Xiaodi! Replies are inline. I'm going to incorporate some of the responses into the proposal.

The rendered version differs from the text appended to your message. I'll assume the more fully fleshed out version is what you intend to submit. Three comments/questions:

Enum Case "Overloading"

An enum may contain cases with the same full name but with associated values of different types. For example:

enum Expr {
    case literal(Bool)
    case literal(Int)
}
The above cases have overloaded constructors, which follow the same rules as functions at call site for disambiguation:

// It's clear which case is being constructed in the following.
let aBool: Expr = .literal(false)
let anInt: Expr = .literal(42)
User must specify an as expression in sub-patterns in pattern matching, in order to match with such cases:

case .literal(let value) // this is ambiguous
case .literal(let value as Bool) // matches `case literal(Bool)`

Comment/question 1: Here, why aren't you proposing to allow `case .literal(let value: Bool)`? For one, it would seem to be more consistent.

The example in proposal doesn't include any labels. Are you suggesting two colons for sub-patterns with labels? Like `case .literal(value: let value: Bool)`? This looks jarring. But I'm definitely open to other suggestions.

That does look jarring. But hmm.

Second, since we still have some use cases where there's Obj-C bridging magic with `as`, using `as` in this way may run into ambiguity issues if (for example) you have two cases, one with associated value of type `String` and the other of type `NSString`.

Either this should be rejected at declaration, or we need a way to accept a "pre-magic" resolution at pattern matching, when this scenarios is at hand.

Or we align pattern matching to function syntax and have such cases disambiguated in that way (see below).

I'm on the phone so I can't verify. Wouldn't function overloading face a similar problem?

Also, since enum cases are to be like functions, I assume that the more verbose `as` version would work for free: `case .literal(let value) as (Bool) -> Expr`?

This is not being proposed. When a user sees/authors a case, their expectation for the declared case constructor should resemble that of a function. Pattern matching was considered separately since it's not relatable syntactically.

This requires justification. If enum cases are to be like functions, then the logical expectation is that pattern matching should work in that way too. I see no rationale to undergird your claim that pattern matching is "not relatable syntactically." Allowing `case .literal(let value) as (Bool) -> Expr` would solve the issue above, as well as provide more flexibility with the issues below.

I have concerns about the verbosity this syntax introduces. Example:

enum A { case v(Int) }
enum B { case v(A); case v(Int) }

To disambiguate a value of type B, it would be

case .v(A.v(let xValue)) as ((Int -> A) -> B)

This scales poorly for cases with deeper recursions and/or more associated values.

Disambiguate at the sub-pattern level doesn’t have this scalability problem.

This also is not the right meaning of `as`. `as` coerces the subpattern type, and a `.v(...)` pattern has type B. This coercion makes no sense.

Personally I think that overloading is unnecessary scope creep and should be left out of the proposal.

We’ve encountered a bigger question that it initial seems. Let’s zoom out.

There are 2 popular kinds of patterns for value deconstruction in PLs: patterns for trees and sequences. The former deconstructs value who’s prominently recursive: enum, struct, tuple; the latter deals with list-like (grows in 1 direction indefinitely) things. We are now investigating the syntax that can potentially be used for all tree patterns. Whereas the “shape” alone isn’t enough information, user must use the type to supplement the pattern for a successful match. If we introduce patterns for structs in the future, whatever we came up here for type disambiguation should work there.

<https://github.com/dduan/swift-evolution/blob/SE0155-rev2/proposals/0155-normalize-enum-case-representation.md#alternative-payload-less-case-declaration>Alternative Payload-less Case Declaration

In Swift 3, the following syntax is valid:

enum Tree {
    case leaf() // the type of this constructor is confusing!
}
Tree.leaf has a very unexpected type to most Swift users: (()) -> Tree

We propose this syntax declare the "bare" case instead. So it's going to be the equivalent of

enum Tree {
    case leaf // `()` is optional and does the same thing.
}

Comment/question 2: First, if associated values are not to be modeled as tuples, for backwards compatibility the rare uses of `case leaf()` should be migrated to `case leaf(())`.

Yes,

Cool.

and when user uses a arbitrary name when they should have used a label, or when labels are misspelled, the compiler should suggest the correct labels.

As below, I disagree with this restriction very strongly.

I wasn't sure how much of migrator related thing should go into a proposal. Perhaps there should be more.

Second, to be clear, you are _not_ proposing additional sugar so that a case without an associated value be equivalent to a case that has an associated value of type `Void`, correct? You are saying that, with your proposal, both `case leaf()` and `case leaf` would be regarded as being of type `() -> Tree` instead of the current `(()) -> Tree`?

Correct. I'm _not_ proposing implicit `Void`.

[The latter (i.e. `() -> Tree`) seems entirely fine. The former (i.e. additional sugar for `(()) -> Tree`) seems mostly fine, except that it would introduce an inconsistency with raw values that IMO is awkward. That is, if I have `enum Foo { case bar }`, it would make case `bar` have implied associated type `Void`; but, if I have `enum Foo: Int { case bar }`, would case `bar` have raw value `0` of type `Int` as well as associated value `()` of type `Void`?]

<https://github.com/dduan/swift-evolution/blob/SE0155-rev2/proposals/0155-normalize-enum-case-representation.md#pattern-consistency>Pattern Consistency

(The following enum will be used throughout code snippets in this section).

indirect enum Expr {
    case variable(name: String)
    case lambda(parameters: [String], body: Expr)
}
Compared to patterns in Swift 3, matching against enum cases will follow stricter rules. This is a consequence of no longer relying on tuple patterns.

When an associated value has a label, the sub-pattern must include the label exactly as declared. There are two variants that should look familiar to Swift 3 users. Variant 1 allows user to bind the associated value to arbitrary name in the pattern by requiring the label:

case .variable(name: let x) // okay
case .variable(x: let x) // compile error; there's no label `x`
case .lambda(parameters: let params, body: let body) // Okay
case .lambda(params: let params, body: let body) // error: 1st label mismatches
User may choose not to use binding names that differ from labels. In this variant, the corresponding value will bind to the label, resulting in this shorter form:

case .variable(let name) // okay, because the name is the same as the label
case .lambda(let parameters, let body) // this is okay too, same reason.
case .variable(let x) // compiler error. label must appear one way or another.
case .lambda(let params, let body) // compiler error, same reason as above.
Comment/question 3: Being a source-breaking change, that requires extreme justification, and I just don't think there is one for this rule. The perceived problem being addressed (that one might try to bind `parameters` to `body` and `body` to `parameters`) is unchanged whether enum cases are modeled as tuples or functions, so aligning enum cases to functions is not in and of itself justification to revisit the issue of whether to try to prohibit this.

To reiterate, here patterns are changed not for any kind of "alignment" with function syntax. It changed because we dropped the tuple pattern (which remains available for matching with tuple values, btw), therefore we need to consider what a first-class syntax for enum case would look like.

Since the rationale for this proposal is to "normalize" enum cases by making them more function-like, again you will need to justify why pattern matching should break from that overarching goal.

This is a source-breaking change, so it's not enough that a "first-class syntax" from the ground up would be different from the status quo (which was the Swift 3 evolution standard--if we were to do it again from scratch, would we still do it this way?). The Swift 4 evolution expectation is that a source-breaking change should require "extreme" justification.

Fair enough. I think the Swift 3 criteria is met. As for Swift 4, I used the word “deprecated” in the source compatibility section. I imagine this means that only deprecation warnings and fix-its are issued in Swift 4 and the warning becomes an error in Swift 5. Obviously, that’s not a justification…

What do you think Joe?

Unifying the declaration model is worth a minor source break, IMO.

The justification for this breaking change is this: with tuples, labels in pattern is not well enforced. User can skip them, bind value to totally arbitrary names, etc. I personally think emulating such rule prevents us from making pattern matching easier to read for experienced devs and easier to learn for new comers.

Perhaps, but this is an argument that tuple pattern binding is inferior. It has nothing to do with enum cases in particular. In fact, several threads have touched on this topic holistically. The conclusions there have been that allowing (a: Int, b: Int) to bind (Int, Int) or vice versa is healthy and useful, but allowing (a: Int, b: Int) to bind (b: Int, c: Int) is not so good, and (a: Int, b: Int) binding (b: Int, a: Int) is counterintuitive and should be removed.

Fantastic! Really appreciate this summary.

Tuples are at the type system level convertible from labeled and unlabeled and in label-changing ways. We should arguably clamp down on that, and that might affect how pattern matching works with them. It isn't necessarily analogous to enum cases, since with enum cases we're working with a named declaration where the labels are needed for name lookup.

-Joe

···

On Mar 9, 2017, at 11:48 AM, Daniel Duan <daniel@duan.org> wrote:

On Mar 9, 2017, at 12:31 AM, Xiaodi Wu <xiaodi.wu@gmail.com <mailto:xiaodi.wu@gmail.com>> wrote:
On Thu, Mar 9, 2017 at 1:07 AM, Daniel Duan <daniel@duan.org <mailto:daniel@duan.org>> wrote:
On Mar 8, 2017, at 9:56 PM, Xiaodi Wu <xiaodi.wu@gmail.com <mailto:xiaodi.wu@gmail.com>> wrote:


(Joe Groff) #15

Matching and construction are closely related and designed to mimic each other. They aren't completely orthogonal.

-Joe

···

On Mar 9, 2017, at 5:59 AM, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:

I have argued repeatedly that enum cases play two distinct roles (and sometimes a third in the future). We need to embrace all of these roles. This proposal primarily normalizes the role of case-as-factory-method.

Pattern matching is a second role enum cases play. We already have syntax for patterns that match based on type. I see no reason we should introduce new matching syntax to disambiguate overloaded cases. The existing patterns work well for this purpose. Matching should be considered a role played by enum cases that is completely orthogonal to their role as a factory method.