On Tue, 21 Feb 2017 at 19:50, Patrick Pijnappel via swift-evolution < swift-evolution@swift.org> wrote:
Just to clarify, the proposal doesn't suggest to allow the associated
value to be used as a subtype of the enum.
enum Result<T> { case .success(T), .error(Error) }
func foo(_ x: Result<Int>) { /* ... */ }
func bar(_ x: Result<Int>.success) { /* ... */ }
// Not this:
foo(5)
bar(5)
// But rather:
foo(.success(5))
bar(.success(5))
Effectively, Result<T>.success would behave like a struct that is a
subtype of Result<T>.
On Tue, Feb 21, 2017 at 12:50 PM, Joe Groff via swift-evolution < > swift-evolution@swift.org> wrote:
On Feb 20, 2017, at 1:53 PM, Matthew Johnson <matthew@anandabits.com> > wrote:
On Feb 20, 2017, at 3:22 PM, Joe Groff <jgroff@apple.com> wrote:
On Feb 20, 2017, at 1:04 PM, Matthew Johnson <matthew@anandabits.com> > wrote:
On Feb 20, 2017, at 2:38 PM, Joe Groff <jgroff@apple.com> wrote:
On Feb 20, 2017, at 7:32 AM, Matthew Johnson via swift-evolution < > swift-evolution@swift.org> wrote:
On Feb 20, 2017, at 12:40 AM, Niels Andriesse via swift-evolution < > swift-evolution@swift.org> wrote:
I'd like to discuss the possibility of treating the cases of a given enum
as if they are subtypes of that enum. This seems like a natural thing to do
because enum cases (especially when they have associated values)
effectively define a closed set of subtypes.
Doing so would allow for constructions such as the following:
enum Foo {
case a(name: String)
}
func isA(foo: Foo) -> Bool {
// The old way:
if case .a = foo { return true }
return false
// The new way:
return foo is .a
}
func printNameIfFooIsA(foo: Foo) -> Bool {
// The old way:
if case let .a(name) = foo {
print(name)
}
// The new way (1):
if let a = foo as? .a {
print(a.name)
}
// The new way (2):
if let name = (foo as? .a)?.name {
print(name)
}
}
Treating an enum's cases as its subtypes would make enums easier to work
with because handling them would be syntactically the same as handling
other types.
The pattern matching capabilities of enums wouldn't be affected by this
proposal.
Multiple other proposals have already attempted to simplify enum handling
(they have particularly focused on getting rid of "if case" and adding the
ability to treat enum case tests as expressions), but none of the solutions
presented in those proposals have worked out so far.
I believe that this could be the right solution to multiple enum-related
problems that have been brought up repeatedly.
I would like to see enum cases treated as subtypes of the enum type. This
is an interesting way to refer to the type of a case. Unfortunately I
don’t think it will work if we accept the proposal to give cases a compound
name. If we do that the name of this case becomes `a(name:)` which is not
a valid type name.
I think there are definitely places where having cases be a subtype of an
enum make sense, but I don't think it makes sense for *all* cases to be
subtypes. For example, with "biased" containers like Optional and Result,
it makes sense for the "right" side to be a subtype and the "wrong" side to
be explicitly constructed, IMO. If the types of cases overlap, it would
also be *ambiguous* which case ought to be constructed when the payload is
converted to the enum type
Identical case types would definitely be a problem but I don’t think
overlapping case types are always a problem. I imagine this conversion
working the same as any other ordinary overload resolution for ad-hoc
overloads.
Conversions happen at runtime too. `0 as Any as? Either<Int, Int>`
wouldn't have any way to tell what `Either` to form if both arms of the
Either were subtype candidates. An Either<T, U> in <T, U> context can end
up being bound to Either<Int, Int> at runtime and interacting with runtime
casts that way.
Hmm. This is unfortunate.
In cases where T and U overlap and form a linear hierarchy but are not
identical couldn’t the runtime determine the most direct path and choose
that?
If the compiler prohibited cases with exactly the same types like
`Either<Int, Int>` from being expressed statically how do these types end
up getting formed dynamically? Is there any way those operations could be
failable?
—remember that enums are sums, not unions, and that's important for
composability and uniform behavior with generics.
I’ve always thought of enums as nominal discriminated unions. Maybe I’m
using the wrong terminology. Can you elaborate on the difference between
sums and unions? When you say union are you talking about the kind of
thing some people have brought up in the past where any members in common
are automatically made available on the union type?
Sums maintain structure whereas unions collapse it. As a sum, Optional<T>
maintains its shape even when T = Optional<U>. If it were a union, T u Nil
u Nil would collapse to T u Nil, losing the distinction between the inner
and outer nil and leading to problems in APIs that use the outer nil to
communicate meaning about some outer structure, such as asking for the
`first` element of a collection of Optionals.
Got it. This is certainly a problem for `Optional`.
But sometimes this behavior of collapsing the syntactic specification to a
canonical sum type would be very useful. What is the reason we can’t have
something syntactic type expressions like `Int | String`, `Int | String |
String, `String | Int | String | Int`, etc all collapse to the same
canonical structural sum type:
enum {
sub case int(Int), string(String)
}
This is how I’ve been thinking about those syntactic types. We already
allow existential types to be formed using syntax that collapses to a
canonical type:
typealias Existential1 = Protocol1 & Protocol2
typealias Existential2 = Protocol2 & Existential1 & Protocol 3 & Protocol1
typealias Existential3 = Existential1 & Protocol3
In this example Existential1 and Existential3 are different names for the
same type.
Is there a reason we can’t have similar syntax that collapses to a
similarly canonical sum type? If we’re going to allow case subtypes this
feels to me like a very natural and useful direction.
A couple reasons that come to mind:
- Most directly, we don't allow abstraction over generic constraints.
`ExistentialN<T, U> = T & U` isn't allowed. As soon as you have abstraction
over either unions or intersections, type checking becomes an unbounded
search problem in the worst case, since every T binding is potentially
equivalent to a T1 & T2 or T1 | T2 with T1 == T2 == T.
- Sums and unions both imply a matching branch structure in the code
somewhere to handle both possibilities. If the number of actual
possibilities is different in different situations, that's a source of
bugs, such as the overloading of `nil` I mentioned previously. Even if you
did allow generic T & T types, the worst result of someone seeing that as
T1 & T2 is that the operations enabled through conforming to T1 and T2 map
to the same conformance.
-Joe
If we don’t allow it there are two problems: people have to invent a
largely meaningless name for the enum and it is incompatible with any other
similarly structured enum. Neither is a significant problem but they do
add (seemingly) unnecessary friction to the language.
I wouldn’t expect these to be widely used - they would play a similar role
as tuples - but they would be very appreciated where they are used.
-Joe
_______________________________________________
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