How to avoid calling CustomStringConvertible.description as a property?

CustomStringConvertible is pretty adamant that description should not be called directly.

Calling this property directly is discouraged. Instead, convert an instance of any type to a string by using the String(describing:) initializer. This initializer works with any type, and uses the custom description property for types that conform to CustomStringConvertible:

accordingly, i try to write string conversions with interpolations ("\(x)") whenever possible. but this gets pretty awkward when you try to combine it with optional chaining, because you wind up with contortions like

return (article?.title).map { "\($0)" } 
    ?? (fallback?.title).map { "\($0)" }
    ?? "some fallback for the fallback"

alternatively:

return (article?.title).map(String.init(describing:))
    ?? (fallback?.title).map(String.init(describing:))    
    ?? "some fallback for the fallback"

what is the right way to express these kinds of string conversions? i have a hard time understanding how this is preferable to a simple

return article?.title.description
    ?? fallback?.title.description
    ?? "some fallback for the fallback"

Out of curiosity, what's the type of title on these values, and what's the context for grabbing description instead of a more semantic property? (e.g., are you implementing description for a type which contains article/fallback?)

in this example, title was some Markdown.Bytecode that is normally rendered to HTML. however, it can also be rendered to plain text, which is what i was doing here, and the interface for that is CustomStringConvertible, via description.

I wonder if you wouldn't be better served offering

var plainText: String {
    ...
}

var description: String {
    plainText
}

if you care about the semantics and want to be guaranteed to be working with some stable value.

Edit: Jinx

1 Like

Sounds like your type guarantees something above and beyond CustomStringConvertible for this property; it just happens to fulfill the protocol requirement too.

So my advice would be to feel free to call description from the concrete context, or otherwise create a differently named property such as plainText if you feel like distinguishing the guarantees of this API from that of CustomStringConvertible.description.

[Edit: Samesies with what @itaiferber said. Geez, we even chose the same name for the property.]

3 Likes

Hmm. It's interesting that CustomStringConvertible doesn't mention why String(describing:) is the preferred method of converting a value of Any type to a String.

It looks like at some point the documentation for CustomStringConvertible used to say:

This textual representation is used when values are written to an output stream, for example, by print.

But it no longer says so. Is it possible that the original goal of CustomStringConvertible was to be a tool to customize the output of functions like print and not as a general way to expose a var description: String, even though a lot of people ended up using it as such? That would explain the seemingly bizarre discouragement of accessing it directly (or using it as a generic constraint).


FWIW, what I usually do in my own code is to create protocols similar to CustomStringConvertible that convey more clearly the intent of that description. For example:

/// A type with a text representation shown in the UI.
protocol DisplayableString {
    var displayString: String { get }
}
1 Like

To expand on the documentation you're quoting—what it's trying to say is that something like the following is discouraged:

func f<T: CustomStringConvertible>(_ t: T) {
  print(t.description)
}

...because you can instead write:

func f(_ t: T) {
  print(String(describing: t))
}

Every type is string convertible, and there are no semantic guarantees available solely because a type is custom string convertible. Thus, to constrain generic code only to work with types that conform to the protocol is needlessly narrow and never semantically necessary. In other words, by "calling...directly" it's more trying to get at "calling...in generic code."

Any concrete type is free to make load-bearing guarantees of its own description property that go beyond this protocol, and calling description in concrete code based on those guarantees ought to be no different from calling the concrete implementation of any other property.

5 Likes

i try to avoid having multiple spellings for the same underlying operation, so when i wrote Markdown.Bytecode, i thought that it would be better to avoid having a “forwarding” property like description, and just put the implementation in description proper, since the type was going to have to conform to CustomStringConvertible anyways.

i’m not sure i understand how the guarantee is different, i’ve always treated CustomStringConvertible as meaning “this thing can be converted to a String, and description is how you do it”. for Markdown.Bytecode, rendering to plain text is the only sensible direct String conversion; if you want to convert to HTML, you convert through the HTML type.

so in my mind, adding a plainText property that just returns description is like adding a length property to Array that just returns count.

it was originally planned to open-source Markdown.Bytecode as a library type, so i did not want users of the library to also have to adopt a particular string conversion framework, which could duplicate or conflict with a DisplayableString system the user has already defined for themselves.

the thing is, i don’t really agree with this - every type is string reflectable, but not every type is (or should be) string convertible. i can recall exactly zero scenarios outside of debugging/introspection when i intended to convert something to a string by obtaining its reflection. this has almost always been a bug, like "https://forums.swift.org/\(postID)" producing "https://forums.swift.org/PostID(rawValue: 72605)".

It would be a perfectly conformant implementation, as far as CustomStringConvertible goes, to have description for your Bytecode type be something like "(256 bytes)".

String(reflecting:) and its property counterpart, debugDescription, guarantee a string that's "suitable for debugging"—description does not! Rather, it's what you get when you print the value, and that in itself is plenty used even if there are exactly zero scenarios besides.


[Edit: Naturally, you might ask: "Why isn't the protocol just called Printable then?" And the answer is...it literally was, up until some release of Swift 2.]

2 Likes

From the docs:

Types that conform to the CustomStringConvertible protocol can provide their own representation to be used when converting an instance to a string.

description will return you a textual representation of your type, but no guarantees are provided about what that representation means. Markdown.Bytecode could very well have a description in the format

<Markdown.Bytecode length=34 depth=2 bytes=...>

and this would fit the semantics of CustomStringConvertible just fine.

Its discouraged to rely on the result of description because it offers no further guarantees beyond "this returns a string".


What you do with this is up to you. If you want, you could decide for Markdown.Bytecode that description is the plain text, document it as such, and reference .description to your heart's content. Or, you could decide that you'd prefer a representation that's semantically-named and go with .plainText (and optionally mirror it in .description).


i.e., what @xwu said

2 Likes

well that’s just one interpretation of it. it was probably the interpretation back when it was called Printable. but today i find another, more-useful interpretation is that description is what you get when you pass it to DefaultStringInterpolation.appendInterpolation(_:).

it could, in the sense that it wouldn’t be openly contradicting anything in its docs, but as a practical matter, this is not a good “seating arrangement” so long as CustomDebugStringConvertible exists. you would want the object-style representation in logging contexts, and the ‘text’ representation in business logic. if you claim CustomStringConvertible for the logging-optimized representation, you don’t have an interoperable way of providing the long-form representation to code in other packages.

1 Like

True, but what @xwu and I are claiming is that CustomStringConvertible is not a good way to get that "text representation" for business logic. CustomStringConvertible is, in practice, how you get types coerced into some string, most often for printing.

It's hard to "reserve" the debug description because even if you offer both CustomStringConvertible and CustomDebugStringConvertible, it's the former that gets used for string interpolation everywhere. Even for debug logging, you have to be careful to remember to write "\(x.debugDescription)" and not "\(x)", which is a pain — and in practice, CustomStringConvertible is implemented with logging in mind significantly more often than CustomDebugStringConvertible.

But again, you can decide on the semantics of your types; if you define description to mean the plain text of the type, then you're free to do so, and free to use .description directly with no risk.

(And, if you offer your type as public API, consider how API consumers would expect to use the type. I wouldn't find it intuitive at all to get the plain text of a Markdown document via .description rather than by a semantically-named property — and in fact, would probably fumble with the API a bit because I'd never consider .description to return anything meaningful to me, so I'd never even try it.)

4 Likes

at the risk of diving too deep into the weeds, Markdown.Bytecode is rarely a document, it is really just a precompiled variation of AttributedString. so you would usually use its plain text rendering capabilities like:

var meta:String 
{
    """
    \(self.displayName) · \
    \(self.followers) Followers · \
    \(self.bio)
    """
}

the point of Markdown.Bytecode is to avoid having to carry around colorful and plain representations of the same attributed strings, so it’s an explicit goal to make them interpolation-friendly.

it sounds like best thing to do here is probably vend the plainText member anyways and prefer "\(self.bio)" over "\(self.bio.plainText)" as a style guide rule.

Given this, I think I would be even more surprised as an API consumer that description would return a plain text rendering of the string. I would expect printing the string to produce the raw bytes of the text, or some other description; I probably wouldn't even consider checking the documentation of description to notice that it would be plain text.

Instead, I would expect to write something like

var meta: String {
    [
        displayName.plainText,
        followers.plainText + " Followers",
        bio.plainText
    ].joined(separator: " · ")
}

I don't think I'd be comfortable relying on string interpolation to combine these, given how unreliable descriptions tend to be.


Edit: to be clear, I think this is a nice additional interface, but not one I would expect or know to reach for. I would still suggest offering both plainText and CustomStringConvertible

Adding to the discussion of whether API consumers should expect anything from CustomStringConvertible's description other than a String with some sort of text representation that may be helpful for debugging, there's a very interesting post on NSHipster about the history of APNS tokens and how, prior to iOS 7, some apps were relying precisely on description to get a representation of the memory contents of the backing NSData of the APNs device token... until description changed (as the underlying type changed from NSData into Data).

This is obviously not the same thing, but I still think there's a lesson to be learned there that may apply to this. For example, it makes me think about how changes in this (documented, but not obvious in code) expectation of what description does would be communicated to users.

If one uses the documentation to direct API consumers towards using description for something specific (like the plain text version of Markdown.Bytecode in this case), the API maintainers would be heavily tied to that decision forever: to change what description means in the future, or deprecate it altogether (if you no longer wanted to offer whatever you told users that description should contain) there would be no good way of communicating this to users (unlike with a specific plainText property, that you could deprecate to show warnings/errors in code).

3 Likes