1

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?

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
  • 1
    For me it feers strange to use a protocol like `CustomStringConvertible` in this way and make it a requirement for a property in another protocol. Also note that in the documentation you’re discouraged from accessing the `description` property directly. – Joakim Danielson Jul 23 '18 at 17:35
  • Thanks for the heads-up to use `String(describing:x)` instead of `x.description`. However, we're not making it a requirement for a property in another protocol. We're simply trying to make that protocol adhere to it. – Mark A. Donohoe Jul 23 '18 at 17:42
  • 2
    `extension RawRepresentable : CustomStringConvertible where RawValue:CustomStringConvertible` is forbidden because if it were allowed, it would preclude any `RawRepresentable` type from having its own `CustomStringConvertible` implementation (as a type cannot conform to a protocol more than once). Like Joakim, I don't think this is an idiomatic usage of `CustomStringConvertible`. The protocol really only serves as a customisation point for types to customise their printed output, and shouldn't be used as a generic constraint or protocol-typed value – conformance is an implementation detail. – Hamish Jul 23 '18 at 17:51
  • So then help me think of another way to achieve this... where I want the function to take anything that can be represented as a string, either directly, or via it's rawValue if it's a raw representable. Is there anything better than what I've created? – Mark A. Donohoe Jul 23 '18 at 17:54
  • 1
    @MarqueIV The thing is that *any* value can be represented as a String – `String(describing:)` will spit out a string for anything, regardless of `CustomStringConvertible` conformance (hence why it's an implementation detail). I'm not sure I fully understand your concrete use case, but perhaps you want a protocol that represents a type that can be turned into a query name or value? You'd have to specify the conformance for your enums explicitly, but I think that's A Good Thing, as not all `String` backed enums should be usable as URL query values. – Hamish Jul 23 '18 at 18:20
  • Perhaps not entirely relevant to your use case, but I think a pretty neat way to do URL query encoding is with `Encodable`, see https://gist.github.com/hamishknight/d1cabdf19cce90ca8458da9294562542. – Hamish Jul 23 '18 at 18:20
  • @Hamish, I know what you mean about anything being able to be input to `String(describing:)` but that's precisely why we used 'CustomStringConvertible' instead of 'Any'. The same argument could be made that any string constant could be passed even if it has nothing to do with query values, so the restriction to only `CustomStringConvertible` makes sense, at least to us. I guess we could make them both `Any`, then test for a `RawRepresentable` within the function, and if not, feed it to `String(describing:)` but I'm trying to get the compiler to do as much as possible. – Mark A. Donohoe Jul 23 '18 at 18:33
  • I'm going to rewrite the question without using CustomStringConvertible because I think it's detracting from what I'm actually asking. Stay tuned... – Mark A. Donohoe Jul 23 '18 at 18:38
  • Please don't post fake code. Your very first protocol, Stringable, all by itself, is invalid syntax and won't compile. Post _real_ code. Thanks. – matt Jul 23 '18 at 19:00
  • Also Rob has already told you that you can't use an extension to conform a protocol to a protocol. Yet here you are trying to say `extension RawRepresentable : Stringable`. Is the rule not plain enough? – matt Jul 23 '18 at 19:03
  • I had real code. The topic went in the wrong direction focusing on CustomStringConvertible instead of my question. I'll put that code back again. And that's why I also said that code didn't work. I had that *before* his answer (when using CustomStringConvertible). I left it there for posterity, but sure, get snarky here. That helps! :) – Mark A. Donohoe Jul 23 '18 at 19:11
  • 1
    I think the point that many commenters are circling is that this feels very over-flexible in a way that makes it over-complicated. What kind of use case do you have where you need this in your current program, and don't know the list of specific types that are passed? (Trying to build generic Swift code "just in case it might be needed someday" is step 1 of dozens of hair-pulling sessions on Stack Overflow. Make sure you need this complexity before you create it.) Can you give examples of the kinds of calls you're having to deal with? Swift will fight you on auto-stringing things. – Rob Napier Jul 23 '18 at 19:36
  • We do need it. This example which illustrates the question is from a generic library that several external teams need, therefore we don't have access to the enums they may be passing. For instance, if they have an OrderType enum, and there's an order type query param, we want them to simply pass the enum like `OrderType.buy` rather than `OrderType.buy.rawValue`. We've been successfully using it for over a year now and our API is praised by other teams. However, we had to add yet another similar function that required another four methods, hence my posting this question. Make sense? – Mark A. Donohoe Jul 23 '18 at 19:48
  • In other words, people seem to be focusing on the specific example of the question rather than the actual question, which is... is there a way to simplify the overloads for things that take disparate types but which can all be represented by a common type, in this example, a string? – Mark A. Donohoe Jul 23 '18 at 19:50
  • I believe you're having trouble because this fights the Swift type system. If `OrderType.buy` and `PurchaseType.buy` are passed to the same object, you're going to have collisions. Swift works hard to avoid this happening by accident, and discourages stringly-typing. Instead, Swift typically handles this by parameterizing the type as `QueryBuilder` so that it cannot collide with `QueryBuilder`. (See `CodingKeys` and `Dictionary` for inspiration.) I understand if you don't want to do that, but I am not surprised that you're encountering headaches. Best of luck. – Rob Napier Jul 23 '18 at 20:01
  • (The short answer is that there should almost certainly be a protocol of your own like `QueryName` that consumers of your system conform their enum to explicitly (which may have default implementations available). Much like `Encodable`, but not trying to take over an existing protocol like `CustomStringConvertible` or `RawRepresentatable`. ) – Rob Napier Jul 23 '18 at 20:02
  • But then you wouldn't be able to pass pure strings unless you made String conform to it, which means *every* string would conform to it. That's why we've gone full-circle back to the original point... `String` is what we're after at the end, not a custom protocol. Does that make things more clear? In other words, we're not 'repurposing' CustomStringConvertible, we're counting on it! – Mark A. Donohoe Jul 23 '18 at 20:07
  • I guess what it comes down to is what we have, the matrix of overloads, is the only way to achieve what we want since we can't explicitly target an Enum (since there is no 'enum' base class), nor can we make a protocol conform to another with an extension. As mentioned before, we *could* simply pass `Any`, but that puts the work on at runtime, not compile time. Then again, that's also the simplest and most flexible since we could test for a String-based RawRepresentable, and everything else can be fed through `String(describing:)` – Mark A. Donohoe Jul 23 '18 at 20:13

2 Answers2

1

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:

protocol QueryName {
    var queryName: String { get }
}

protocol QueryValue {
    var queryValue: String { get }
}

Now for string-convertible enums, it's nice to not have to implement this yourself.

extension QueryName where Self: RawRepresentable, Self.RawValue == String  {
    var queryName: String { return self.rawValue }
}

extension QueryValue where Self: RawRepresentable, Self.RawValue == String  {
    var queryValue: String { return self.rawValue }
}

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.

enum Order: String, RawRepresentable, QueryName {
    case buy
}

enum Item: String, RawRepresentable, QueryValue {
    case widget
}

Now maybe QueryItems really has to take strings. OK.

class QueryItems {
    func append(name: String, value: String) {}
}

But the thing that wraps this can be type-safe. That way Order.buy and Purchase.buy don't collide (because they can't both be passed):

class QueryBuilder<Name: QueryName, Value: QueryValue> {
    var queryItems = QueryItems()

    func addQueryItem(name: QueryName, value: QueryValue?) {
        if let value = value {
            queryItems.append(name: name.queryName, value: value.queryValue)
        }
    }
}

You can use the above to make it less type-safe (using things like StringCustomConvertible and making QueryBuilder 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:

protocol QueryName {
    var queryName: String { get }
}

protocol QueryValue {
    var queryValue: String { get }
}

extension QueryName where Self: RawRepresentable, Self.RawValue == String  {
    var queryName: String { return self.rawValue }
}

extension QueryValue where Self: RawRepresentable, Self.RawValue == String  {
    var queryValue: String { return self.rawValue }
}

extension QueryName where Self: CustomStringConvertible {
    var queryName: String { return self.description }
}

extension QueryValue where Self: CustomStringConvertible {
    var queryValue: String { return self.description }
}


class QueryItems {
    func append(name: String, value: String) {}
}

class QueryBuilder {
    var queryItems = QueryItems()

    func addQueryItem<Name: QueryName, Value: QueryValue>(name: Name, value: Value?) {
        if let value = value {
            queryItems.append(name: name.queryName, value: value.queryValue)
        }
    }
}

enum Order: String, RawRepresentable, QueryName {
    case buy
}

enum Item: String, RawRepresentable, QueryValue {
    case widget
}
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Again, the QueryParams is just an example of something that takes string values. Those same values can also be used (keeping the same theme) as headers, or as string-based key-value pairs in our string-based dictionaries. Generally, as a rule of thumb, rather than creating string constants, we try to use Enums, but that can't always be the case. That's why ultimately, we want something that represents a string, but with more context. But just context, not more information. Ultimately we want to say 'Item 'X' can be represented as a string'. That's it. With the above (continued...) – Mark A. Donohoe Jul 23 '18 at 20:21
  • We would have to create a protocol for every use-case of the values even though every use-case ultimately ends up with a string. That doesn't add clarity *in this case!*. Of course there are plenty of other cases where everything you've stated above is spot-on, but in this case, the focus is on that it's ultimately representing a string, and therefore wherever we would normally simply accept a string, we'd like to also pass *any* RawRepresentables that are represented by a string. No reason to discuss further. I'll just stick with the overloads. They work and are clear to our clients. – Mark A. Donohoe Jul 23 '18 at 20:25
  • When you say "can be represented as a string" do you really mean that, or do you men "can be represented as a *unique* string" or do you really mean "can be represented as a unique string that is appropriate for this purpose?" If it's just "can be represented as a string" that is literally everything in Swift. `"\(value)"`. And `CustomStringConvertible` promises nothing more than that. But you seem to want a stronger promise. (If your clients are happy, everyone should be happy; but it's useful to see why Swift is fighting you here. You're twisting protocols beyond their meaning I think.) – Rob Napier Jul 23 '18 at 20:25
  • Best of luck. You don't have to you convince me; you know your use case better than I do. – Rob Napier Jul 23 '18 at 20:27
  • No I don't mean unique. I mean a string. Any string. Period. And yes anything *can* be represented as a string in Swift, as pointed out by the `String(describing:)` which is exactly why I was focusing on CustomStringConvertible since everything does *not* conform to that, but String does. All I wanted was to also make RawRepresentable conform to that if it was based on strings. Short answer: can't be done, so sticking with the overloads it is! – Mark A. Donohoe Jul 23 '18 at 20:28
  • Believe me, I understand everyone is just trying to help, but it's frustrating when I ask specific questions for specific reasons, and it goes off the rails. If I don't post an example, I get zero replies. When I do, people start to challenge the example instead of the question. I could switch the examples, but the same thing just happens over and over. Almost all of my dialog here is around CustomStringConvertible instead of the question of reducing the matrix of functions. Again, I know people are trying to help, but it's exhausting at times trying to keep the topic on track. – Mark A. Donohoe Jul 23 '18 at 20:33
  • Understood, but the underlying problem is there is no answer. So either people will not respond, respond "there is no answer" (which most people avoid doing because no one knows for 100% certainty that there is no answer), or they will try to help you adjust your problem to have an answer. Swift does not allow (and I believe it *intentionally* does not allow because of the many corner cases it creates) you do this. You could just create a single `MyStringingProtocol`. But callers will need to explicitly conform (with default implementations as above). This is intentional. – Rob Napier Jul 23 '18 at 20:59
  • There is no need to create a separate protocol for every use case; you can just have one. But you can't cause types outside your control to conform automatically to protocols. Instead you do what I did above: you make it very easy to explicitly conform by giving default impls. – Rob Napier Jul 23 '18 at 21:02
  • Then how do you give a default implementation for ***any*** enums based on strings? I thought the only way to target that was to use RawRepresentable, which is a protocol, thus a non-starter. – Mark A. Donohoe Jul 23 '18 at 21:04
  • Look at my `extension QueryValue where Self: RawRepresentable, Self.RawValue == String`. Replace `QueryValue` with `WhateverNameYouWant`. Then look at `Order`. (Also, I believe you can trivially do `extension Order: QueryValue {}` separately, even in a separate module.) – Rob Napier Jul 23 '18 at 21:04
  • But if you do that, then you're not allowing ***any*** enum based on a string, only those you decorate. So let me ask it that way... how can you write a function that takes either a string, or an enum based on a string *without the author of the enum having to decorate their enum*. I'm trying to write it so simply decorating it as a String is sufficient. – Mark A. Donohoe Jul 23 '18 at 21:07
  • At this point I'm just asking for fun because I've already checked in the code with the matrix of overloads. – Mark A. Donohoe Jul 23 '18 at 21:07
  • That is the thing I'm saying is intentionally impossible. Someone (either the author or someone who writes an extension) must explicitly say "this enum should be usable for this." It is not possible to automatically imply this. But all they must do is say "please give me this power." They don't have to write any further implementations. – Rob Napier Jul 23 '18 at 21:08
  • Guess we're just answering different questions. I want ***any** RawRepresentable via String to be usable, alongside using strings. Maybe that's what I need to do... conform String to RawRepresentable with a rawValue of String! :) – Mark A. Donohoe Jul 23 '18 at 21:09
  • I understand the desire. I'm saying that's impossible. (Or, as you've found, it's only possible by explicitly calling it out with specialized methods. But you cannot conform types this way.) – Rob Napier Jul 23 '18 at 21:11
0

No, you cannot conform a protocol to another protocol via an extension. The language does not support it.

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • So is there a different way to simplify the functions so I don't have to create the 'matrix of overloads'? – Mark A. Donohoe Jul 23 '18 at 17:46
  • 1
    Nothing springs to mind. I agree with the other commenters: your use of `CustomStringConvertible` is weird and suspicious. You should edit your question to include some example calls that demonstrate why you think you need it. – rob mayoff Jul 23 '18 at 18:03
  • That's supplementary to the actual question, which is about the 'matrix'. We use it because we have other things which implement CustomStringConvertible which we also pass. If it helps you focus better, simply replace all occurrences of CustomStringConvertible with String (and remove the String(describing:) calls.) The question remains the same... two types that ultimately resolve to a string. How can you pass them to the same parameter. – Mark A. Donohoe Jul 23 '18 at 18:07
  • 1
    Conform them all to a protocol you define, and take that protocol as your argument type. – rob mayoff Jul 23 '18 at 18:21
  • But wouldn't that mean we'd have to touch every enum that uses String for its RawValue? Again, we can't extend the RawRepresentable protocol so I'm not sure what that actually solves. If that were the case, we'd simply just type x.rawValue and be done with it, but we're trying to avoid that where possible. Make sense? – Mark A. Donohoe Jul 23 '18 at 18:25