swiftlang / swift

The Swift Programming Language
https://swift.org
Apache License 2.0
67.46k stars 10.35k forks source link

[SR-2940] Generic types cannot be refined with protocol conformance #45534

Open itaiferber opened 8 years ago

itaiferber commented 8 years ago
Previous ID SR-2940
Radar rdar://problem/28758896
Original Reporter @itaiferber
Type New Feature
Additional Detail from JIRA | | | |------------------|-----------------| |Votes | 1 | |Component/s | Compiler | |Labels | New Feature, LanguageFeatureRequest | |Assignee | @itaiferber | |Priority | Medium | md5: dc8627120fe0771eeb2e6cc79ec4e5dc

Issue Description:

As part of the design of an API (which we are implementing internally, but intend to also offer for third parties to implement), I have a class of things which are Fooable. Some things are, say PrimitiveFooable, while others are StructuredFooable:

protocol Fooable {}
protocol PrimitiveFooable: Fooable {}
protocol StructuredFooable: Fooable { ... }

What I would like to do is write a generic method which accepts all {Fooable}, but dispatches correctly on whether the parameter is PrimitiveFooable or StructuredFooable:

// Part of the public API.
func doFoo<T>(with value: T) where T: Fooable {
   // Send primitives to doPrimitiveFoo, and others to doStructuredFoo
}

// Part of private implementation.
fileprivate func doPrimitiveFoo<T>(with value: T) where T: PrimitiveFooable {
  // ...
}

fileprivate func doStructuredFoo<T>(with value: T) where T: StructuredFooable {
  // ...
}

However, it seems to currently be impossible to constrain the type T within doFoo in a type-safe manner:

func doFoo<T>(with value: T) where T: Fooable {
    if T is PrimitiveFooable {
        // The type T here is not refined to T: PrimitiveFooable
        doPrimitiveFoo(with: value) // "Argument type 'T' does not conform to expected type 'PrimitiveFooable"
    } else if T is StructuredFooable {
        // Same here
        doStructuredFoo(with: value)
    }

    if let v = value as? PrimitiveFooable {
        // The type of v is now just PrimitiveFooable; the type information of T is lost.
        doPrimitiveFoo(with: value) // "Cannot invoke 'doPrimitiveFoo' with an argument list of type "'(with: PrimitiveFooable)'"
    } else if let v = value as? StructuredFooable {
        // Same here
        doStructuredFoo(with: v)
    }

    // Unfortunately, & syntax is for protocols only:
    if let v = value as? T & PrimitiveFooable { // "Non-protocol type 'T' cannot be used within a protocol composition"
        ...
    }
}

The "right" solution might appear to be to overload the method based on type:

func doFoo<T>(with value: T) where T: PrimitiveFooable { ... }
func doFoo<T>(with value: T) where T: StructuredFooable { ... }

but in practice, this leads to an untenable number of overloads for this specific API:

protocol Fooer {
    func doFoo<T1, T2>(with: T1?, for: T2) where T1: Fooable, T2: Fooable
    func doFoo<T1, T2>(with: [T1], for: T2) where T1: Fooable, T2: Fooable
    func doFoo<T1, T2>(with: [T1?], for: T2) where T1: Fooable, T2: Fooable
    func doFoo<T1, T2, T3>(with: [T1: T2], for: T3) where T1: Fooable, T2: Fooable, T3: Fooable
    func doFoo<T1, T2, T3>(with: [T1: T2?], for: T3) where T1: Fooable, T2: Fooable, T3: Fooable
}

must necessarily then become

protocol Fooer {
    func doFoo<T1, T2>(with: T1?, for: T2) where T1: PrimitiveFooable, T2: PrimitiveFooable
    func doFoo<T1, T2>(with: T1?, for: T2) where T1: StructuredFooable, T2: PrimitiveFooable
    func doFoo<T1, T2>(with: T1?, for: T2) where T1: PrimitiveFooable, T2: StructuredFooable
    func doFoo<T1, T2>(with: T1?, for: T2) where T1: StructuredFooable, T2: StructuredFooable
    func doFoo<T1, T2>(with: [T1], for: T2) where T1: PrimitiveFooable, T2: PrimitiveFooable
    func doFoo<T1, T2>(with: [T1], for: T2) where T1: StructuredFooable, T2: PrimitiveFooable
    func doFoo<T1, T2>(with: [T1], for: T2) where T1: PrimitiveFooable, T2: StructuredFooable
    func doFoo<T1, T2>(with: [T1?], for: T2) where T1: StructuredFooable, T2: StructuredFooable
    func doFoo<T1, T2>(with: [T1?], for: T2) where T1: PrimitiveFooable, T2: PrimitiveFooable
    func doFoo<T1, T2>(with: [T1?], for: T2) where T1: StructuredFooable, T2: PrimitiveFooable
    func doFoo<T1, T2>(with: [T1?], for: T2) where T1: PrimitiveFooable, T2: StructuredFooable
    func doFoo<T1, T2>(with: [T1?], for: T2) where T1: StructuredFooable, T2: StructuredFooable
    func doFoo<T1, T2, T3>(with: [T1: T2], for: T3) where T1: PrimitiveFooable, T2: PrimitiveFooable, T3: PrimitiveFooable
    func doFoo<T1, T2, T3>(with: [T1: T2], for: T3) where T1: StructuredFooable, T2: PrimitiveFooable, T3: PrimitiveFooable
    func doFoo<T1, T2, T3>(with: [T1: T2], for: T3) where T1: PrimitiveFooable, T2: StructuredFooable, T3: PrimitiveFooable
    func doFoo<T1, T2, T3>(with: [T1: T2], for: T3) where T1: StructuredFooable, T2: StructuredFooable, T3: FooaPrimitiveFooableble
    func doFoo<T1, T2, T3>(with: [T1: T2], for: T3) where T1: PrimitiveFooable, T2: PrimitiveFooable, T3: StructuredFooable
    func doFoo<T1, T2, T3>(with: [T1: T2], for: T3) where T1: StructuredFooable, T2: PrimitiveFooable, T3: StructuredFooable
    func doFoo<T1, T2, T3>(with: [T1: T2], for: T3) where T1: PrimitiveFooable, T2: StructuredFooable, T3: StructuredFooable
    func doFoo<T1, T2, T3>(with: [T1: T2], for: T3) where T1: StructuredFooable, T2: StructuredFooable, T3: StructuredFooable
    func doFoo<T1, T2, T3>(with: [T1: T2?], for: T3) where T1: PrimitiveFooable, T2: PrimitiveFooable, T3: PrimitiveFooable
    func doFoo<T1, T2, T3>(with: [T1: T2?], for: T3) where T1: StructuredFooable, T2: PrimitiveFooable, T3: PrimitiveFooable
    func doFoo<T1, T2, T3>(with: [T1: T2?], for: T3) where T1: PrimitiveFooable, T2: StructuredFooable, T3: PrimitiveFooable
    func doFoo<T1, T2, T3>(with: [T1: T2?], for: T3) where T1: StructuredFooable, T2: StructuredFooable, T3: FooaPrimitiveFooableble
    func doFoo<T1, T2, T3>(with: [T1: T2?], for: T3) where T1: PrimitiveFooable, T2: PrimitiveFooable, T3: StructuredFooable
    func doFoo<T1, T2, T3>(with: [T1: T2?], for: T3) where T1: StructuredFooable, T2: PrimitiveFooable, T3: StructuredFooable
    func doFoo<T1, T2, T3>(with: [T1: T2?], for: T3) where T1: PrimitiveFooable, T2: StructuredFooable, T3: StructuredFooable
    func doFoo<T1, T2, T3>(with: [T1: T2?], for: T3) where T1: StructuredFooable, T2: StructuredFooable, T3: StructuredFooable
}

The implementations of these methods would be quite similar, causing a lot of unnecessary repetition and copypasta.

Unless there's some method or syntax for doing this that I've missed, this would be an extremely helpful addition. Right now, we work around this by making PrimitiveFooable conform to StructuredFooable (and fatalError() by default on its methods), which solves the duplication problem as we only have methods which take StructuredFooable (and check dynamically for PrimitiveFooable), but is not safe at all.

belkadan commented 8 years ago

Today the only fully general way to do this is to put requirements in Fooable that can then be satisfied by default implementations in PrimitiveFooable and StructuredFooable. In cases where there are no associated parameters and no self-requirements, you can also "get the T back out" by using an extension method:

protocol Fooable {}
protocol StructuredFooable: Fooable { /*...*/ }
extension StructuredFooable {
  func doStructuredFooThing() { print("structured!") }
}

func doFoo<T>(with value: T) where T: Fooable {
    if let v = value as? StructuredFooable {
        // Same here
        v.doStructuredFooThing()
    }
}

struct StructuredFooableImpl: StructuredFooable {}
doFoo(with: StructuredFooableImpl())
itaiferber commented 8 years ago

Thanks for the quick response, @belkadan!
Unfortunately, PrimitiveFooable and StructuredFooable are disparate, so there's no requirement I can place inside Fooable that really makes sense (what I truly need here is not Fooable but an anonymous union type of PrimitiveFooable | StructuredFooable, but Swift doesn't support that ATM), but I will keep exploring this.