Skip to content
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

any chance to get a trait tagged to the struct as well that exposes the "FIELD_NAMES_AS_ARRAY" #24

Open
cchance27 opened this issue Sep 9, 2023 · 8 comments · Fixed by #25

Comments

@cchance27
Copy link

I'm using generics and based on the generic type i need the field names, but right now i can't do a T::FIELD_NAMES_AS_ARRAY because there's no way for me to say the T implements that.

@jofas
Copy link
Owner

jofas commented Sep 9, 2023

Thanks for the great feature request. I actually was working on this when I was trying to implement a flatten attribute, but it hasn't landed on master yet. Getting a new version out with the trait should be no problem though. Currently this crate is only a proc-macro crate which can't export anything but proc macros, so we'd need to remodel the structure a little, but that should be fine.

@cchance27
Copy link
Author

yes, would definitely be helpful, as a way to deal with generics that are deriving this. I don't understand the blackmagic of procmacros enough to guess, in standard macro_rules it seems it would be simple but procmacros are a whole different world lol

@jofas
Copy link
Owner

jofas commented Sep 19, 2023

I've been trying to find the right interface and I think I have a solution that I'm okay with. The thing is, having an associated array requires our trait to have a const generic parameter like this:

trait FieldNamesAsArray<const N: usize> {
    const FIELD_NAMES_AS_ARRAY: [&'static str; N];
}

Now that makes it really cumbersome to use it as a bound on a generic type, because we always have to provide N as well, i.e.:

struct X {
    y: u8,
    z: u8,
}

impl FieldNamesAsArray<2> for X {
    const FIELD_NAMES_AS_ARRAY: [&'static str; 2] = ["y", "z"];
}

fn foo<const N: usize, T: FieldNamesAsArray<N>>() {
    println!("{:?}", T::FIELD_NAMES_AS_ARRAY);
}

// not really an elegant call
foo::<{X::FIELD_NAMES_AS_ARRAY.len()}, X>();

There is a primitive type that behaves enough like an array, without requiring the additional constant size parameter: a slice. Working with a trait that has an associated slice, rather than an associated array, is a lot smoother:

trait FieldNamesAsSlice {
    const FIELD_NAMES_AS_SLICE: &'static [&'static str];
}

impl FieldNamesAsSlice for X {
    const FIELD_NAMES_AS_SLICE: &'static [&'static str] = &X::FIELD_NAMES_AS_ARRAY;
}

fn bar<T: FieldNamesAsSlice>() {
    println!("{:?}", T::FIELD_NAMES_AS_SLICE);
}

// a lot better!
bar::<X>();

Unfortunately we are unable to combine both traits (which would've been really cool) in the sense that we have only one trait (FieldNamesAsArray) we have to implement, while getting the other (FieldNamesAsSlice) for free, thanks to a blanket implementation like this:

impl<T, const N: usize> FieldNamesAsSlice for T where T: FieldNamesAsArray<N> {
    const FIELD_NAMES_AS_SLICE: &'static [&'static str] = &T::FIELD_NAMES_AS_ARRAY;
}

This does not work, because nothing stops us from implementing both FieldNamesAsArray<N> for X and FieldNamesAsArray<{N + 1}> for X. The compiler can't figure out which FIELD_NAMES_AS_ARRAY it should take, the one with size N or the one with size N + 1.

So since we can't meaningfully combine both traits, we should keep them separated (I want to keep the array variant because of the crate name). That way users can decide if they want to implement FieldNamesAsArray or FieldNamesAsSlice, or both:

use struct_field_names_as_array::{FieldNamesAsArray, FieldNamesAsSlice};

#[derive(FieldNamesAsArray, FieldNamesAsSlice)]
struct Foo {
    bar: String,
    baz: String,
    bat: String,
}

assert_eq!(Foo::FIELD_NAMES_AS_ARRAY, ["bar", "baz", "bat"]);
assert_eq!(Foo::FIELD_NAMES_AS_ARRAY, Foo::FIELD_NAMES_AS_SLICE);

Any thoughts on the proposed API? I feel like a FieldNamesAsSlice trait together with a procedural macro of the same name should fit the use-case of having FIELD_NAMES_AS_SLICE available on generic types quite nicely, do you agree?

@cchance27
Copy link
Author

Holy crap I, really sent you down a rabbit hole LOL...

API is definitely nicer looking to work with on the slice, I'm pretty sure you've covered the edge cases, and having it accessible as a slice to be able to work with the generics is definitely an improvement to usability. Honestly if it wasn't for the name of the crate the slice feels nicer in general to me, but that might just be me.

One question though you mentioned ...
This does not work, because nothing stops us from implementing both FieldNamesAsArray<N> for X and FieldNamesAsArray<{N + 1}> for X. The compiler can't figure out which FIELD_NAMES_AS_ARRAY it should take, the one with size N or the one with size N + 1.

My question is i don't see how this occurs unless someones purposefully trying to manually implement it on purpose to break it, since the crates designed to be used as a derive, I can't really derive the same struct with different amounts of properties, unless i'm missing something.

@jofas
Copy link
Owner

jofas commented Sep 22, 2023

Honestly if it wasn't for the name of the crate the slice feels nicer in general to me, but that might just be me.

Yeah. Originally I published v0.1.* with the "array" being a slice and only corrected that semantic mistake in v0.2.0. Since most users still use v0.1.* judging from the downloads from crates.io, you are not alone 😄.

My question is i don't see how this occurs unless someones purposefully trying to manually implement it on purpose to break it, since the crates designed to be used as a derive, I can't really derive the same struct with different amounts of properties, unless i'm missing something.

Yes sorry for this convoluted description. What I wanted to say is that the compiler doesn't know that there will only ever be a single FieldNamesAsArray<N> implementation for T (if the trait is used correctly). Because it doesn't know and because we can't guarantee that all T only ever implement FieldNamesAsArray<N> for a single N, the compiler won't allow us to compile the the trait generically over N for any T. Theoretically one could implement FieldNamesAsArray<N> for every N in usize for T which is enough for the compiler to throw a compile time error.

@cchance27
Copy link
Author

Ya that clears things up, and makes sense lol, adding an AsSlice with the trait would be great, and solve most of the hang ups... Guess that the one good thing for when people name crates obscure words instead of what they do because it gives flexibility XD.

Any chance you'll release a version with the FielNamesAsSlice+Trait available?

@jofas
Copy link
Owner

jofas commented Oct 2, 2023

Any chance you'll release a version with the FielNamesAsSlice+Trait available?

Yes, definitely. Would've already started but had a chaotic schedule lately. This week or maybe next I should be ready with the v0.3.0 release with the traits and slice functionality.

@jofas jofas closed this as completed in #25 Oct 10, 2023
@jofas jofas reopened this Oct 10, 2023
@jofas
Copy link
Owner

jofas commented Oct 10, 2023

Released v0.3.0 with the traits, I hope it fixes your issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants