Search code examples
swiftprotocolstype-aliasassociated-types

Create a copyable protocol default initializer


I got this functions:

public protocol ContentType {
    associatedtype T

    static var allTypes: [T] { get }
    static func getContentType(contentTypeId: String) -> T

    var contentTypeId: String? { get }
}

public protocol Copyable: class {
    associatedtype T

    static func copy(old: T, new: T)
}

public protocol CopyableContentType: class {
    associatedtype T
    init(existingContentType: T, newContentTypeId: String?)
}

I want to provide a default initializer for a class that:

  • Conforms to ContentType
  • Conforms to Copyable

With above code, I always do the same in the required init: calling the copy function of the implementing class. Is there any way to omit this duplicate code across classes while keeping this initializer? I thought of it adding to a protocol extension:

public extension CopyableContentType where Self: ContentType & Copyable {
    typealias T = Self // So wrong but I have no idea what to put here
                       // T should be the same T as the T usud in ContentType and Copyable
    init(existingContentType: T, newContentTypeId: String?) {
        Self.copy(old: existingContentType, new: self)
    }
}

But this results in a few errors. I do not know what to put in at the typealias and I can not use associatedtypes. Is there any way I can provide a default initializer that calls the copy function?


Solution

  • I want to provide a default initializer for a class that:

    • Conforms to ContentType
    • Conforms to Copyable

    I'll attempt to answer this question assuming what you mean is that:

    • You want to provide a default implementation of a init(existingContentType: T, newContentTypeId: String?) initializer, as blueprinted in ContentType, if the type conforming to ContentType also conforms to Copyable.

    Let's first have a look at your Copyable protocol. It might be due to omission of details/use case in your question, but I don't see the need of the associated type T here, as the protocol basically blueprints/promises "the class type conforming to Copyable, say e.g. TheClassType, will provide an implementation of the static copy(from: TheClassType, to: TheClassType) function" — where the conforming type (TheClassType) is just Self. I.e.:

    protocol Copyable: class {
        static func copy(from: Self, to: Self)
    }
    

    The same holds for ContentType: is there a need for an associatedType here, or is the ContentType of a given concrete type simply the type itself; i.e. Self? Before continuing, lets strip away the parts not relevant to your question:

    protocol ContentType {
        var contentTypeId: String? { get }
    }
    

    Now, prior for any initializer to copy (not assign - we are dealing with reference types) anything into self (e.g. value type members of self), self must have been initialized (or assigned to). So to allow providing a default implementation for a init(existingContentType: Self, newContentTypeId: String?) initializer of ContentType (in case of conformance to Copyable; Self is a class type)—where the implementation is intended to make use of the blueprinted copy(from: Self, to: Self) of copyable—the type conforming to ContentType must know of a way to initializer itself prior to the copying step. I.e., the ContentType needs to blueprint some initializer that can be used to initialize self in the "copying initializer" prior to invoking the copy(from:to) method. Let's simply blueprint init():

    protocol ContentType {
        var contentTypeId: String? { get set }
        init()
    }
    

    Now, as the ContentType blueprints a contentTypeId member, and the copying initializer contains a newContentTypeId parameter, it could be sensible to provide a default implemented initializer with contentTypeId as its only parameter; namely init(contentTypeId: String?):

    extension ContentType {
        init(contentTypeId: String?) {
            self.init()
            self.contentTypeId = contentTypeId
        }
    }
    

    With this in place, we can provide the default implemented init(existingContentType: Self, newContentTypeId: String?) initializer as a constrained extension to ContentType based on conformance of Self to Copyable:

    extension ContentType where Self: Copyable {
        init(existingContentType: Self, newContentTypeId: String?) {
            self.init(contentTypeId: newContentTypeId)
            Self.copy(from: existingContentType, to: self)
        }
    }
    

    Example

    Putting the above together:

    protocol Copyable: class {
        static func copy(from: Self, to: Self)
    }
    
    protocol ContentType {
        var contentTypeId: String? { get set }
        init()
    }
    
    extension ContentType {
        init(contentTypeId: String?) {
            self.init()
            self.contentTypeId = contentTypeId
        }
    }
    
    extension ContentType where Self: Copyable {
        init(existingContentType: Self, newContentTypeId: String?) {
            self.init(contentTypeId: newContentTypeId)
            Self.copy(from: existingContentType, to: self)
        }
    }
    

    Example:

    // Copyable content type
    final class Foo: ContentType, Copyable {
        // Note that since all stored properties have initial values,
        // the compiler provides a synthesized initializer for init().
        var contentTypeId: String?
        var data = 0
    
        static func copy(from: Foo, to: Foo) {
            to.data = from.data
        }
    }
    
    let foo1 = Foo(contentTypeId: "foo1")
    foo1.data = 42
    let foo2 = Foo(existingContentType: foo1, newContentTypeId: "foo2")
    
    print(foo1.contentTypeId ?? "None", foo1.data) // foo1 42
    print(foo2.contentTypeId ?? "None", foo2.data) // foo2 42
    
    
    // Non-copyable content type
    final class Bar: ContentType {
        var contentTypeId: String?
    } // Bar has no access to
      // init(existingContentType: Self, newContentTypeId: String?)