Intentions behind CustomStringConvertible and CustomDebugStringConvertible

I am wondering what the core team think the intention is behind the two CustomStringConvertible and CustomDebugStringConvertible protocols. I was of the understanding one would give a textual representation of your object and other would be a textual representation of your object with additional debug information. With that in mind I wouldn't want to be using CustomDebugStringConvertible in a released product as it might include information I didn't want to share. For instance in a server library you might include personally identifiable information in the debugDescription but leave it out in description.

So far so good...

Well until it was pointed out to me, the following code

struct MyType {}
extension MyType: CustomStringConvertible {
    var description: String { "description" }
}
extension MyType: CustomDebugStringConvertible {
    var debugDescription: String { "debugDescription" }
}
let t = MyType()
print(t)
print([t])

will output

description
[debugDescription]

I would have expected the second line to be [description] but it appears that the description of an Array uses debugDescription for its elements. This means there can be no meaningful differentiation between the two protocols as the debug representation is used at times when you want the non debug representation.

4 Likes

That was reported to be intentional:

If I remember correctly, the Objective-C NSArray has the same behavior.

1 Like

Oh I'm sure it was intentional, but if the only reason was to ensure strings were quoted when printed as elements of an Array it seems a little short sighted. It makes it impossible to provide two distinctive string representations of a type which will be used in two different situations.

3 Likes

This is pretty close. The language steering group has discussed this issue twice with recent proposals on new conformances to these protocols. As reported out in our decision notes, the debug description is a string representation that is useful ‘for debugging.’ Principally, this is to say that, for a debug description, the exact representation may change without notice and should not be depended upon in production use—e.g., parsed for deserialization. More than this we declined to specify.

My own exegesis follows—

A representation that is useful ‘for debugging’ may contain more, less, or different information and/or more, less, or different formatting than a non-debug representation. As users often use logs or other tooling to facilitate debugging, typically, where the two differ, the debug representation will have a more structured format suitable for logging.

In every version of Swift publicly shipped, print may fall back to the debug description if there is one and not also a non-debug description (and vice versa for debugPrint), and aggregates or collections (like Array) may—and do—choose to use the debug description of types in their own non-debug description. Thus, every representation that is useful ‘for debugging’ must also be suitable (though not necessarily the most ideal) for these general uses, excepting that it specifically disavows any stability of the output.

I have mixed feelings on these two protocols these days (and their equivalents in Rust, and Python, and other languages that have both).

CustomDebugStringConvertible is the more straightforward one for me: it’s a representation useful for debugging that doesn’t unintentionally obscure information. Delimiters are good, escaping non-printable characters is good, but if you want to truncate the contents, sure, fine. There are no particular guarantees here.

CustomStringConvertible is weirder, because if I have to describe it it’s something like “the default, non-localized way something should be printed”. In a professional app, how often are you printing something that isn’t localized? And in some plain old tool, maybe a personal one, how often is there one best representation for printing? Integers, maybe; arrays, maybe; but even by dictionaries it’s starting to get a bit less obvious. (Swift syntax? JSON syntax? …Kotlin syntax?) If something’s not LosslessStringConvertible, what exactly determines its CSC representation?

The last piece of the frustration puzzle is what Xiaodi brought up: everything is “describable” in Swift (you can always print it or use String(describing:)). This is convenient for several things, especially when just starting out! But it also means you can’t omit a CustomStringConvertible to indicate you don’t have a good printable form. Instead, you get the problem you brought up originally: your debug representation now becomes the one that’s easy to use, by accident even. Rust’s Debug and Display are not like this.

In retrospect I think “make everything describable” was probably the mistake more than having both CustomStringConvertible and CustomDebugStringConvertible. I suppose you’d still have to decide what Array should do, but at least you’d have the option of it not being describable. In any case, things are unlikely to change at this point.

5 Likes