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

Alexis Beingessner abeingessner at apple.com
Thu Aug 18 16:55:21 CDT 2016


Slava and Joe's suggestion of self conformance and the constraints involved are something that's been explored in other languages with similar systems. There's a great series of posts discussing some of the issues at hand:

These describe the constraints of interest (“object safety”):
http://huonw.github.io/blog/2015/01/object-safety/
http://huonw.github.io/blog/2015/05/where-self-meets-sized-revisiting-object-safety/

These are in the context of the Rust language, whose traits are similar in many ways to Swift protocols. These posts should give enough background to those familiar with protocols but not traits:
http://huonw.github.io/blog/2015/01/peeking-inside-trait-objects/
http://huonw.github.io/blog/2015/01/the-sized-trait/

One idea presented here that's applicable to Swift is the ability to distinguish between “static type” and “dynamic type” via the Sized trait (Sized referring to "statically sized"). By default most locations that work with generics implicitly assume Sized, which leads to more efficient code, and allows things like:

let x = T.staticMethod()

to make sense. However, if a piece of code isn’t interested in these capabilities, it can add ?Sized to the type constraint to “remove” the assumption, constraining the body of the method. Let’s call `?Sized` in Swift `MaybeExistential` (because that’s what it would mean). So you would write something like:

func foo<T: MyProto & MaybeExistential>(input: T) { … }

and the effect would be that MyProto existentials could be passed to this function in the knowledge that the function would be forbidden from calling static initializers and any other problematic methods. This isn’t a totally unprecedented strategy in Swift either: this is the same kind of idea behind `@noescape`; constraining the user of the generic type to empower the provider.

> On Aug 17, 2016, at 12:42 AM, Slava Pestov via swift-evolution <swift-evolution at swift.org> wrote:
> 
> 
>>> 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> 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
> 
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160818/46de8f0c/attachment.html>


More information about the swift-evolution mailing list