We're trying to create a function addQueryItem which ultimately uses a string and an optional string internally.
For more flexibility in the API, rather than use String for the argument types, we are instead using CustomStringConvertible (which String implements) so we can use anything that can be represented as a string.
Additionally, so we can pass it String-based enums, we also want it to accept RawRepresentable types where RawValue is a CustomStringConvertible itself.
However, since we're now technically accepting two different kinds of values for each parameter, we end up having to create a 'matrix of overloads'--four total--for each combination of the two types.
My first thought was to use protocol-oriented programming by extending RawRepresentable so it adheres to CustomStringConvertible if its RawValue was also a CustomStringConvertible. Then I could just pass that directly to the version which takes two CustomStringConvertible arguments and eliminate the other three. However, the compiler didn't like it because I'm trying to extend a protocol, not a concrete type.
// This doesn't work
extension RawRepresentable : CustomStringConvertible
where RawValue:CustomStringConvertible {
var description: String {
return self.rawValue
}
}
As a result of not being able to do the above, as mentioned, I have to have all four of the following:
func addQueryItem(name:CustomStringConvertible, value:CustomStringConvertible?){
if let valueAsString = value.flatMap({ String(describing:$0) }) {
queryItems.append(name: String(describing:name), value: valueAsString)
}
}
func addQueryItem<TName:RawRepresentable>(name:TName, value:CustomStringConvertible?)
where TName.RawValue:CustomStringConvertible {
addQueryItem(name: name.rawValue, value: value)
}
func addQueryItem<TValue:RawRepresentable>(name:CustomStringConvertible, value:TValue?)
where TValue.RawValue:CustomStringConvertible {
addQueryItem(name: name, value: value?.rawValue)
}
func addQueryItem<TName:RawRepresentable, TValue:RawRepresentable>(name:TName, value:TValue?)
where TName.RawValue:CustomStringConvertible,
TValue.RawValue:CustomStringConvertible
{
addQueryItem(name: name.rawValue, value: value?.rawValue)
}
So, since it doesn't look like it's possible to make RawRepresentable to adhere to CustomStringConvertible, is there any other way to solve this 'matrix-of-overloads' issue?
To expand on my comments, I believe you're fighting the Swift type system. In Swift you generally should not try to auto-convert types. Callers should explicitly conform their types when they want a feature. So to your example of an
Orderenum, I believe it should be implemented this way:First, have a protocol for names and values:
Now for string-convertible enums, it's nice to not have to implement this yourself.
But, for type-safety, you need to explicitly conform to the protocol. This way you don't collide with things that didn't mean to be used this way.
Now maybe
QueryItemsreally has to take strings. OK.But the thing that wraps this can be type-safe. That way
Order.buyandPurchase.buydon't collide (because they can't both be passed):You can use the above to make it less type-safe (using things like
StringCustomConvertibleand makingQueryBuildernon-generic, which I do not recommend, but you can do it). But I would still strongly recommend that you have callers explicitly tag the types they plan to use this way by explicitly labelling (and nothing else) that they conform to the protocol.To show what the less-safe version would look like: