Extending a constrained protocol for an array argument is not possible

82 views Asked by At

I'm going to explain it by an example. We have a protocol for force having firstName and lastName like:

protocol ProfileRepresentable {
    var firstName: String { get }
    var lastName: String { get }
}

the type we are going to use have these two, but in an optional form:

struct Profile {
    var firstName: String?
    var lastName: String?
}

so after conforming to the ProfileRepresentable, we will extend the ProfileRepresentable and try to return the value and a default one for nil state:

extension Profile: ProfileRepresentable { }
extension ProfileRepresentable where Self == Profile {
    var firstName: String { self.firstName ?? "NoFirstName" }
    var lastName: String { self.lastName ?? "NoLastName" }
}

So far so good

Now there is a similar flow for a list of Profiles.

protocol ProfilerRepresentable {
    var profiles: [ProfileRepresentable] { get }
}

struct Profiler {
    var profiles: [Profile]
}

First issue

conforming to ProfilerRepresentable does NOT automatically done the implementation as expected (since Profile already conforms to ProfileRepresentable)

extension Profiler: ProfilerRepresentable { }

Second Issue

Following the previous pattern, extending ProfilerRepresentable is not working as expected and it raises a warning:

⚠️ All paths through this function will call itself

extension ProfilerRepresentable where Self == Profiler {
    var profiles: [ProfileRepresentable] { self.profiles }
}

How can I achieve the goal for arrays by the way ?

2

There are 2 answers

2
Rob Napier On

[Profile] is not a subtype of [ProfileRepresentable]. (See Swift Generics & Upcasting for a related but distinct version of this question.) It can be converted through a compiler-provided copying step when passed as a parameter or assigned to a variable, but this is provided as a special-case for those very common uses. It doesn't apply generally.

How you should address this depends on what precisely you want to do with this type.

If you have an algorithm that relies on ProfilerRepresentable, then Asperi's solution is ideal and what I recommend. But going that way won't allow you to create a variable of type ProfileRepresentable or put ProfileRepresentable in an Array.

If you need variables or arrays of ProfilerRepresentable, then you should ask yourself what these protocols are really doing. What algorithms rely on these protocols, and what other reasonable implementations of ProfileRepresentable really make sense? In many cases, ProfileRepresentable should just be replaced with a simple Profile struct, and then have different init methods for creating it in different contexts. (This is what I recommend if your real problem looks a lot like your example, and Asperi's answer doesn't work for you.)

Ultimately you can create type erasers (AnyProfile), but I suggest exploring all other options (particularly redesigning how you do composition) first. Type erasers are perfect if your goal is to erase a complicated or private type (AnyPublisher), but that generally isn't what people mean when they reach for them.

But designing this requires knowing a more concrete goal. There is no general answer that universally applies.


Looking at your comments, there no problem with having multiple types for the same entity if they represent different things. Structs are values. It's fine to have both Double and Float types, even though every Float can also be represented as a Double. So in your case it looks like you just want Profile and PartialProfile structs, and an init that lets you convert one to the other.

struct Profile {
    var firstName: String
    var lastName: String
}

struct PartialProfile {
    var firstName: String?
    var lastName: String?
}

extension Profile {
    init(_ partial: PartialProfile) {
        self.firstName = partial.firstName ?? "NoFirstName"
        self.lastName = partial.lastName ?? "NoLastName"
    }
}

extension PartialProfile {
    init(_ profile: Profile) {
        self.firstName = profile.firstName
        self.lastName = profile.lastName
    }
}

It's possible that you have a lot of these, so this could get a bit tedious. There are many ways to deal with that depending on exactly the problem you're solving. (I recommend starting by writing concrete code, even if it causes a lot of duplication, and then seeing how to remove that duplication.)

One tool that could be useful would be Partial<Wrapped> (inspired by TypeScript) that would create an "optional" version of any non-optional struct:

@dynamicMemberLookup
struct Partial<Wrapped> {
    private var storage: [PartialKeyPath<Wrapped>: Any] = [:]

    subscript<T>(dynamicMember member: KeyPath<Wrapped, T>) -> T? {
        get { storage[member] as! T? }
        set { storage[member] = newValue }
    }
}

struct Profile {
    var firstName: String
    var lastName: String
    var age: Int
}

var p = Partial<Profile>()
p.firstName = "Bob"
p.firstName    // "Bob"
p.age          // nil

And a similar converter:

extension Profile {
    init(_ partial: Partial<Profile>) {
        self.firstName = partial.firstName ?? "NoFirstName"
        self.lastName = partial.lastName ?? "NoLastName"
        self.age = partial.age ?? 0
    }
}

Now moving on to your Array problem, switching between these is just a map.

var partials: [Partial<Profile>] = ...
let profiles = partials.map(Profile.init)

(Of course you could create an Array extension to make this a method like .asWrapped() if it were convenient.)

The other direction is slightly tedious in the simplest approach:

extension Partial where Wrapped == Profile {
    init(_ profile: Profile) {
        self.init()
        self.firstName = profile.firstName
        self.lastName = profile.lastName
        self.age = profile.age
    }
}

If there were a lot of types, it might be worth it to make Partial a little more complicated so you could avoid this. Here's one approach that allows Partial to still be mutable (which I expect would be valuable) while also allowing it to be trivially mapped from the wrapped instances.

@dynamicMemberLookup
struct Partial<Wrapped> {
    private var storage: [PartialKeyPath<Wrapped>: Any] = [:]
    private var wrapped: Wrapped?

    subscript<T>(dynamicMember member: KeyPath<Wrapped, T>) -> T? {
        get { storage[member] as! T? ?? wrapped?[keyPath: member] }
        set { storage[member] = newValue }
    }
}

extension Partial {
    init(_ wrapped: Wrapped) {
        self.init()
        self.wrapped = wrapped
    }
}

I don't love this solution; it has a weird quirk where partial.key = nil doesn't work to clear a value. But I don't have a nice fix until we get KeyPathIterable. But there are some other routes you could take depending on your precise problem. And of course things can be simpler if Partial isn't mutable.

The point is that there's no need for protocols here. Just values and structs, and convert between them when you need to. Dig into @dynamicMemberLookup. If your problems are very dynamic, then you may just want more dynamic types.

5
Asperi On

Here is possible solution. Tested with Xcode 12 / swift 5.3

protocol ProfilerRepresentable {
    associatedtype T:ProfileRepresentable
    var profiles: [T] { get }
}

extension Profiler: ProfilerRepresentable { }
struct Profiler {
    var profiles: [Profile]
}