So I'm implementing the following:
LanguageType
protocol, which conforms to Hashable
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:
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 anyLanguageType
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 }
}
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.
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())