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

[POC] Redo properties #2369

Closed
wants to merge 3 commits into from
Closed

Conversation

ranile
Copy link
Member

@ranile ranile commented Jan 12, 2022

This PR is a proof-of-concept which redoes properties. Right now, the new properties attribute macro is not feature complete and thus the code does not compile. However, this implementation demonstrates inheritance for props (see #1533)

It is possible to build upon on this implementation. While implementing this, I ran into some roadblocks that may or may not be possible to overcome while keeping certain API in mind.

My Observations

  • If we want properties to be extendable, they must be Default
  • Because of the nature of Rust type system, we must use Deref/DerefMut to "emulate" inheritance for properties. This results in function signature of setters similar to: fn class(&mut self, value: AttrValue). These are called on the builder struct one-by-one and cannot be chained.
  • Regular properties (ones that do not extend other properties, and are not Default) are checked at compile time. This is possible by following "typestate pattern". The function signature of the setter is used to ensure all required props are set and handle the default cases.
  • The function signature required for compile time verification does not match with the one required to "emulate" inheritance.
  • Because of the above point, we cannot construct the properties from the html! macro
    • A workaround would be to panic! at runtime for missing props. This would ensure the function signature is the same.

TL;DR

Having both, compile time verification of validity of passed props and a fix for #1533 (props inheritance) is impossible. We need to pick a poison and go with it.


We should explore more ways to solving the issue at hand: being able to pass props which are not explicitly defined in the Properties struct (may be defined else where)

@WorldSEnder
Copy link
Member

WorldSEnder commented Jan 12, 2022

My observations with Properties and how they work so far:

You have a Builder, you can get a fresh instance of with Properties::builder. Inheritance with Deref/DerefMut on the builder could work by one builder storing the builder for the inherited properties type.

A wrapper struct around the builder keeps track of the "step" at which to builder is to store the state in a generic argument. This is kinda of hacky and I think there are now better ways to do it. After bumping to 1.56 I think we can do better and use const fn to keep track of this, which might even lead to better error messages. While it would probably allow users who don't accept #[doc(hidden)] to peak behind the curtain and build objects without keeping track of the set properties when calling build, the actual values are unwrapped anyway.

Tl;dr: don't use generics to keep track of set props, use a const fn. Or I might be wrong and const fn is not sufficient as of now.

EDIT: ah, I see there is const_deref which would allow this? but is not stabilized, I was under the impression it already was :(

EDIT2: Look mah only two feature gates :(

@WorldSEnder
Copy link
Member

Can you maybe expand upon

If we want properties to be extendable, they must be Default

I don't see why that is necessary.

@WorldSEnder
Copy link
Member

Another experiment, this one actually works on stable 1.56.

@ranile
Copy link
Member Author

ranile commented Jan 14, 2022

Can you maybe expand upon

If we want properties to be extendable, they must be Default

I don't see why that is necessary.

We need to construct the builder struct. In order to do so, we need to able to name all the fields, which we can't because we have no idea (other than the ident) of the struct passed as extends. This is solved by requiring it to be Default. See the generated code here:

fn build_extended_props(&self) -> TokenStream {
let ItemStruct {
attrs,
vis,
struct_token,
ident,
generics,
..
} = &self.item;
let fields = self.fields();
let setters = self
.fields()
.map(|field| build_setters(field.ident.as_ref().unwrap(), &field.ty, false));
let extends_ty = &self.extends_ty.as_ref().unwrap();
let builder_ident = self.builder_ident();
quote! {
#(#attrs)*
#vis #struct_token #ident #generics {
#(#fields)*,
__parent: #extends_ty
}
impl #ident {
#(#setters)*
}
impl ::std::ops::Deref for #ident {
type Target = #extends_ty;
fn deref(&self) -> &Self::Target {
&self.__parent
}
}
impl ::std::ops::DerefMut for #ident {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.__parent
}
}
struct #builder_ident { props: #ident }
impl #builder_ident {
fn build(self) -> #ident {
self.props
}
}
impl ::yew::Properties for #ident {
type Builder = #builder_ident;
fn builder() -> Self::Builder {
#builder_ident { props: Self::default() }
}
}
impl ::std::ops::Deref for #builder_ident {
type Target = #ident;
fn deref(&self) -> &Self::Target {
&self.props
}
}
impl ::std::ops::DerefMut for #builder_ident {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.props
}
}
}
}

I couldn't think of any other way to handle this case.


Another experiment, this one actually works on stable 1.56.

It seems that we have to know all the props upfront. We can't know that because in the proc-macro, all we have access to is the ident of ancestor.

const FOO_PROPS: RequiredProps = RequiredProps {
    own: &["a"],
    ancestor: None,
};

const BAR_PROPS: RequiredProps = RequiredProps {
    own: &["b"],
    ancestor: Some(&FOO_PROPS),
};

const MORE_PROPS: RequiredProps = RequiredProps {
    own: &["c"],
    ancestor: Some(&BAR_PROPS),
};

At the time of constructing more_props, we have no idea which props it can take and who they belong to.
It is possible that I may be misunderstanding something. If that is the case, please correct me.

@WorldSEnder
Copy link
Member

WorldSEnder commented Jan 14, 2022

We need to construct the builder struct. In order to do so, we need to able to name all the fields, which we can't because we have no idea (other than the ident) of the struct passed as extends. This is solved by requiring it to be Default. See the generated code here:

I didn't expect struct #builder_ident { props: #ident }. I was thinking that the following props should translate to the builder below (excuse the non-typechecked code). And excuse me also for changing the syntax slightly to be more aligned with the existing one.

#[derive(PartialEq, Properties)]
struct BaseProps {
    #[prop] required: String,
    #[prop_or_default] optional: String,
}
#[derive(PartialEq, Properties)]
struct ExtendedProps {
    #[prop] foo: String,
    #[prop_extends] base: BaseProps, // only allow one item to be marked, corresponds to the extends =
}
// Expanded builders (not considering the wrapper for state)
struct BasePropsBuilder {
    required: Option<String>,
    optional: Option<String>,
}
struct ExtendedPropsBuilder {
    foo: Option<String>,
    base: BasePropsBuilder, // expanded from BaseProps::Builder
}
impl Deref for ExtendedPropsBuilder {
    type Target = BasePropsBuilder;
    fn deref(&self) -> &Self::Target {
        &self.base
    }
}
// + DerefMut. Stil can't implement the old style of validating all required props, since the type of BasePropsBuilder changes

That way props shouldn't need to be Default.


It seems that we have to know all the props upfront. We can't know that because in the proc-macro, all we have access to is the ident of ancestor.

Well, at the point of the html! {}/the props! {} macro invocation, you do know all attributes that the user provided. FOO_PROPS, BAR_PROPS and MORE_PROPS would be associated constants on the Builder (and some internal trait) that are generated by the derive(Properties). Then the html! {} and props! {} macro would include a call

const _: () = check_all_present(&Props::Builder::REQUIRED_PROPS, &[<list of all provided props>]);

which errors if not all of them are given. No idea if this scales, if good enough error messages can be constructed or if this is really practical 😆

@ranile
Copy link
Member Author

ranile commented Jan 14, 2022

It seems that we have to know all the props upfront. We can't know that because in the proc-macro, all we have access to is the ident of ancestor.

Well, at the point of the html! {}/the props! {} macro invocation, you do know all attributes that the user provided. FOO_PROPS, BAR_PROPS and MORE_PROPS would be associated constants on the Builder (and some internal trait) that are generated by the derive(Properties). Then the html! {} and props! {} macro would include a call

const _: () = check_all_present(&Props::Builder::REQUIRED_PROPS, &[<list of all provided props>]);

which errors if not all of them are given. No idea if this scales, if good enough error messages can be constructed or if this is really practical laughing

This approach looks really interesting to me but I can't wrap my head around it. Can you give some rough input and output of the macro?

@WorldSEnder
Copy link
Member

This approach looks really interesting to me but I can't wrap my head around it. Can you give some rough input and output of the macro?

Input in comments, output in code.

//#[derive(PartialEq, Properties)]
//struct FooProps {
//    a: String,
//}
const FOO_PROPS: RequiredProps = RequiredProps {
    own: &["a"],
    ancestor: None,
};
//#[derive(PartialEq, Properties)]
//struct BarProps {
//    b: String,
//    #[prop_extends]
//    base: FooProps,
//}
const BAR_PROPS: RequiredProps = RequiredProps {
    own: &["b"],
    ancestor: Some(&FOO_PROPS),
};

pub fn main() {
    // html! { <Foo a="a string" /> }
    const _: () = check_all_present(&FOO_PROPS, &["a"]); // No error
    // html! { <Bar a="a string" /> }
    const _: () = check_all_present(&BAR_PROPS, &["a"]); // error!
                                                         // although the error message is not too pretty
    // html! { <Bar a="a string"  b="another string" /> }
    const _: () = check_all_present(&BAR_PROPS, &["a", "b"]); // No error
    // Remember: this should only check that all props were provided, building them is separate.
}

Copy link
Member

@voidpumpkin voidpumpkin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment in #1533

@ranile
Copy link
Member Author

ranile commented Jan 19, 2022

// Remember: this should only check that all props were provided, building them is separate.

@WorldSEnder, The big problem comes from building because of function signatures. If you have a way to implement a builder with this pattern, I would love to hear it.

@voidpumpkin
Copy link
Member

@hamza1311 So as I understand this PR can become a draft as we will not be pursuing this change?

@ranile
Copy link
Member Author

ranile commented Jan 22, 2022

#2396 exists. Closing this.

@ranile ranile closed this Jan 22, 2022
@WorldSEnder
Copy link
Member

For completeness, I have tried yet another approach to inheritance, and apart from requiring a feature (adt_const_params), I think it's the cleanest of all of them so far...

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=1df819a9d6290c1a959645eabd97b49f

@ranile
Copy link
Member Author

ranile commented Apr 6, 2022

That looks clean. Is there a way to backport this to stable? Instead of &'static str, we should use the ASCII of the prop name and pass it as a number (don't know if that's even possible)?

@WorldSEnder
Copy link
Member

It might be possible to use the hashes of prop names to identify them with a low chance of collisions. Downside is that at the moment I have no idea for making the error messages readable then :/

@ranile
Copy link
Member Author

ranile commented Apr 6, 2022

I think we may be able to take a page from typed-builder's book and use deprecated warnings for errors. The compile error can be unreadable but that wouldn't matter

@WorldSEnder
Copy link
Member

WorldSEnder commented Jun 10, 2022

It's that time of the month again, and I think this time I've done it! A stable version that seems like a very clean implementation:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=1df70e8c455065ad67679039cd9f704f

It would take some time to write the macro code for all this, but i think it's doable!

@WorldSEnder WorldSEnder mentioned this pull request Jun 12, 2022
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants