-
Notifications
You must be signed in to change notification settings - Fork 123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Redesign Display
derive macro
#225
Conversation
@tyranron @JelteF so we are on the same page, let me go over design decisions, that are already set in stone, before final decision on #142 is made. This should cover every possible use-case, everything else should result in compile-time error. For example: #[derive(Display)]
#[display("Field: {}")] // error: 1 positional argument in format string, but no arguments were given
struct Single(i32); StructsUnit#[derive(Display)]
struct Unit; // "Unit"
#[derive(Display)]
struct Tuple (); // "UnitTuple"
#[derive(Display)]
struct Struct {} // "UnitStruct" Single-field#[derive(Display)]
struct Tuple(i32); // "1"
#[derive(Display)]
struct Struct { field: i32 } // "1"
#[derive(Display)]
#[display("Pre: {_0}")]
struct PreTuple(i32); // "Pre: 1"
#[derive(Display)]
#[display("Pre: {field}")]
struct PreStruct { field: i32 } // "Pre: 1" Multi-field#[derive(Display)]
#[display("{_0} {ident} {_1} {} {}", _1, any_expr(_0, _1), ident = 123, _1 = _0)]
struct Tuple(i32, i32) // "1 123 1 2 3"
#[derive(Display)]
#[display("{field1} {ident} {field2} {} {}", field2, any_expr(_0, _1), ident = 123, field2 = field1)]
struct Struct { field1: i32, field2: i32 } // "1 123 1 2 3" EnumsWithout variants (Infallible-like)#[derive(Display)]
enum Void {}
const fn has<T: Display>() {}
const _ = has::<Void>(); Unit variant#[derive(Display)]
enum Enum {
First, // "First"
FirstTuple(), // "FirstTuple"
FirstStruct {}, // "FirstStruct"
#[display("SECOND")]
Second, // "SECOND"
#[display("SECOND_TUPLE")]
SecondTuple(), // "SECOND_TUPLE"
#[display("SECOND_STRUCT")]
SecondStruct {}, // "SECOND_STRUCT"
} Single-field variant#[derive(Display)]
enum Enum {
First(i32), // "1"
FirstNamed { field: i32 }, // "1"
#[display("Second: {_0}")]
Second(i32), // "Second: 1"
#[display("Second: {field}")]
SecondNamed { field: i32 }, // "Second: 1"
} Multi-field variant#[derive(Display)]
enum Enum {
#[display("{_0} {ident} {_1} {} {}", _1, any_expr(_0, _1), ident = 123, _1 = _0)]
Tuple(i32, i32), // "1 123 1 2 3"
#[display("{field1} {ident} {field2} {} {}", field2, any_expr(_0, _1), ident = 123, field2 = field1)]
Struct { field1: i32, field2: i32 }, // "1 123 1 2 3"
} Interactions with top-level attributeTODO: Fill out, once #142 is resolved Unions#[derive(Display)]
#[display("No field accesses are allowed, but {} is ok", "interpolation")]
union Union { // No field accesses are allowed, but interpolation is ok
field: i32,
} |
Discussed with @tyranron: Merge this PR without support for top-level enum attribute and do it as a separate PR. |
@ilslv @JelteF one thing I still dislike is semantics for unit types: #[derive(Display)]
struct Unit; // "Unit"
#[derive(Display)]
struct Tuple (); // "UnitTuple"
#[derive(Display)]
struct Struct {} // "UnitStruct"
#[derive(Display)]
enum Enum {
First, // "First"
FirstTuple(), // "FirstTuple"
FirstStruct {}, // "FirstStruct"
} This is too subtle and implicit. And may be too unexpected if I write I think it's better to return error in such cases, pushing a user to explicitly specify what he wants. Giving a convenient top-level attribute semantics, there would be almost no ergonomics impact if he needs exactly this behaviour: #[derive(Debug, Display)]
#[display("{self:?}")]
struct Unit; // "Unit"
#[derive(Debug, Display)]
#[display("{self:?}")]
struct Tuple (); // "Tuple"
#[derive(Debug, Display)]
#[display("{self:?}")]
struct Struct {} // "Struct"
#[derive(Debug, Display)]
#[display("{self:?}")]
enum Enum {
First, // "Enum::First"
FirstTuple(), // "Enum::FirstTuple"
FirstStruct {}, // "Enum::FirstStruct"
} But I would argue that most of the time, a user doesn't expect something |
Valid point, if something like this exists, it should be only for I want to explain, why I've added this behaviour in a first place. // #[derive(Display)]
enum Enum {
A,
B,
C,
}
impl fmt::Display for Enum {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::A => "A".fmt(f),
Self::B => "B".fmt(f),
Self::C => "B".fmt(f),
}
}
} This is a lot of tedious code to write and copy-pasting may lead to errors. Have you noticed, that code block above contains an error? I consider this feature desired, understandable and easily discoverable just by writing code without looking into the docs. Emphasis on discoverable, because this is one of the most delightful features of Rust to me. Discoverable intuitive design with compile-time checks is a match made in heaven. I don't think you can misinterpret it and your suggested approach doesn't cover this use-case. |
Isn't this the first thing a rustacean would try (or write manually) when he thinks about "
I do think such a feature implicitly pushes a library user to mix
Ah, you're talking more about Yeah, restricting it to |
To be fair I didn't think about this possibility, when implementing this functionality. And even if I just wasn't thinking enough, this assumption applies only to experienced rustaceans, but this library isn't only for them with ~50k daily downloads.
I wouldn't just jump to this assumption, especially when there is
Exactly! |
@JelteF @ilslv @ModProg gathering all the discussions regarding this PR here. Sorry for late reply, was quite overwhelmed with stuff last weeks.
As @ilslv mentioned in #216, it's already present in
I gave this quite a though and came to quite a fresh idea: #[derive(Display)]
enum AnotherEnum { // just a regular general-purpose enum here, not an error type
A, // prints "A"
B(String), // prints the inner string
}
#[derive(Error)] // look, ma! we don't need to specify obvious `Display` here
enum AnotherError {
A, // expansion error, as we forgot to specify error description here
B(String), // prints the inner string
}
#[derive(Error)]
enum YetAnotherError {
#[error("Errored because of A")]
A, // prints "Errored because of A"
#[error("Because of {_0}")]
B(String), // prints "Because of <inner-string>"
}
#[derive(Display, Error)]
#[error(skip(Display))]
enum StrangeError { // we intentionally do want exactly this behavior here
A, // prints "A"
B(String), // prints the inner string
} This way, we:
|
@tyranron generally sound good, only thing it cannot unite is my "I want an error when I forget to add a format to an enum Variant". But that would've been a breaking change anyway. |
Why? Isn't this case about this? #[derive(Error)] // look, ma! we don't need to specify obvious `Display` here
enum AnotherError {
A, // expansion error, as we forgot to specify error description here
B(String), // prints the inner string
} Or you've meant any enum variant, not just a unit-like? |
@tyranron yes, and my usecase wasn't an error. It was generating commands. |
I don't really like approach of
What if there is manual Moreover I don't really think "forgetting to specify formatting" is a big deal. In this case error message may confuse you for a second, but code itself should help pin down the cause for an error. |
Well if it is an error that is, in different usecases this might be a bigger issue. But I do understand that for most users this is more convenient than requiring them always. Maybe it could be a default-feature that can be disabled? But in the end, I'm fine with anything. |
@tyranron While I agree that Error deriving display is more ergonomic, it's not in line with how derives normally work afaik. At least in the stdlib it's not how it works. After all this extra information and discussion, I'm now siding with @ilslv and I agree we should derive units by default. |
That seems like a non-breaking change (since it's opt-in), so I think we can postpone that decision until after 1.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll review this PR shortly.
shortly
_shortly_™... 😅
@ilslv what an amazing work. Thanks!
I've adjusted docs a little and some local variables naming. Everything else seems good.
I merged #231 if this is rebased I guess CI should pass now. Feel free to merge it once that happens. |
Related to #225 ## Synopsis Diagnostic introduced in #225 regarding old syntax usage, displays incorrect message in case formatting arguments are specified as identifiers (not string literals): ```rust #[derive(derive_more::Display)] #[display(fmt = "Stuff({}): {}", _0)] pub struct Bar(String); ``` ``` error: unknown attribute, expected `bound(...)` --> tests/compile_fail/display/legacy_fmt_syntax.rs:8:11 | 8 | #[display(fmt = "Stuff({}): {}", _0)] | ^^^ ``` In my experience, this situation happens very often in the wild. ## Solution Fix the diagnostic to consider ident formatting arguments properly.
Resolves #217
Synopsis
Solution
#[display("{}", "crab.rave()")]
->#[display("{}", crab.rave())]
Checklist