[swift-evolution] protocol can only be used as a generic constraint because it has Self or associated type requirements

Paul Cantrell cantrell at pobox.com
Mon Dec 14 11:19:40 CST 2015


> On Dec 14, 2015, at 10:01 AM, Dave Abrahams <dabrahams at apple.com> wrote:
>>     struct RequestBatch
>>         {
>>         var requests: [Request]  // Sadness. Despair. DOOOOM.
>>         
>>         func cancelAll()
>>             {
>>             for request in requests
>>                 { request.cancel() }
>>             }
>>         }
>> 
>> The current Swift solution is either to ditch the type safety of ContentType, or split Request into two protocols. The latter makes the API hard to read, and may decouple related protocol requirements that don’t make sense independently.
> 
> IMO the fact that you created an a array of requests and wrote cancelAll above *demonstrates* that the cancel requirement makes sense independently from the others.

…and therein lies the danger of small examples trimmer from larger projects. In the actual project, I end up with a family of related behaviors where the “cancel / everything else” separation no longer makes sense.

Here’s a little more context — still a super pared down toy example, but hopefully enough to demonstrate. What I want:

    protocol Resource
        {
        typealias ContentType
        
        var allRequests: [Request<ContentType>] { get }
        var latestData: ContentType? { get }
        func load() -> Request<ContentType>
        }

    protocol Request
        {
        typealias ContentType
        
        func onSuccess(callback: ContentType -> Void)
        func onFailure(callback: ErrorType -> Void)
        func cancel()
        }

    struct ResourceBatch
        {
        var observedResources: [Resource]
        var reloadsInProgress: Int = 0
        
        func reloadAll()
            {
            for resource in observedResources
                {
                reloadsInProgress += 1
                resource.load().onSuccess
                    { _ in self.reloadsInProgress -= 1 }
                }
            }
        
        func cancelAll()
            {
            for resource in observedResources
                {
                for request in resource.allRequests
                    { request.cancel() }
                }
            }
        }

And here’s the nonsense I have to engage in to make this work with Swift as it stands:

    protocol AnyResource
        {
        var allRequests: [AnyRequest] { get }
        var anyLatestData: Any? { get }
        func load() -> AnyRequest
        }

    protocol Resource: AnyResource
        {
        typealias ContentType
        
        var allRequests: [AnyRequest] { get }  // ← Still no way to make this typesafe
        var latestData: ContentType? { get }
        
        func load<RequestType: Request where RequestType.ContentType == Self.ContentType>() -> RequestType
        }

    protocol AnyRequest
        {
        func onSuccess(callback: Any -> Void)
        func onFailure(callback: ErrorType -> Void)
        func cancel()
        }

    protocol Request: AnyRequest
        {
        typealias ContentType
        
        func onSuccess(callback: ContentType -> Void)
        }

    struct ResourceBatch
        {
        var observedResources: [AnyResource]
        var reloadsInProgress: Int = 0
        
        mutating func reloadAll()
            {
            for resource in observedResources
                {
                reloadsInProgress += 1
                resource.load().onSuccess
                    { _ in self.reloadsInProgress -= 1 }
                }
            }
        
        func cancelAll()
            {
            for resource in observedResources
                {
                for request in resource.allRequests
                    { request.cancel() }
                }
            }
        }

Still just a sketch, but already the problems abound.

This Resource / AnyResource and Request / AnyRequest distinction makes no useful sense to the user of this library; it’s just there to satisfy Swift’s type system. “Resource whose ContentType is known at compile time” and “Resource whose ContentType is unknown at compile time” are still the same construct, conceptually and semantically, just with varying degrees of type information. They should not be two types.

I tried playing out this approach in Siesta, and it lead to a combinatorial explosion of duplicated methods, and numerous type inference problems related to the collision of methods that differed only by Any vs. ContentType. I wasn’t willing to inflict the resulting API on my users.

Maybe I’m missing something big. David, you certainly know Swift better than I do. If there’s a better way to do this, I would love to hear about it!

Cheers,

Paul

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


More information about the swift-evolution mailing list