Search code examples
swiftprotocolsexistential-type

How to work around Swift not supporting first class meta types?


So I'm implementing the following:

  • A simple LanguageType protocol, which conforms to Hashable
  • A Translateable protocol, which should allow you to get (and set) a [String] from a dictionary, using a LanguageType as key

// MARK: - LanguageType

protocol LanguageType: Hashable {
    var description: String { get }
}

extension LanguageType {
    var description: String { return "\(Self.self)" }
    var hashValue: Int { return "\(Self.self)".hashValue }
}

func ==<T: LanguageType, U: LanguageType>(left: T, right: U) -> Bool {
    return left.description == right.description
}

// MARK: - Translateable

protocol Translateable {
    var translations: [LanguageType: [String]] { get set }
}

As usual, Swift has a problem with the way the LanguageType protocol is used:

Error

From what I've read, this has to do with Swift not supporting Existentials, which results in protocols not actually being first class types.

In the context of generics this problem can usually be solved with a type-erased wrapper.
In my case there are no generics or associated types though.

What I want to achieve is to have translations.Key to be any LanguageType, not just one generic type conforming to LanguageType.
So for example this wouldn't work:

protocol Translateable {
    typealias Language: LanguageType

    var translations: [Language: [String]] { get set }
}

For some reason I just can't think of a way to achieve this. I find it sounds like I need some kind of type-erased wrapper, as I want

translations.Key to be any LanguageType

I think I need to erase the exact type, which is supposed to conform to LanguageType in Translateable. What can I do to fix this issue?


Update 1: As just determined in this question, LanguageType actually has associated type requirements (do to it's conformance to Equatable). Therefore I will try to create a type-erased wrapper around LanguageType.

Update 2: So I've realized, that creating a type-erased wrapper for LanguageType won't actually resolve the problem. I've created AnyLanguage:

struct AnyLanguage<T>: LanguageType {
    private let _description: String
    var description: String { return _description }
    init<U: LanguageType>(_ language: U) { _description = language.description }
}

func ==<T, U>(left: AnyLanguage<T>, right: AnyLanguage<U>) -> Bool {
    return left.description == right.description
}

If I now used it in place of LanguageType it wouldn't do much, as Translateable would still require an associated type:

protocol Translateable {
    typealias T
    var translations: [AnyLanguage<T>: [String]] { get set }
}

Solution:

I removed the generic from AnyLanguage:

struct AnyLanguage: LanguageType {
    private(set) var description: String
    init<T: LanguageType>(_ language: T) { description = language.description }
}

func ==(left: AnyLanguage, right: AnyLanguage) -> Bool {
    return left.description == right.description
}

protocol Translateable {
    var translations: [AnyLanguage: [String]] { get set }
}

Not sure why I introduced T in Update 2, as it doesn't do anything. But this seems to work now.


Solution

  • The solution seems to be a type-erased wrapper. Type-erasure fixes the problem of not being able to use protocols with associated types (PATs) as first-class citizens, by creating a wrapper type, which only exposes the properties defined by the protocol, which it wraps.

    In this case, LanguageType is a PAT, due to its adoption of Equatable (which it conforms to, due to its adoption of Hashable):

    protocol LanguageType: Hashable { /*...*/ }
    

    Therefore it can not be used as a first-class type in the Translatable protocol:

    protocol Translatable {
        var translations: [LanguageType: [String]] { get set } // error
    }
    

    Defining an associated type for Translatable would not fix the problem, as this would constrain the LanguageType to be one specific type:

    protocol Translatable {
        typealias Language: LanguageType
    
        var translations: [Language: [String]] { get set } // works
    }    
    
    struct MyTranslatable<T: LanguageType>: Translatable {
        var translations: [T: [String]] // `T` can only be one specific type
    
        //...
    }
    

    As mentioned the solution is a type-erased wrapper AnyLanguage (Apple uses the same naming convention for their type-erased wrappers. For example AnySequence):

    // `AnyLanguage` exposes all of the properties defined by `LanguageType`
    // in this case, there's only the `description` property
    struct AnyLanguage: LanguageType {
        private(set) var description: String
    
        // `AnyLanguage` can be initialized with any type conforming to `LanguageType`
        init<T: LanguageType>(_ language: T) { description = language.description }
    }
    
    // needed for `AnyLanguage` to conform to `LanguageType`, as the protocol inherits for `Hashable`, which inherits from `Equatable`
    func ==(left: AnyLanguage, right: AnyLanguage) -> Bool {
        return left.description == right.description
    }
    
    // the use of `AnyLanguage` allows any `LanguageType` to be used as the dictionary's `Key`, as long as it is wrapped as `AnyLanguage`
    protocol Translateable {
        var translations: [AnyLanguage: [String]] { get set }
    }
    

    This implementation now allows the following:

    struct SomethingTranslatable: Translatable {
        var translations: [AnyLanguage: [String]] = [:]
    }
    
    func ==(left: SomethingTranslatable, right: SomethingTranslatable) -> Bool { /*return some `Bool`*/ }
    
    struct English: LanguageType { }
    struct German: LanguageType { }
    
    var something = SomethingTranslatable()
    something.translations[AnyLanguage(English())] = ["Hello", "World"]
    let germanWords = something.translations[AnyLanguage(German())]
    

    Different types, conforming to LanguageType, can now be used as the Key. The only syntactical difference, is the necessary initialization of an AnyLanguage:

    AnyLanguage(English())