When to implement Display, FromStr, and ToString?

The standard library defines the 3 traits which are related to each other, but, in my opinion, it's not clear enough when the traits should be and should not be implemented. Below I will try to describe my understanding of those traits. I am interested to hear whether everyone agrees with it and, if not, to hear alternative interpretations.

Let's start with ToString, its docs are quite clear:

ToString shouldn’t be implemented directly: Display should be implemented instead

This gets enforced by the new to_string_trait_impl Clippy lint. So ToString is essentially a convenience proxy for Display.

Next, let's consider the FromStr trait. Its main use is the std::parse method. It's not stated explicitly in the docs, but I think it's implied that FromStr should be symmetric to ToString. In other words, FromStr::from_str(&ToString::to_string(&val)).unwrap() should produce the same value as in val. Because of its relation to ToString and subsequently to Display, it looks like the main use of the trait is to handle user-provided input, i.e. data which users may manually input.

Finally, the Display trait. The docs state the following:

Display is for user-facing output

fmt::Display implementations assert that the type can be faithfully represented as a UTF-8 string at all times. It is not expected that all types implement the Display trait.

I have the following personal conditions for having a Display implementation:

  • It should be intended for direct user consumption.
  • It should be stable and unambiguous.
  • It should be usable with non-trivial (i.e. non-"{}") formatting strings.

The later condition means that an implementation should be "simple enough", i.e. collections (e.g. Vec) should not implement Display because formatting result may be huge.

The main reason for creating this post is this PR. The elliptic-curve crate currently has ToString and FromStr (but not Display) implementations for PublicKey and SecretKey types which are based on the PEM encoding. The implementations are used as convenient and easily discoverable serialization/deserialization APIs.

In my opinion, it's a misuse of the APIs, since PEM encoded data is not intended for direct human consumption (it contains a human readable header, but otherwise it contains unintelligible base64-encoded bytes) and users certainly will not manually provide such data. But @bascule would prefer to keep the implementations for convenience sake.

A more ambiguous example is Display and FromStr implementations for Scalar and NonZeroScalar types (essentially, bounded stack-based bigints). The implementations are based on hex encoding. It works fine, but I don't think that Display and FromStr should be implemented since (application) users should never deal with these types directly. Instead I think we should use Debug, LowerHex, and UpperHex formatting traits and inherent serialization/deserialization method to/from hex encoding.

What do you think? What criteria do you use in your code?

1 Like

I personally think that “for human users” is not a good criterion, and a better one would be something like “for the usage” whether humans are involved or not.

The important thing is that Display and FromStr should be implemented for the canonical textual format for this data type, and should not be implemented if there isn't a canonical textual format — that is, if there is more than one reasonable choice.

  • For example, serde_json::Value implements Display to emit JSON. That JSON is very likely to not ever be seen by human eyes outside of debugging. But, JSON text is certainly the canonical, unsurprising, result of formatting a serde_json::Value.

  • Similarly, PEM encoding is (as far as I know; I don't do a lot of cryptography) the canonical choice for how to store keys in text format. This is a weaker argument than the previous case, where the type is for representing one specific data format, but then, consider that Display for i32 bakes in the choice of base 10, which is equally arbitrary convention.

And when there is an ambiguity between formats, what I'd do is define wrapper types (in the style of Path::display()) that narrow down to one format — not avoid Display entirely.

10 Likes

This statement resonates with me. I reach for these traits when there is only one obvious way to do it right. When the implementation involves some choices that might surprise a user, then I defer the implementation to a method on the type like MyT::to_custom_string or MyT::to_abbreviated().

You might also want to check out the previous discussion on ToString, FromStr and Display in this thread, at least I found it a good read :slight_smile: :

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.