Responses inline. Thanks for taking the time to read through and comment,
your feedback is incredibly useful!
I put together the skeleton of a proposal detailing enhancements to how
associated types can be referenced in Swift 3+. It's certainly not ready
for submission, but I think it gets across my ideas pretty well. Would love
to gather feedback and especially improvements.
Be unsparing; whatever form this feature takes will profoundly affect how
Swift developers program for years, so it needs to be done right.
See below:
*Proposal*
An existential type is defined using the "Any<...>" construct. Existential
types can be nested.
The empty existential type "Any<>" is typealiased to 'Any’.
Also, Any<SomeProtocol> is equivalent to SomeProtocol.
Yes. protocol<> currently also behaves like this.
Within the angle brackets are zero or more 'clauses'. Clauses are
separated by semicolons. (This is so commas can be used in where
constraints, below. Better ideas are welcome. Maybe it's not necessary; we
can use commas exclusively.)
I’m not a fan of the semicolon idea. I don’t see any reason for this.
The `where` keyword separates the protocol list from the constraints just
fine. The list on either side should be able to use commas with no problem
(or line breaks if that proposal goes through).
I'm leaning towards getting rid of the commas, but would like to write out
a few 'dummy' examples to see if there are any readability issues that
arise.
There are five different possible clauses:
- 'class'. Must be the first clause, if present. Places a constraint
on the existential to be any class type. (Implies: Only one can exist.
Mutually exclusive with class name clause.)
(In the future a follow-up proposal should add in 'struct' or 'value' as a
counterpart.)
If we’re going to allow `struct` we should also allow `enum`. `value`
would allow either of those.
Of course. A future proposal can allow list members to discuss the exact
details as to how struct, value, or enum specifiers should work.
- Class name. Must be the first clause, if present. (Implies: Only one
can exist. Mutually exclusive with 'class'.) Places a constraint on the
existential (not really an existential anymore) to be an instance of the
class, or one of its subclasses.
It is still be an existential if it includes protocol requirements that
the class does not fulfill. For example, you might have Any<UIView,
> where UIView does not conform to SomeProtocol, but various
subclasses do.
Fair enough. (I don't think the way things work would be affected.)
Your proposal doesn’t discuss composing Any in the way that Adrian’s did
like this:
typealias Foo = Any<SomeClass, SomeProtocol, OtherProtocol>
Any<AnotherProtocol, Foo>
I didn't think it needed to be discussed. An Any<...> existential type is a
type 'expression' just like any other, and should be allowed to participate
in other Any<...>s.
I like the idea of composition as it allows us to factor out constraints.
If we are going to do that we should allow a class to be specified in the
composition as long is it is a subclass of all class requirements of Any
types it composes. For example, this should be allowed:
typealias Bar = Any<SubclassOfSomeClass, Foo, AnotherProtocol>
This is still one class requirement for Bar, it just refines the class
requirement of Foo to be SubclassOfSomeClass rather than just SomeClass.
This is a good point. There should be clarification as to how special cases
of Any<...> used in another Any<...> behave. For example, like you said
Any<MyClass, Any<SomeSubclassOfMyClass, Protocol>> should be valid. This
will go into any proposal that emerges from the discussion.
Example: Any<UIViewController; UITableViewDataSource; UITableViewDelegate>
"Any UIViewController or subclass which also satisfies the table view data
source and delegate protocols"
- Dynamic protocol. This is entirely composed of the name of a
protocol which has no associated types or Self requirement.
Example: Any<CustomStringConvertible; BooleanType>
"Any type which conforms to both the CustomStringConvertible and
BooleanType protocols"
I'm going to use 'static protocol' to refer to a protocol with associated
types or self requirements. Feel free to propose a more sound name.
- Self-contained static protocol, simple. This is composed of the name
of a static protocol, optionally followed by a 'where' clause in which the
associated types can be constrained (with any of the three basic
conformance types: subclassing, protocol conformance, or type equality).
Associated types are referred to with a leading dot.
Please do not introduce terms “dynamic protocol” and “static protocol”.
We want to support existentials of protocols that have self or associated
type requirements. The dynamic vs static distinction is a limitation of
the current implementation of Swift and doesn’t make sense for the long
term vision.
I'm not trying to introduce new terms, these are just placeholders. At the
same time "protocols with self or associated type requirements" is
cumbersome to work with and it would be nice for someone to come up with a
descriptive term of art for referring to them.
Example: Any<Collection where .Generator.Element : NSObject,
.Generator.Element : SomeProtocol>
"Any type that is a Collection, whose elements are NSObjects or their
subclasses conforming to SomeProtocol.”
Swift does not allow disjunction of requirements. Only conjunctions are
supported. That means the correct reading is:
"Any type that is a Collection, whose elements are NSObjects *and* their
subclasses conforming to SomeProtocol.”
Yes, that is what I meant. "whose elements are (NSObjects or their
subclasses) conforming to SomeProtocol".
- Bound static protocol. This is the same as a self-contained static
protocol, but with a leading "<name> as " which binds the protocol to a
generic typealias. The name can be then be used in subsequent clauses to
build constraints.
Example: Any<T as Collection; IntegerLiteralConvertible where
.IntegerLiteralType == T.Element>.
"Any type that is a Collection, and also can be built from an integer
literal, in which the collection elements are the same type as the type of
the integer used for the integer literal conformance.”
I’m not sure about this, but if we’re going to do it it should be the
other way around: `Collection as T` with the alias *after* the name of
the protocol.
I like this, it flows better. "Protocol as T where Protocol.Foo == Int,
Protocol.Bar : Baz".
You are also using “dot shorthand” here to refer to an associated type of
IntegerLiteralConvertible. I think “dot shorthand” should be limited to
cases where there is only one protocol that is getting constrained. In
other cases, we need to be clear about which protocol we are referring to.
I borrowed dot shorthand from the generics manifesto. But you are right, it
should only be allowed if there is one protocol with associated types or
self requirements clause in the Any<...> construction.
There will be rules to prevent recursive nesting. For example, if generic
typealiases are allowed, they cannot refer to each other in a circular
manner (like how structs can't contain themeselves, and you can't create a
cyclic graph of enums containing themselves).
How an existential can be used depends on what guarantees are provided by
the clauses. For example, 'Any<Equatable>' can't be used for much; if there
were any methods on Equatable that did not use the associated types at all
you'd be able to call them, but that's about it. However, 'Any<Equatable
where .Self == String>' would allow for == to be called on instances. (This
is a stupid example, since Any<Equatable where .Self == String> is
equivalent to 'String', but there are almost certainly useful examples one
could come up with.)
In order of increasing 'power':
- Don't constrain any associated types. You can pass around
Any<Equatable>s, but that's about it.
- Constrain associated types to conform to protocols.
- Fully constrain associated types.
I think we need to spell out pretty clearly what members we expect to be
available or not available. This section probably needs the most design
and elaboration.
For example, we probably can’t access a member who uses an associated type
as an input unless it is constrained to a specific type. On the other hand
output types probably don’t need to limit access to a member. However, if
the output type is Self or an associated type the visible signature would
have an output type which has the relevant constraints of the existential
applied, but no more. In some cases this means the output type would
simply be Any.
Absolutely. This is vaguely what I had in mind but I wanted to get
something down first. Thanks for thinking through some of the implications
:).
Where this really gets tricky is for compound types like functions,
generic types, etc. Working out the details in these cases is pretty
complex. I will defer to Doug on whether it is best to just defer those
cases to the future, leave them up to the implementer, or try to work out
all of the relevant details in the proposal (in which case we probably need
a type system expert to help!).
Yes, exactly! For example, can Any<...> existentials involving protocols
with associated types or self requirements be used within generic function
or type definitions? Maybe there's an argument that existential types of
this nature are redundant if you have access to generics (e.g. defining a
property on a generic type that is a Collection containing Ints; you should
be able to do that today). On the other hand, maybe there are use cases I
haven't thought of...
One area you didn’t touch on is “opening” the existential? Is that out of
scope for this proposal? That would be fine with me as this proposal is
already taking on a lot. But if so, you should mention something about
future directions as it is pretty closely related to this proposal.
Yes, existential opening is explicitly separate from this (although I
wanted to mention it in the section where I talk about how Any<Equatable>
is not very useful). But you are absolutely right, this proposal should
discuss how it wants to interact with possible future directions.
Another area you didn’t touch on is whether Any constructs (and
typealiases referring to them) should be usable as generic constraints. I
would expect this to be possible but I think we need to spell it out.
I'm hoping for community input. This is a tricky subject, and at some point
we'll bump into implementation limitations.
···
On Tue, May 17, 2016 at 12:41 PM, Matthew Johnson <matthew@anandabits.com> wrote:
On May 17, 2016, at 1:52 PM, Austin Zheng <austinzheng@gmail.com> wrote:
-Matthew