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
Order
enum, 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
QueryItems
really has to take strings. OK.But the thing that wraps this can be type-safe. That way
Order.buy
andPurchase.buy
don't collide (because they can't both be passed):You can use the above to make it less type-safe (using things like
StringCustomConvertible
and makingQueryBuilder
non-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: