[swift-evolution] [swift-evolution-announce] [Review] SE-0089: Replace protocol<P1, P2> syntax with Any<P1, P2>

L Mihalkovic laurent.mihalkovic at gmail.com
Thu Jun 9 14:05:08 CDT 2016


> On Jun 9, 2016, at 4:55 PM, Dave Abrahams via swift-evolution <swift-evolution at swift.org> wrote:
> 
> 
> on Wed Jun 08 2016, Matthew Johnson <matthew-AT-anandabits.com <http://matthew-at-anandabits.com/>> wrote:
> 
>>> On Jun 8, 2016, at 1:33 PM, Dave Abrahams <dabrahams at apple.com> wrote:
>>> 
>>> 
>>> on Tue Jun 07 2016, Matthew Johnson <matthew-AT-anandabits.com> wrote:
>>> 
>> 
>>>>> On Jun 7, 2016, at 9:15 PM, Dave Abrahams <dabrahams at apple.com> wrote:
>>>>> 
>>>>> 
>>>>> on Tue Jun 07 2016, Matthew Johnson <matthew-AT-anandabits.com <http://matthew-at-anandabits.com/>> wrote:
>>>>> 
>>>> 
>>>>>>> On Jun 7, 2016, at 4:13 PM, Dave Abrahams via swift-evolution <swift-evolution at swift.org> wrote:
>>>>>>> 
>>>>>>> 
>>>>>>> on Tue Jun 07 2016, Matthew Johnson <swift-evolution at swift.org> wrote:
>>>>>>> 
>>>>>> 
>>>>>>>>> , but haven't realized
>>>>>>>>> that if you step around the type relationships encoded in Self
>>>>>>>>> requirements and associated types you end up with types that appear to
>>>>>>>>> interoperate but in fact trap at runtime unless used in exactly the
>>>>>>>>> right way.
>>>>>>>> 
>>>>>>>> Trap at runtime?  How so?  Generalized existentials should still be
>>>>>>>> type-safe.  
>>>>>>> 
>>>>>>> There are two choices when you erase static type relationships:
>>>>>>> 
>>>>>>> 1. Acheive type-safety by trapping at runtime
>>>>>>> 
>>>>>>> FloatingPoint(3.0 as Float) + FloatingPoint(3.0 as Double) // trap
>>>>>>> 
>>>>>>> 2. Don't expose protocol requirements that involve these relationships,
>>>>>>> which would prevent the code above from compiling and prevent
>>>>>>> FloatingPoint from conforming to itself.
>>>>>>> 
>>>>>>>> Or are you talking about the hypothetical types / behaviors people
>>>>>>>> think they want when they don’t fully understand what is happening...
>>>>>>> 
>>>>>>> I don't know what you mean here.  I think generalized existentials will
>>>>>>> be nice to have, but I think most people will want them to do something
>>>>>>> they can't possibly do.
>>>>>> 
>>>>>> Exactly.  What I meant is that people think they want that expression
>>>>>> to compile because they don’t understand that the only thing it can do
>>>>>> is trap.  I said “hypothetical” because producing a compile time error
>>>>>> rather than a runtime trap is the only sane thing to do.  Your comment
>>>>>> surprised me because I can’t imagine we would move forward in Swift
>>>>>> with the approach of trapping.
>>>>> 
>>>>> I would very much like to be able to create instances of “Collection
>>>>> where Element == Int” so we can throw away the wrappers in the stdlib.
>>>>> That will require some type mismatches to be caught at runtime via
>>>>> trapping.
>>>> 
>>>> For invalid index because the existential accepts a type erased index?
>>> 
>>> Exactly.
>>> 
>>>> How do you decide where to draw the line here?  It feels like a very
>>>> slippery slope for a language where safety is a stated priority to
>>>> start adopting a strategy of runtime trapping for something as
>>>> fundamental as how you expose members on an existential.
>>> 
>>> If you don't do this, the alternative is that “Collection where Element
>>> == Int” does not conform to Collection.  
>> 
>> This isn’t directly related to having self or associated type
>> requirements.  It is true of all existentials.  
> 
> That is just an implementation limitation today, IIUC.  What I'm talking
> about here would make it impossible for some to do that.
> 
>> If that changes for simple existentials and generalized existentials
>> expose all members (as in the latest draft of the proposal) maybe it
>> will be possible for all existentials to conform to their protocol.
> 
> Not without introducing runtime traps.  See my “subscript function”
> example.
> 
>> 
>>> That's weird and not very
>>> useful.  You could expose all the methods that were on protocol
>>> extensions of Collection on this existential, unless they used
>>> associated types other than the element type.  But you couldn't pass the
>>> existential to a generic function like
>>> 
>>>  func scrambled<C: Collection>(_ c: C) -> [C.Element]
>>> 
>>>> IMO you should *have* to introduce unsafe behavior like that manually.
>>> 
>>> Collection where Element == Int & Index == *
>>> 
>>> ?
>> 
>> I didn’t mean directly through the type of the existential.
> 
> My question is, why not?  That is still explicit.
> 
>> One obvious mechanism for introducing unsafe behavior is to write
>> manual type erasure wrappers like we do today.
>> 
>> Another possibility would be to allow extending the existential type
>> (not the protocol).  This would allow you to write overloads on the
>> Collection existential that takes some kind of type erased index if
>> that is what you want and either trap if you receive an invalid index
>> or better (IMO) return an `Element?`.  I’m not sure how extensions on
>> existentials might be implemented, but this is an example of the kind
>> of operation you might want available on it that you wouldn’t want
>> available on all Collection types.
>> 
>>> 
>>>> Collection indices are already something that isn’t fully statically
>>>> safe so I understand why you might want to allow this.  
>>> 
>>> By the same measure, so are Ints :-)
>>> 
>>> The fact that a type's methods have preconditions does *not* make it
>>> “statically unsafe.”
>> 
>> That depends on what you mean by safe.  Sure, those methods aren’t
>> going corrupt memory, but they *are* going to explicitly and
>> intentionally crash for some inputs.  That doesn’t qualify as “fully
>> safe” IMO.
> 
> Please pick a term other than “unsafe” here; it's not unsafe in the
> sense we mean the word in Swift.  It's safe in exactly the same way that
> array indexes and integers are.  When you violate a precondition, it
> traps.
> 
> The user doesn't do anything “manual” to introduce that trapping
> behavior for integers.  Preconditions are a natural part of most types.
> 
>>>> But I don’t think having the language's existentials do this
>>>> automatically is the right approach.  Maybe there is another approach
>>>> that could be used in targeted use cases where the less safe behavior
>>>> makes sense and is carefully designed.
>>> 
>>> Whether it makes sense or not really depends on the use-cases.  There's
>>> little point in generalizing existentials if the result isn't very useful.
>> 
>> Usefulness depends on your perspective.  
> 
> Of course.  As I've said, let's look at the use cases.
> 
>> I have run into several scenarios where they would be very useful
>> without needing to be prone to crashes when used incorrectly.  One
>> obvious basic use case is storing things in a heterogenous collection
>> where you bind .
> 
> bind what?
> 
>> 
>> 
>>> The way to find out is to take a look at the examples we currently have
>>> of protocols with associated types or Self requirements and consider
>>> what you'd be able to do with their existentials if type relationships
>>> couldn't be erased.  
>>> 
>>> We have known use-cases, currently emulated in the standard library, for
>>> existentials with erased type relationships.  *If* these represent the
>>> predominant use cases for something like generalized existentials, it
>>> seems to me that the language feature should support that.  Note: I have
>>> not seen anyone build an emulation of the other kind of generalized
>>> existential.  My theory: there's a good reason for that :-).
>> 
>> AFAIK (and I could be wrong) the only rules in the language that
>> require the compiler to synthesize a trap except using a nil IUO, `!`
>> on a nil Optional, and an invalid `as` cast .  These are all
>> syntactically explicit unsafe / dangerous operations.  All other traps
>> are in the standard library (array index, overflow, etc).  Most
>> important about all of these cases is that they have received direct
>> human consideration.
> 
> There is no distinction in the user model between what might be
> synthesized by the language and what appears on standard library types.
> 
>> Introducing a language (not library) mechanism that exposes members on
>> generalized existentials in a way that relies on runtime traps for
>> type safety feels to me like a pretty dramatic turn agains the stated
>> priority of safety.  It will mean you must understand exactly what is
>> going on and be extremely careful to use generalized existentials
>> without causing crashes.  This will either make Swift code much more
>> crashy or will scare people away from using generalized existentials
>> (and maybe both).  
> 
> I don't accept either of those statements without seeing some analysis
> of the use-cases.  For example, I don't believe that AnyCollection et al
> are particularly crash-prone.  The likelihood that you'll use the wrong
> index type with a collection is very, very low.  I'm less certain of
> what happens with Self requirements in real cases.
> 
>> Neither of those outcomes is good.
>> 
>> Collection indices are a somewhat special case as there is already a
>> strong precondition that people are familiar with because it would be
>> too costly to performance and arguably too annoying to deal with an
>> Optional result in every array lookup.  IMO that is why the library is
>> able to get away with it in the current type erased AnyCollection.
>> But this is not a good model for exposing any members on an
>> existential that do not already have a strong precondition that causes
>> a trap when violated.
>> 
>> I think a big reason why you maybe haven’t seen a lot of examples of
>> people writing type erased “existentials" is because it is a huge pain
>> in the neck to write this stuff manually today.  People may be
>> designing around the need for them.  I haven’t seen a huge sampling of
>> type erased “existentials" other people are writing but I haven’t
>> written any that introduce a trap like this.  The only traps are in
>> the “abstract" base class whose methods will never be called (and
>> wouldn’t even be implemented if they could be marked abstract).
>> 
>> What specific things do you think we need to be able to do that rely
>> on the compiler synthesizing a trap in the way it exposes the members
>> of the existential?
> 
> I don't know.  I'm saying, I don't think we understand the use-cases
> well enough to make a determination.
> 
>> Here are a few examples from Austin’s proposal that safely use
>> existential collections.  I don’t understand why you think this
>> approach is insufficient.  Maybe you could supply a concrete example
>> of a use case that can’t be written with the mechanism in Austin’s
>> proposal.
>> 
>> https://github.com/austinzheng/swift-evolution/blob/az-existentials/proposals/XXXX-enhanced-existentials.md#associated-types-and-member-exposure <https://github.com/austinzheng/swift-evolution/blob/az-existentials/proposals/XXXX-enhanced-existentials.md#associated-types-and-member-exposure> <https://github.com/austinzheng/swift-evolution/blob/az-existentials/proposals/XXXX-enhanced-existentials.md#associated-types-and-member-exposure <https://github.com/austinzheng/swift-evolution/blob/az-existentials/proposals/XXXX-enhanced-existentials.md#associated-types-and-member-exposure>>
>> 
>> let a : Any<Collection>
>> 
>> // A variable whose type is the Index associated type of the underlying
>> // concrete type of 'a'.
>> let theIndex : a.Index = ...
>> 
>> // A variable whose type is the Element associated type of the underlying
>> // concrete type of 'a'.
>> let theElement : a.Element = ...
>> 
>> // Given a mutable collection, swap its first and last items.
>> // Not a generic function. 
>> func swapFirstAndLast(inout collection: Any<BidirectionalMutableCollection>) {
>>    // firstIndex and lastIndex both have type "collection.Index"
>>    guard let firstIndex = collection.startIndex,
>>        lastIndex = collection.endIndex?.predecessor(collection) where lastIndex != firstIndex else {
>>            print("Nothing to do")
>>            return
>>    }
>> 
>>    // oldFirstItem has type "collection.Element"
>>    let oldFirstItem = collection[firstIndex]
>> 
>>    collection[firstIndex] = collection[lastIndex]
>>    collection[lastIndex] = oldFirstItem
>> }
>> 
>> var a : Any<BidirectionalMutableCollection where .Element == String> = ...
>> 
>> let input = "West Meoley"
>> 
>> // Not actually necessary, since the compiler knows "a.Element" is String.
>> // A fully constrained anonymous associated type is synonymous with the concrete
>> // type it's forced to take on, and the two are interchangeable.
>> // However, 'as' casting is still available if desired.
>> let anonymousInput = input as a.Element
>> 
>> a[a.startIndex] = anonymousInput
>> 
>> // as mentioned, this also works:
>> a[a.startIndex] = input
>> 
>> // If the collection allows it, set the first element in the collection to a given string.
>> func setFirstElementIn(inout collection: Any<Collection> toString string: String) {
>>    if let element = string as? collection.Element {
>>        // At this point, 'element' is of type "collection.Element"
>>        collection[collection.startIndex] = element
>>    }
>> }
> 
> Neither of these look like they actually make *use* of the fact that
> there's type erasure involved (and therefore should probably be written
> as generics?).  The interesting cases with Any<Collection...>, for the
> purposes of this discussion, arise when you have multiple instances of
> the same existential type that wrap different concrete types.
> 
> Another problem I see: in this new world, what is the model for choosing
> whether to write a function as a protocol extension/generic, or as a
> regular function taking existential parameters?  Given that either of
> the above could have been written either way, we need to be able to
> answer that question.  When existentials don't conform to their
> protocols, it seems to me that the most general thing to do is use
> existentials whenever you can, and only resort to using generics when
> forced by the type system.  This does not seem like a particularly good
> programming model to me, but I might be convinced otherwise.
> 
> Anyway, my overall point is that this all seems like something we *can*
> do and that nicely fills gaps in the type system, but not necessarily
> something we *should* do until we better understand what it's actually
> *for* and how it affects the programming model.
> 

playing with a different syntax.

https://gist.github.com/lmihalkovic/8aa66542f5cc4592e967bade260477ef

what’s missing is the syntax for opening such that it is possible to deal safely with the underlying concrete type (e.g. when dealing with 2 Equatable). But Doug had a nice strawman that can be streamlined.


> -- 
> Dave
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org <mailto:swift-evolution at swift.org>
> https://lists.swift.org/mailman/listinfo/swift-evolution <https://lists.swift.org/mailman/listinfo/swift-evolution>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160609/d55b8861/attachment.html>


More information about the swift-evolution mailing list