Are changes to fmt::Display considered breaking?

As the title says: if I change the way a struct is formatted via fmt::Display, is this considered a breaking change? This would seem to be a "Behavioral change" as defined in RFC 1105, but the rule-of-thumb stated in that section raises another question: to what extent should the format used by fmt::Display implementation be documented?

The context is that I would like to change the fmt::Display format output for a library I have, but I am unsure which part of the version number I should bump. (I'm guessing the answer depends on whether the major version is 0 or not, and would appreciate answers for both cases.)

IMHO, changes to Display shouldn't be breaking unless you've explicitly documented any special guarantees. The std docs say Display is for "user-facing output" which I'd interpret to mean it's not really meant to be parsed by programs, or otherwise relied on.

That said, if it's the only way to get at certain information, programmers may end up "abusing" it to get what they want. So to them changing it would break their program.

10 Likes

Practically, changing anything delivered to the user breaks someone's workflow. You may say it should be ok but they'll not agree on it.

As always there's some xkcd for such problem.

12 Likes

And for the record, the correct way to deal with this is to provide proper debugging and observability hooks, instead of living in fear of changing the Display implementation.

In the variation of SemVer that Rust uses, changing a or b in:

0.a.b
0.0.a

Is the same as changing a or b in:

a.b.c (a > 0)

Since nobody directly answered the question yet:

To put it simply and bluntly: Yes, they (generally) are.

The Display trait is supposed to losslessly represent the implementing type as a string, and it should be the inverse of FromStr, i.e. types are generally expected to implement either both traits or neither of them. In this regard. Display and FromStr are more like a primitive pair of serialization and deserialization traits; in particular, Display is not meant to provide an additional or alternate form of debugging output that is free to change.

Although this is not explicitly documented in the rustdoc for std::, your users will generally depend on Display being stable. Therefore, if your Display format changes, so should your major semver number. (Of course, as per the usual semver rules, before 1.0.0 this actually means bumping the second number, not the first 0 digit).

4 Likes

While I agree this is usefully true for types that implement FromStr, I disagree that this applies to all types, though, because Display is part of the interface for Error.

That said, I do agree that yes, strictly speaking, changing the output of Display::fmt is a breaking change, and moreso than just any minor behavioral change. Whether it's a semver-major change is highly dependent on context, though; for FromStr types, generally yes; for errors, generally no; for typo fixes, generally no.

My personal opinion is that most types that don't impl FromStr or Error just shouldn't impl Display at all, as the problem of "display this to the end user" is much more complicated a decision than just "format into a string, maybe with some small set of generic flags". Any type that implements Display should thus be the result of making those "how do I display this" choices, and at that point, the Display trait implementation is just obvious plumbing.

So I guess my final answer to "is changing Display's output breaking" is "no if it's an Error, yes if it's FromStr, and you're doing it wrong otherwise."

9 Likes

Yes, you are right, I missed this detail. (I was careful enough to include a "(generally)" clause above, though :sweat_smile:)

Where's the source of that? I don't remember neither the losslessness nor invertability constraints for Display in Rust. I know those are the rules for __repr__ / eval in Python and Read / Show in Haskell, but I don't think those are relevant for Rust?

5 Likes

The first constraint is asserted in the fmt module-level docs, here:

fmt::Display implementations assert that the type can be faithfully represented as a UTF-8 string at all times.

I don't remember where I read the invertibility one, maybe in the Rust API Guidlines? Anyway, the standard library seems to adhere to this rule. std:: types implementing FromStr are primitive numbers, their NonZero/Atomic equivalents, string-like types, TokenStream, and IP addresses, all of which are Display, and all of which round-trip.

3 Likes

With OsString and Paths being notable exceptions.

3 Likes

Backing up what @H2CO3 said, the libs team explicitly reserves the right to change Debug output but as far as I know we make no such guarantees about Display, so my understanding is that we currently treat Display output as part of our stable API. That said, we already have a history of making small breaking changes to our API when we think the breakage is minor enough / justified, such as adding an Error impl for &E: Error, so I imagine we would likewise make special exceptions to Display format in some cases. The wording in error messages in particular springs to mind, where we may want to update one to clarify its meaning.

3 Likes

Worth noting that what the libs team does for stdlib isn't necessarily what other crates do. Personally I wouldn't consider changing Display output to be a breaking change, though I'd likely consider the impacts were I to change it without warning.

The sort-of-consensus here that the Display formatting is part of the stable API and that it's at all related to FromStr baffles me. Neither trait documentation mentions the other at all to begin with. And the documentation for Display says "Display is for user-facing output". As such I have always treated Display as strings you show to humans. Humans don't use APIs, machines do. So it's not part of the API IMO.

If I have a struct Person with a name and age field and I were to implement Display for it, I would aim for the string representation that is the nicest to read as a human, since it's supposed to be user-facing. This format is very rarely equal to what's the best for machine parsing Persons from a text file. Nowhere do the docs for these traits say anything about being a poor mans serialization method.

1 Like

Display and FromStr are about going to / coming from the "one obvious string representation," and that implies that they use the same string representation.

The unfortunate reality is that most things don't have one canonical string representation. So unless you're writing a type that does, my recommendation is still to not implement Display or FromStr (unless it is an Error).

"Turn it into a string" has many possible use cases and interpretations, and unfortunately they get muddled up in whatever standard "to string" facilities are provided. Rust does a lot better than a lot of languages here, by providing both "to string for debugging" and "to string for display". But there's still a lot of leeway (in both of them), thus having this discussion at all.

It's hard to say what the "correct" design for display string construction really is. Especially since in a robust application, most are going to be de/serialization to a machine-readable format, or run through a localization system, and a simple "make it a string" API fits neither of those.

I think the only really solid and well-intuition-portable Display implementations are

  • Display + Error: display is how you display the error to "the appropriate audience" for that error
  • Display + FromStr: both use the same canonical (locale invariant) string representation
  • `.method(...) -> impl Display": display adapter for a complex type, read the method docs
  • An actual text or string type
  • (any other type who's sole/primary purpose is to be stuck into a Write output stream)

If your type is Display and doesn't fit one of these categories, you're running a risk of expectation mismatch, especially if you don't document what your Display implementation is. And if you do, changing the documented contract is clearly breaking.

4 Likes

Many unix/linux utilities have user facing output that is depended on in scripts so changes in output are breaking changes. Not all tools work this way, so it kind of depends on what your target audience is.

1 Like