[swift-evolution] Partially Constrained Protocols [Was: [Proposal] Separate protocols and interfaces]

Maximilian Hünenberger m.huenenberger at me.com
Mon Jan 25 15:52:15 CST 2016


Inline

> Am 22.01.2016 um 21:41 schrieb David Waite <david at alkaline-solutions.com>:
> 
> 
>> On Jan 22, 2016, at 9:36 AM, Maximilian Hünenberger <m.huenenberger at me.com> wrote:
>> 
>> 
>> Am 22.01.2016 um 03:05 schrieb David Waite <david at alkaline-solutions.com>:
>> 
>> Since Hashable is only self constrained due to Equatable should we even allow this code?
>> 
>> let a: Equatable = 1
>> let b: Equatable = "2"
>> 
>> a == b
>> 
>> // in this case there could be a more general == function
>> func == <T: Equatable, U: Equatable>(x: T, y: U) -> Bool {
>>     if let yAsT = y as? T {
>>         return yAsT == x }
>>     return false
>> }
> 
> That is fun code :-) But I don’t understand what it has to do with Hashable

It's more an issue with Equatable which has self constraints.

> Since today equatable mandates a defined, public == method, and I think the generic is always loses to that specificity. You don’t even need dynamic casting, because your generic version only gets called when there isn’t a non-generic version.
> 
> // in this case there could be a more general == function
> func == <T: Equatable, U: Equatable>(x: T, y: U) -> Bool {
>     return false
> }
> 
> 1 == 1 // true
> 1 == “1” // false

I thought also of this example:

let a: Equatable = 1
let b: Equatable = 2

a == b // would fail in your case

> Swift doesn’t seem to fight as hard as other languages against overloads that may be ambiguous in some contexts (for instance, two methods identical except for different return types). The flexibility of the compiler in interpreting code is one of the reasons that implicit conversions (such as with optionals or objective C bridging types) can lead to such confusing behavior in code. Most recent case to blow my mind,
> 
> let a = [[1]]
> a == a // error: binary operator '==' cannot be applied to two '[Array<Int>]' operands
> 
> import Foundation
> 
> a == a // $R2: Bool = true
> 
> This works because the compiler promotes [[1]] to either [NSArray] or an NSArray outright to get to a working == operator.
> 
> 
>> But it seems like that there has to be a different implementation of `self` constrained functions (like ==) if a self constrained protocol is used without specifying `self`.
> 
> Thats what we mean by opening the type - exposing enough of the dynamic type information to the calling context such that we can safely expose methods. E.g. Something like this would be equivalent to your earlier fun code, except now the call site of the function is not generically constrained:
> 
> let a:Equatable = 1
> let b:Equatable = “1”
> 
> func ==(lhs:Equatable, rhs:Equatable) -> Bool {
>     typealias T = lhs.dynamicType
>     let x = lhs as! T
>     if let y = rhs as? T {
>          return x == y
>     }
>     return false
> }
> 
> a == b // false

That's exactly what I wanted to do :)

> 
>> 
>>> Protocols would not have a simplistic order to implement (for example, I could be extending two parent protocols, both with their own associated types)
>> 
>> Do you mean conforming to a protocol by "extending"?
>> In this case it would be even more explicit about types:
> 
> 
> Sorry, I used Java parlance where interfaces can extend other interfaces to add additional requirements/exposed capabilities.
> 
> The specific example I had in mind is CollectionType, which inherits from both SequenceType and Indexable. Would Index come before or after Generator and SubSequence if you were positionally declaring arguments. Should swapping the inheritance order on CollectionType be a fragile change?
> 
> 
>> To be clear, I'm proposing:
>> - Generic syntax for all types including protocols to be the same
>> - Named generic parameters eg: Array<Element: Int>  ,  CollectionType<Element: String, Index: Int>
> 
> I’m not a fan of this syntax, because in an example where clause “where Element:Number" is a covariant constraint, and here (I think) you are using it as an invariant constraint. This seems confusing unless you specify Array<Element == Int>, etc.
> 
> You have the issue today that SequenceType does not expose Element but Generator. This kind of relationship can’t be expressed without a ‘where’ type syntax. I did sketch out a version of SequenceType that uses Element explicitly, so I assume this is what you are basing on. Still, such design can’t be assumed universal.
> 
>> - Without name: Array<Int>  ,  CollectionType<String, Int>
> I already pointed out the CollectionType issue with positional arguments above. In addition, SequenceType will have an associated type of SubSequence, which I assume you are assuming would be the third argument and are implicitly stating “don’t care”.
> 
>>> I personally think there is a bit too much overlap between ‘where’ syntax, and possibly conflicting usage. In your example, would Index: Foo indicate that index is exactly Foo, is Foo or some subtype, or is a concrete implementation of Foo? If there were two syntaxes, I’d hope one to be a much closer shortening of the other.
>> 
>> Index: Foo
>> Index is of type Foo or a Subtype.
> 
> In that, the covariant case, Array<Element:Number> is _different_ than Array<Number> today.
> 
> Today, Array<Number> and Array<Int> are different concrete types. One deals in terms of Numbers, and one in terms of Ints. They are structured in memory different, etc.
> 
> When I say partial constraints, applied to generics in this case (reluctantly), I’m saying the following:
> 
> Implicitly define a protocol around all concrete Array instances where Element is defined to be Number or a subtype of Number, exposing all methods, properties, etc which are safe.
> 
> Safety rules are equivalency rules here: If a method or property returns Element, I can say that it is safe to represent the result of “var first:Element {get}” as a Number, because in the cases where it returns a subtype like Int, I can still safely upcast to Number. I have a safe and uniform interface.
> 
> If a method takes an argument or for any property setter, I have to require Element to be invariant. Modifying the first item of an array isn’t safe, because I might try to assign an Int value when really its a UInt8 array.

Why Element has to be invariant?

let uintArray: [UInt8] = [1]
var covariantArray: [Number] = uintArray
// this doesn't cause an error since Int is also a Number
covariantArray[0] = Int(36754)

> 
> Subscripts unfortunately require both get and set, so even the getter has to be hidden currently if the type isn’t invariant. I’m not sure why the language requires this, TBH
> 
> That we are constraining the exposure of a protocol to only expose what is available by the associated type rules we specified is why I stayed away from terse, generic type syntax to start the proposal. Eventually, consensus might be that generic type form is better, but it adds an additional complexity to the understanding of the type model, which is the important part now in discussion. You are now using an existing syntax to refer to a new thing with new properties.
> 
> This is even more confusing if you partially constrain a generic type, because now Array<Number> (aka Array<where Element == Number> and Array<where Element:Number> have different properties (one is a protocol while the other is a concrete type, functionality is hidden because it is unsafe, possible boxing onto the heap and dynamic dispatch, etc)

I don't think that this would be confusing since `where` indicates that the type is used in a more generic way.


You've convinced me. I wanted to keep the generic syntax simple but having thought over it your proposal it much better in handling more complex cases without introducing additional syntax.
Also considering Swifts type inference where types with long names are easier to use.


A "small" overview of several examples:

Using the former example of CollectionType which is used as a more concrete type (using a "leading dot" which is the same as `Self.`).

// These examples are only a demonstration of how you could use collection type

CollectionType<where .Generator.Element == String, .Index == Int, .SubSequence: Self>

CollectionType<where .Generator.Element == String, .Index == Int>

CollectionType<where .Generator.Element == String>

// if having `Element` as associated type
CollectionType<where .Element == String>


// covariance and invariance

protocol A {}
extension Int: A {}

let intArray: [Int] = [1, 2, 3]
let aArray: [A] = [1, 2, 3]

var col1: CollectionType<where .Element == A>
col1 = intArray // doesn't work since Int != A
col1 = aArray // obviously works

var col2: CollectionType<where .Element: A>
// both work since `Int` and `A` conform to `A` (currently in Swift 2.2 `A` does not conform to `A`, I don't know if this is a feature)
col2 = intArray
col2 = aArray


// with a concrete type using the example above:

// replace type of `col1` with (all replacements are equivalent)
`Array<A>` or `Array<where .Element == A>` or `[A]` or `[where .Element == A]`

// replace type of `col2` with (all replacements are equivalent)
`Array<where .Element: A>` or `[where .Element: A]`


// to be discussed: using many protocols together with protocol<>

// declaring a generic type T which is used in two protocols
protocol<where T, Self: SequenceType<where .Element == T>, Self: Indexable<where .Index == T>>

// although in this case it can be written as
SequenceType<where T, .Element == T, Self: Indexable<where .Index == T>>

Even though the latter one is shorter I'm skeptical about using `Self:` in protocol where clauses since at a first glance it implies that the type is only a `SequenceType`.

>>> 
>>> 
>>> I do have a mild personal conflict around whether exposing the ability to have partially constrained generics is a good idea, but I admit my hesitation is based on philosophies of good application and API design, and not based on language design.
>>> 
>>>  implicit protocols on types seem pragmatic, because otherwise I’d have to create a protocol just for others to use me. 
>>> 
>>> Allowing a protocol with associated types to be partially constrained seems pragmatic to me, since otherwise I have to use one of the workarounds above (hand-coded new protocol, generic constraints ever, and/or type-erasure struct-wrapper).
>>> 
>>> Expanding the usage of the implicit protocol on some generic type seems anti-pragmatic, just because it allows someone to go longer before they really evaluate whether locking consumers of their API into a single implementation is a good idea. They can always declare a protocol explicitly if they want partially constrained behavior.
>> 
>> That was also my concern since I want to keep the generic syntax consistent with protocols. So maybe we could make it the same in case of generic parameter names but without the "_" so there are no protocol like behaviors for generic types.
>> 
>> Even though it would be nice to have a covariant Array< _ > type.
> 
> Yep :-) I can’t speak for the core team, but from my perspective whether this proposal lives or dies depends on:
> 
> 1. Implementability
> 2. Abiding by the Principal of Least Surprise
> 
> -DW

Do you mean my proposal by "this proposal"?

Assuming you meant the proposal in general:

I think it can be implemented since the types are like generics in functions with where clauses. This proposal is almost only syntactic sugar.

I also think that it would not be surprising to offer this functionality because "where clauses" are quite well understood and I wouldn't count adding a new syntax as surprising.

- Maximilian

PS: Have you made a formal proposal already?
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160125/b7592577/attachment.html>


More information about the swift-evolution mailing list