[swift-evolution] PITCH: New :== operator for generic constraints

Charles Srstka cocoadev at charlessoft.com
Wed Aug 17 00:16:48 CDT 2016


> On Aug 16, 2016, at 11:42 PM, Slava Pestov <spestov at apple.com> wrote:
>> 
>> 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:

Agh, you’re right, I’d forgotten about that. It’s days like this that I miss Objective-C’s “It just works” dynamism. ;-)

>> 
>> 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.

Oh dear. I don’t know how confusing those things were, but if you Google the “self or associated type requirements” error to see how many people it has confused, I think you’ll find that it’d be quite hard for the more precise check to create more confusion than that. Perhaps I should write up a proposal to change this, if it’s easily enough done that it’s actually been done once already.

Imagine if other aspects of the system worked this way, like the ObjC bridge for instance. Imagine you had an NSObject-derived class that some ObjC code somewhere was calling, and the minute you add a method somewhere that returns a tuple or something, instead of just not giving ObjC access to that one method, it suddenly just up and said BZZT. NO OBJC BRIDGE FOR YOU. Wouldn’t that be frustrating?

The other trouble is that it’s not just confusing; it can very easily get in the way of your work even if you know exactly what’s going on, necessitating kludges like AnyHashable just to do things like have a dictionary that can take more than one key type (an example that’s particularly irritating since the only method you care about, hashValue, is just a plain old Int that doesn’t care about the Self requirement at all). I know that a while ago I ended up using my own Equatable substitute with an ObjC-style isEqual() method on some types, just because actually implementing Equatable was throwing a huge spanner into the rest of the design.

> 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 :-)

Well, the idea was to create an easier-to-implement alternative to self-conforming protocols, which could be done if :== were expanded to one function that uses ==, and another with the same body that uses :, because I was under the impression that the compiler team did not want to implement self-conforming protocols.

Charles

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160817/ce4de50f/attachment.html>


More information about the swift-evolution mailing list