iosswiftenumsprotocolskeypaths

Dynamic member lookup with multiple keypaths for enums with associated values


Implementing dynamic member lookup on an enum with associated values leads to build errors when there is more than one keypath function.

I have an enum with multiple associates values, as described below:

@dynamicMemberLookup
enum Media {
    case movie(Movie)
    case book(Book)
    
    subscript<T>(dynamicMember keyPath: KeyPath<Entertainment, T>) -> T {
        switch self {
            case .movie(let movie):
                return movie[keyPath: keyPath]
            case .book(let book):
                return book[keyPath: keyPath]
        }
    }
}

The structs Movie and Book inherit protocols Filmed and Written, which themselves inherit from the more generic protocol Entertainment:

protocol Entertainment {
    var isbn: Int { get }
    var title: String { get }
    var sales: Int { get set }
}

protocol Written: Entertainment {
    var author: String { get }
}

protocol Filmed: Entertainment {
    var actors: [String] { get set }
}

struct Movie: Filmed {
    var isbn: Int
    var title: String
    var sales: Int
    var actors: [String]
}

struct Book: Written {
    var isbn: Int
    var title: String
    var sales: Int
    var author: String
}

So far, this works and has allowed me to access variables such as isbn and title, which are defined in Entertainment, from the top-level Media object through the subscript keypath lookup.

let media: Media = .movie(Movie(isbn: 1011, title: "Top Gun", sales: 1000, actors: ["Tom Cruise", "Miles Teller"]))

print(media.title)
print(media.sales)

Now, when I try to add the subscript function below in Media so that I can optionally access variables conforming to either Filmed or Written, I receive an error for the above print statements stating Ambiguous use of 'subscript(dynamicMember:)'

subscript<T>(dynamicMember keyPath: KeyPath<Filmed, T>) -> T? {
    switch self {
        case .movie(let movie):
            return movie[keyPath: keyPath]
        default:
            return nil
    }
}

In this example, the build errors are as follows:

let media: Media = .movie(Movie(isbn: 1011, title: "Top Gun", sales: 1000, actors: ["Tom Cruise", "Miles Teller"]))

print(media.title) // BUILD ERROR: Ambiguous use of 'subscript(dynamicMember:)'
print(media.sales) // BUILD ERROR: Ambiguous use of 'subscript(dynamicMember:)'
print(media.actors) // This doesn't throw an error and successfully logs

When I remove the second subscript function, the build errors are gone.

Please let me know if I'm doing anything wrong in my implementation here.


Solution

  • Figured out the answer:

    By changing Filmed and Written to no longer conform to Entertainment, the build errors were removed. The code as following now works:

    protocol Entertainment {
        var isbn: Int { get }
        var title: String { get }
        var sales: Int { get set }
    }
    
    protocol Written {
        var author: String { get }
    }
    
    protocol Filmed {
        var actors: [String] { get set }
    }
    
    struct Movie: Entertainment, Filmed {
        var isbn: Int
        var title: String
        var sales: Int
        var actors: [String]
    }
    
    struct Book: Entertainment, Written {
        var isbn: Int
        var title: String
        var sales: Int
        var author: String
    }
    
    @dynamicMemberLookup
    enum Media {
        case movie(Movie)
        case book(Book)
        
        subscript<T>(dynamicMember keyPath: KeyPath<Entertainment, T>) -> T {
            switch self {
                case .movie(let movie):
                    return movie[keyPath: keyPath]
                case .book(let book):
                    return book[keyPath: keyPath]
            }
        }
    
        subscript<T>(dynamicMember keyPath: KeyPath<Filmed, T>) -> T? {
            switch self {
                case .movie(let movie):
                    return movie[keyPath: keyPath]
                default:
                    return nil
            }
        }
    }
    

    Here is the output of the print statements:

    let media: Media = .movie(Movie(isbn: 1011, title: "Top Gun", sales: 1000, actors: ["Tom Cruise", "Miles Teller"]))
    
    print(media.title) // OUTPUT: "Tom Cruise"
    print(media.sales) // OUTPUT: 1000
    print(media.actors) // OUTPUT: Optional<[String]>: ["Tom Cruise", "Miles Teller"]