[swift-evolution] PITCH: New :== operator for generic constraints
Slava Pestov
spestov at apple.com
Tue Aug 16 23:42:39 CDT 2016
> On Aug 16, 2016, at 8:52 PM, Charles Srstka <cocoadev at charlessoft.com> wrote:
>
>> On Aug 16, 2016, at 8:51 PM, Slava Pestov <spestov at apple.com <mailto:spestov at apple.com>> wrote:
>>
>> Here is why we must have that requirement. Consider the following code:
>>
>> protocol P {
>> init()
>> }
>>
>> struct A : P {
>> init() {}
>> }
>>
>> struct B : P {
>> init() {}
>> }
>>
>> func makeIt<T : P>() -> T {
>> return T()
>> }
>>
>> I can use this function as follows:
>>
>> let a: A = makeIt() // Creates a new ‘A'
>> let a: B = makeIt() // Creates a new ‘B’
>>
>> Now suppose we allow P to self-conform. Then the following becomes valid:
>>
>> let p: P = makeIt()
>>
>> What exactly would makeIt() do in this case? There’s no concrete type passed in, or any way of getting one, so there’s nothing to construct. The same issue would come up with static methods here.
>
> Argh, that’s particularly frustrating since in something like ‘func foo<T : P>(t: T)’ or ‘func foo<S : Sequence>(s: S) where S.IteratorElement: P’, you’re only ever getting instances anyway since the parameter is in the input, so calling initializers or static functions isn’t something you can even do (unless you call .dynamicType, at which point you *do* have a concrete type at runtime thanks to the dynamic check).
Well, if you have ‘func foo<T : P>(t: T)’, then you can write T.someStaticMember() to call static members — it’s true you also have an instance ’t’, but you can also work directly with the type. But I suspect this is not what you meant, because:
>
> I wish there were a way to have partial conformance in cases like these. Like how this causes what’s probably Swift’s most confusing compiler error (certainly one of its most asked about):
>
> protocol P: Equatable {
> static func ==(l: Self, r: Self) -> Bool
>
> func foo()
> }
>
> struct S: P {
> static func ==(l: S, r: S) -> Bool {
> return true
> }
>
> func foo() {
> print("foo")
> }
> }
>
> let s = S()
> let p = s as P // error: Protocol ‘P’ can only be used as a generic constraint because it has Self or associated type requirements
Yep :) So the property of ‘can be used as an existential type’ is actually a bit different from ‘protocol conforms to itself’. The rules here are:
- Self must not appear in contravariant position
- Protocol has no associated types
Note that static members and initializers are OK, and you can call them via ‘p.dynamicType.foo()’ where p : P.
>
> It would make using protocols so much less teeth-grinding if the compiler *did* allow you to type the variable as P, but then would just throw an error if you tried to call one of the “problem” methods (in this case, using the ‘==' operator would be an error, but calling ‘foo’ would be fine). If this were possible, the conformance for a variable typed P would just not pick up “illegal” things like initializers, and would also leave out conformance for things like 'makeIt()' above which return the generic parameter in the output, rather than the input, necessitating a concrete type. I’m probably dreaming, I know.
In the type checker, this more precise, per-member check is already implemented, interestingly enough. It comes up with protocol extensions. Imagine you have a protocol ‘P’ that can be used as an existential, but an extension of P adds a problematic member:
protocol P {
func f() -> Int
}
extension P {
func ff(other: Self) -> Int { return f() + s.f()) }
}
Here, you don’t want to entirely ban the type ‘P’, because the extension might come from another module, and it shouldn’t just break everyone’s code. So the solution is that you can use ‘P’ as an existential, but if you try to reference ‘p.ff’ where p : P, you get a diagnostic, because that particular member is unavailable.
In fact, I think the separate restriction that rules out usage of the overall type when one of the protocol’s requirements is problematic, is mostly artificial, in that it could just be disabled and you’d be able to pass around ‘Equatable’ values, etc, because the lower layers don’t care (I think).
I do remember it was explained to me at one point that this is how it was in the early days of Swift, but it made code completion and diagnostics confusing, because with some protocols (like Sequence) most members became inaccessible.
A better approach is to implement more general existential types which expose ways of working with their associated types, rather than just banning certain members from being used altogether. This is described in Doug's ‘completing generics’ document, and again, it is quite a large project :)
>
> Actually, what I wish is that Swift had an equivalent of the 'id <P>’ type in Objective-C. That notation always stood for an instance of something that conformed to P, rather than "maybe P itself, and maybe something that conforms to it”. If we could do that, we could just pass sequences of 'id <P>’ (in whatever syntax we gave it in Swift) to a sequence where Element: P, and it’d work fine regardless of anything that prevented P from conforming to P.
In fact I think some of the proposals call for Any<P> as the syntax for the most general existential of type ‘P’, with other syntax when associated types are bound. I must admit I haven’t followed the discussions around generalized existentials very closely though.
So it sounds like your original :== operator idea is really about implementing self-conforming protocols, as well as generalized existentials. These are quite difficult projects, but I hope we’ll tackle them one day. Patches are welcome :-)
>
> Charles
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160816/a8561ae9/attachment.html>
More information about the swift-evolution
mailing list