[swift-evolution] Partially Constrained Protocols [Was: [Proposal] Separate protocols and interfaces]
David Waite
david at alkaline-solutions.com
Fri Jan 22 14:41:01 CST 2016
> 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 <mailto: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
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
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
>
>> 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.
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 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
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160122/9b37cec3/attachment-0001.html>
More information about the swift-evolution
mailing list