-
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
affix
is confusing with outer-level enum
display formatting
#142
Comments
(sorry for not responding earlier to this) @ilslv @tyranron @ModProg Given we're working towards 1.0 and we're planning to change some of the
|
We could also make the specific variant format availble via some variable name so you can use it via |
I would prefere using an affix style fmt as it is more flexible than just prefix suffix and also more readable conserning white space etc., but I'd be open on the naming. |
Would you be able to access the enum variants values via e.g. |
My first reaction to this was to remove However, given it a thought, I do see a lot of usefulness having it, and even see places in my codebases where it may simplify things or allow removing manual I also do think, that use thiserror::Error;
#[derive(Debug, Error)]
#[error("error")]
enum Enum {
A, // displays "error"
#[error("b")]
B, // displays "b"
} The initial design choice of use derive_more::Display;
#[derive(Display)]
#[display(fmt = "<{}>")]
enum UsesAffix {
#[display(fmt = "a")]
A, // prints "<a>"
#[display(fmt = "b")]
B, // prints "<b>"
}
#[derive(Display)]
#[display(fmt = "<{_0}>")]
struct Foo(u8); While I do agree with this claim:
That the form Returning to use derive_more::Display;
#[derive(Display)]
#[display(default("error"))]
enum Foo {
A, // prints "error"
#[display("b")]
B, // prints "b"
} Another point here, that I do see defaulting and top-level affixing being quite orthogonal. It seems fully OK (and even desired) to have both at the same time: use derive_more::Display;
#[derive(Display)]
// Look, ma! No weird syntax anymore!
#[display("Foorror: {self}! And {}", "arbitrary_expression".to_owned())]
#[display(default("error"))]
enum Foo {
A, // prints "Foorror: error! And arbitrary_expression"
#[display("b")]
B, // prints "Foorror: b! And arbitrary_expression"
} The question is: do we want to be semantically compatible with If yes, then for compatibility, we only need to invert attribute markers. If we don't like use derive_more::Display;
#[derive(Display)]
#[display(wrap("Foorror: {self}! And {}", "arbitrary_expression".to_owned()))]
#[display("error")]
enum Foo {
A, // prints "Foorror: error! And arbitrary_expression"
#[display("b")]
B, // prints "Foorror: b! And arbitrary_expression"
} As for me, I'd like to use
Just using
I do agree here. Prefix/suffix is just not that powerful, because we may want to repeat the inner twice, for example.
Yup, I'd propose it to work in the most obvious and clever way. So |
@tyranron I agree with everything you said here. I'd also prefer default. One helpful error could be when you specify a non default fmt string and neither specify |
I think this would be too limiting, disallowing to express patterns like this: #[derive(Display)]
#[display("My app errored: {_0}")]
enum MyError {
Io(io::Error, PathBuf),
Serde(serde_json::Error, String),
} While I understand the possible caveats like this: #[derive(Display)]
#[display("My app errored: {_0}")]
enum MyError {
Io(io::Error, PathBuf),
#[display("serde failed on input `{_1}`: {_0}")] // won't be displayed
Serde(serde_json::Error, String),
} Maybe it makes sense to throw such error when both |
But why wouldn't you use default in that case? #[derive(Display)]
#[display(default("My app errored: {_0}"))]
enum MyError {
Io(io::Error, PathBuf),
Serde(serde_json::Error, String),
} |
I cannot give here any meaningful reason, except "because I don't care and only want to specify formatting for the whole thing?". For the inexperienced user, it seems meaningful to provide Is this a good motivator to prefer inverted (I still do tend to use |
That makes sense. And there is really no difference to a wrap without a reference to self and a default (as long as there are no variant specific formats). Technically, we could also make a format the default, as long as it does not reference self. And then it becomes wrapping... and you need to specify the default for the variants not specified as default. i.e. // Is the default
#[derive(Display)]
#[display("My app errored: {_0}")] // item level attribute (default)
enum MyError {
Io(io::Error, PathBuf),
#[display("serde failed on input `{_1}`: {_0}")] // will be displayed instead of item level attribute
Serde(serde_json::Error, String),
}
// References self therefor is nolonger the default
#[derive(Display)]
#[display("My app errored: {self}")] // item level attribute (wrap)
#[display(default("{_0}"))] // will be displayed inside of item level attribute
enum MyError {
Io(io::Error, PathBuf),
#[display("serde failed on input `{_1}`: {_0}")] // will be displayed inside of item level attribute
Serde(serde_json::Error, String),
} Not 100% sure on that though as it feels a bit magically, but as we need to detect and replace |
@tyranron @JelteF I think my argument about discoverability applies here as well. If we support following: // derive_more::From fits nicely into this pattern as well
#[derive(Debug, Display, Error, From)]
enum CompoundError {
Simple,
WithSource {
source: Simple,
},
#[from(ignore)]
WithBacktraceFromSource {
#[error(backtrace)]
source: Simple,
},
#[display(fmt = "{source}")]
WithDifferentBacktrace {
source: Simple,
backtrace: Backtrace,
},
WithExplicitSource {
#[error(source)]
explicit_source: WithSource,
},
#[from(ignore)]
WithBacktraceFromExplicitSource {
#[error(backtrace, source)]
explicit_source: WithSource,
},
Tuple(WithExplicitSource),
WithoutSource(#[error(not(source))] Tuple),
} I don't see why we shouldn't support this: // derive_more::From fits nicely into this pattern as well
#[derive(Debug, Display, Error, From)]
#[display("Compound error: {}")]
enum CompoundError {
Simple,
WithSource {
source: Simple,
},
#[from(ignore)]
WithBacktraceFromSource {
#[error(backtrace)]
source: Simple,
},
#[display(fmt = "{source}")]
WithDifferentBacktrace {
source: Simple,
backtrace: Backtrace,
},
WithExplicitSource {
#[error(source)]
explicit_source: WithSource,
},
#[from(ignore)]
WithBacktraceFromExplicitSource {
#[error(backtrace, source)]
explicit_source: WithSource,
},
Tuple(WithExplicitSource),
WithoutSource(#[error(not(source))] Tuple),
} And I think that single empty placeholder |
But I'd argue that And is I wouldn't be opposed to have the behavior of: "If there are no additional arguments passed to |
Isn't |
True. So maybe Is there any use for a recursive Display? Atleast in the usecase of this macro? |
No, I don't think so.
As I mentioned before I'm against "magical" names. |
@ilslv I am not opposed to have |
I think there's been enough time to think on this. What is everyone's opinion at this time? I definitely think we should implement this for the 1.0 release, since changing it later would be a breaking change. |
My position hasn't really changed. I still think we should allow We should have a way of specifying both a default and a wrap, but I'd be fine with making a format that doesn't reference the variants formatting (either through |
After some reflection, some real-world use, and considering @ModProg's comment, I'm concerned that all prior proposals actually don't go far enough in breaking from current and OP-proposed syntax, which has problems. Even the already implemented design space is too complex to rely on a single
Will post another comment here with details about a solution I think will solve the problems we're debating about here. 🙂 |
Designing from scratchLet's enumerate the behaviors we have expressed that we want:
I think the best way to offer the above behaviors going forward would involve new syntax that fulfills these requirements:
Each of the current proposals fails to fulfill these in one way or another. Let me prove it to you with a table (and, *ahem*, some new contenders):
You may have noticed that I snuck in a couple of new solutions at the end! Yes, I believe that we can fulfill all of the above requirements. 🤪 I propose we move to have distinct tags for each of the numerous flavors of format spec that make behavior unambiguous in all cases, and therefore easier to use overall.1 Let me give an applied example, rewriting last example in the original OP in the Parens Edition™ syntax: #[derive(Display)]
#[display(item("<{@variant}>"))] // 1ii: affix formatting
#[display(default("unknown"))] // 1i: default variant formatting
enum UsesAffix {
#[display(variant("a"))] // 2i: non-overriding formatting
A, // prints "<a>"
B, // prints "<unknown>"
#[display(override("{c}"))] // 2ii: overriding formatting
c, // prints "{C}"
} ...or, with the Equals Edition™: #[derive(Display)]
#[display(item = "<{@variant}>")] // 1ii: affix formatting
#[display(default = "unknown")] // 1i: default variant formatting
enum UsesAffix {
#[display(variant = "a")] // 2i: non-overriding formatting
A, // prints "<a>"
B, // prints "<unknown>"
#[display(override = "{c}")] // 2ii: overriding formatting
c, // prints "{C}"
} Cases needing errors or warnings become much more clear, too; using Parens Edition™ notation: #[derive(Display)]
#[display(item("\\{@variant}/"))]
enum NoDefaultProvided {
#[display(variant("1"))]
One, // prints `\1/`
Two, // error: no `default` provided by item level, no `variant` specified here
} #[derive(Display)]
enum NoDefault {
#[display(variant("Left"))]
Left, // prints `Left`
Right, // error: no `default` provided by item level, no `variant` specified here
} #[derive(Display)]
#[display(item("oops, I meant to write a default"))] // maybe throw an error: no `@variant` formatting arg was used
enum ShouldaDoneDefault {
#[display(variant("Red"))] // otherwise, warning: this is ignored because `item` has no `@variant` placeholder, which probably wasn't intended! Maybe suggest switching `item` to `default`, or using the placeholder.
Red, // prints `oops, I meant to write a default`
} #[derive(Display)]
enum PersonKind {
#[display(variant("dog"))]
Dog, // prints `dog`
#[display(override("cat"))] // warning: trying to `override` an `item` that doesn't exist
Cat,
} EDIT: Added the Footnotes
|
I'd prefer the paren syntax. I agree that something like Some proposed changes to paren:
#[derive(Display)]
#[display("affix: {@}")] // errors if it doesnt contain an `@`
#[display(default("the {@name} {_0}"))] // could error if every variant has a formatting
enum Enum {
Default(usize),
#[display("{@name} formatting")]
Custom.
#[display(override("Likely only very seldom used")] // could error if no "affix" exists
Override
} We also need to decide how this all plays together with the default display for Unit and Newtype and with the proposed |
Reexamining my proposal(s) above, I don't think I've given @tyranron enough credit; their solution was, at least, a partial inspiration for the Parens Edition™ version of my above proposal. Kudos! |
@ModProg: RE: your suggestions:
|
Just personal taste, as I felt like those are the ones that you most likely want. Similar to how
yeah, I just wanted to mention it, so we can build the rest in a way that doesn't prohibit such features. |
Just wanted to add my use case for consideration since it currently doesn't seem to be supported. I have a lot of simple enums that I serialize to JSON strings. I would like to derive #[derive(Deserialize, Serialize, Eq, PartialEq, PartialOrd, Ord, Clone, Copy, Debug, Display)]
#[serde(rename_all = "lowercase")]
#[display(fmt = "{}", "serde_plain::to_string(&self).unwrap()")]
pub enum TestOs {
Alpine,
RockyLinux,
Debian,
Ubuntu,
Macos,
Windows,
} However the |
@JelteF speaking of |
Regarding 1.0.0 vs 0.100.0 The main thing I would want to do is release 1.0.0 soon-ish, because I think derive_more is in a stable state. The first 0.99 release was 3 years ago. My plan at that time was to release 1.0.0 soon after, but I never did. Now we have a ton of breaking changes, which make the syntax more consistent and bring it in line with current Rust versions. After these changes I think we just shouldn't break things for a while again. All this new code also has a lot of tests (a lot of those are thanks to you). So I'm pretty confident that more breaking changes are not necessary for a while. And if I'm wrong, I don't think it's a big deal to release 2.0 quickly after 1.0 if the amount of breakage is tiny and we break for a good reason (i.e. some crazy bug, not some cosmetic cleanup). |
For me releasing 2.0 soon after 1.0 for such popular and foundation library is a big deal. Ecosystem is still fragmented after the release of syn 2.0 and I think it will be like that for a long time. |
There's no difference in amount of split between doing 1.0 and then 2.0 or doing 0.100 and then doing 1.0. You get this split because there's two major versions. We want to release a 1.0, so by releasing 1.0 directly in the happy path there's only 1 major version, and in the bad case if there's some crazy bug then we have two major versions. But if we start with 0.100 and we still want to release 1.0, then even if we don't need to do any more breaking changes we need to release a new major version (or stay at a 0.x release again for 3 years). So I say, let's just bite the bullet and release 1.0 once we have no obvious breaking changes that we want to do. |
I don't agree with that. People have certain expectations about stability of 1.0. By releasing 0.100 I expect us getting bug reports/ideas from early adopters, that may be very helpful. And after 1.0 I expect slow migration of the rest of the ecosystem.
I don't see any problems with that. |
People already have certain expectations about derive_more. It's been stable for 3 years. There's 110k repos on github using derive_more. Suddenly starting to do multiple breaking releases in a row is something I think we should do. And thus I don't think we should plan our versioning to that. Our goal with the next release should be: This is a stable version that we don't plan on changing. It does have new features. But they are tested well. Any bugs that don't require breaking changes we will fix soon. Once that require breakage, will probably have to wait for another 3 years to be fixed. And in the extremely unlikely case where something is completely broken, and requires a new major version to fix. Then we'll likely know within a few hours/days, which makes the ecosystem split problem pretty much a non-problem.
Even if there's no actual breaking changes, cargo will still consider it incompatible versions. Thus doing that will split the ecosystem for no good reason. |
You /could/ release a 0.100, wait a few weeks/months to see if there are breakages, and if there aren't any, then release a 1.0 and a 0.100.n that depends on 1.0 and reexports everything from it. Not saying you must, but that would be one way to deal with the situation without having to rush a 2.0 to fix problems that require a breaking change while avoiding an ecosystem split. (I'll add that a 0.99.18 with syn2 changes only would be very welcome too) |
Why did you say this. After thinking a bit more I agree that it would be good to have some more testing before actually releasing 1.0.0. But I think the nicest way to do that is to release a And I think we'll get enough feedback from people since quite a few people seem to want a release for the |
@ilslv Also, what do you think of my previous argument for the
|
@JelteF unfortunately very few people upgrade to alpha/beta versions. They aren't suggested on This problem isn't really unique to this crate, I remember even |
@ilslv I think a feedback from a few involved/invested users is exactly what we want for such an intermediary release. And mostly for applications, not libraries. Otherwise we get this ecosystem split. So to me what you're describing sounds like a good thing, instead of a problem. I think even with ~20 people trying it out in their own projects we should have enough feedback to be relatively certain that nothing major is broken. And anyone searching for derive_more + syn2 will find that you can install a beta. So unless you have major concerns, I think I'll publish a beta release later this week. IT seems pretty low risk, with possibly high reward. Worst case no-one uses it and we're at the same stage as where we are now. Best case we get valuable feedback. Intermediary case, people that really want syn2 support have a way out without pulling from git. |
@JelteF hm, I think that this is no-lose situation. In case we get valuable feedback — great. In case there is no feedback — we can still push breaking changes. |
I published |
@ilslv @tyranron @MegaBluejay This is the last major item that's still blocking the 1.0.0 release. Let's make a decision here and implement it. |
@JelteF okay, let's discuss what's left... After re-reading and rethinking the whole thread once again, it seems to me that the
Regarding this, I also feel fine. If no major opposing would be expressed in next weeks, I'll implement this. At least, before really going |
One addition concerning the edgecase of having a field called Probably unlikely, but could result in possible confusion. I think using This probably should error #[derive(Display)]
#[display("{_inner}")]
enum Enum {
Variant{ _inner: u8 }
} and recommend #[derive(Display)]
#[display(default ("{_inner}"))]
enum Enum {
Variant{ _inner: u8 }
} but the actual confusing to look at usage would be #[derive(Display)]
#[display("{_inner}")]
#[display(default ("{_inner}"))]
enum Enum {
Variant{ _inner: u8 }
} though this will happen for every non keyword name and is therefore an unavoidable compromise. and while it means that noone will be able to use a const named const _inner: u8 = 5;
#[derive(Display)]
#[display("{_inner}")]
enum Enum {
Variant
} Will yield |
To be clear: +1 on the
agreed
yeah seems fine to me. Especially since underscore prefix usually means it's a non public field, so renaming it shouldn't be big deal normally.
I think you should still be able to do the following (should have a test in the actual implementation): const _inner: u8 = 5;
#[derive(Display)]
#[display(default("{_inner}"))]
enum Enum {
Variant
} |
Or you could do const _inner: u8 = 5;
#[derive(Display)]
#[display("{_inner}: {}", self::_inner)]
enum Enum {
Variant
}
// -> "Variant: 5" so yeah, the only thing against |
@tyranron do you still have bandwidth to work on this? I think we agree on the API design, now we "only" need to actually implement it. |
One thing I just came across is wanting to have the repr number in enum's display, but I can probably just do |
Currently, when one uses a top-level
display
attribute for theDisplay
derive on anenum
, it switches to an affix mode that defines theenum
's overall format and expects, at most, a single placeholder where inner variant display logic can print into:However, there's a few problems with this current behavior:
fmt
works rather differently than anywhere else here. It's not obvious why formatting should be different here, and there's no "obvious" signal that behavior is different. I feel qualified to say this since I implemented this behavior myself a year ago, and still was confused after reading diagnostics until I remembered what I had implemented.Display
impl requires using a specific format some of the time,I think that we can do better here. There are enough surprises with the current behavior that I think exploring alternatives is warranted. Here's what we stand to gain:
derive_more
inhabits the same ecosystem as other crates that doDisplay
deriving with rather different approaches, we could possibly try to find a top-levelenum
formatting option that is also consistent with those approaches (and play a bit better with other crates conceptually). In a second, I'll propose something similar to whatthiserror
, an error crate with facilities for derivingDisplay
, currently does.So, here's my proposal:
The current affix-oriented behavior should be moved to an
affix
attribute key, and (this) new behavior will be defined forfmt
instead.Re-define the
fmt
spec at the top level of anenum
to be the default printing logic for anenum
's variants that can be overridden by a more specificfmt
rule. An overridable default has basis in some prior art;thiserror::Error
treats top-levelenum
format specifiers as the "default" message (motivating issue, current relevant upstreammaster
code at time of writing):Disallow
affix
andfmt
to be used together at the same time for the time being, since that's a lot of open design space that doesn't need to be handled right this second.To concretely translate my above suggestions into code:
I'm working on a draft PR for this here. I'm speculatively doing work without getting validation in this issue first, but I don't mind. :)
The text was updated successfully, but these errors were encountered: