Search code examples
swiftparametersrawrepresentable

How to use generic default parameters


This is my code:

class Person {
    init<T: RawRepresentable>(raw: T = Child.johnDoe) {}
}

enum Child: String {
    case johnDoe
}

It doesn't compile. The error is:

Default argument value of type 'Child' cannot be converted to type 'T'

Why can't it be converted? According to the docs, Child.someEnum is RawRepresentable:

Enumerations with Raw Values For any enumeration with a string, integer, or floating-point raw type, the Swift compiler automatically adds RawRepresentable conformance. When defining your own custom enumeration, you give it a raw type by specifying the raw type as the first item in the enumeration’s type inheritance list.

This also compiles:

class Person {
    static func accept<T: RawRepresentable>(raw: T) where T.RawValue == String {}
}

enum Child: String {
    case johnDoe
}

Person.accept(raw: Child.johnDoe)

Why doesn't it work as a default parameter?

Use case: I want to accept any RawPresentable value, so I can extract the rawValue from it. I want to provide a default value (always "") (I just create a struct with rawValue = ""). I do not want to create multiple initializers, since I got some subclasses and that would get a mess. The best for me is just to provide a default RawRepresentable object.

When I add a cast: init(ty: T = (Child.johnDoe as! T)) where T.RawValue == String {

}

Or make it nillable:

(ty: T? = nil)

It compiles. But now I can not call:

let x = Person()

It gives the error:

Generic parameter 'T' could not be inferred


Solution

  • This is certainly possible. However, you have to use your own protocol and add the default value to that protocol:

    protocol MyRawRepresentable: RawRepresentable {
        static var defaultValue: Self { get }
    }
    
    class Person {
        init<T: MyRawRepresentable>(raw: T = T.defaultValue) {}
    }
    
    enum Child: String, MyRawRepresentable {
        case johnDoe
    
        static let defaultValue: Child = .johnDoe
    }
    

    There is another issue though. How will you specify the generic type if you use the default parameter value and all you will have will be just Person.init()?

    The only solution I see is to also specify a default generic type which means you actually want:

    class Person {
       init<T: RawRepresentable>(raw: T) {
       }
    
       convenience init() {
           self.init(raw: Child.johnDoe)
       }
    }
    

    Unless you actually want to make Person itself a generic class because then you could just use

    Person<Child>.init()