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

Implicit optional attributes #1637

Merged
merged 45 commits into from
Apr 30, 2021
Merged

Implicit optional attributes #1637

merged 45 commits into from
Apr 30, 2021

Conversation

siku2
Copy link
Member

@siku2 siku2 commented Oct 29, 2020

Description

See #1550 for further details.

This is still very WIP but from the looks of it this is gonna require A LOT of breaking changes.

Closes: #1550
Closes: #1623 (this was required to support optional prop_or*)

Goals

  • Get rid of Transformer trait. It's already very confusing and it just keeps getting worse. Moves complexity out of the macro making it more transparent.
  • Rethink the implicit transformations done by the Transformer trait. It's no longer possible to sustain all of them without specialization. They also make it very easy to write suboptimal code because they obscure what's happening under the hood.
  • Unify the component and element prop behaviour.

TODO

  • Update a ton of documentation 😭 (done by @cecton)
  • Add a proper optional property attribute for Option<T> types. #[prop_or*] can't be used for this because types end up being Option<Option<T>> which is very confusing. (done by @cecton)

@siku2 siku2 added ergonomics macro Issues relating to our procedural or declarative macros labels Oct 29, 2020
@siku2 siku2 added this to the v0.18 milestone Oct 29, 2020
@siku2
Copy link
Member Author

siku2 commented Oct 30, 2020

@jstarry, I'd love to hear your thoughts on the following two points:

1. Unifying attributes with props

Attributes now behave like a prop with type #[prop_or_default] Option<Cow<'static, str>> and listeners behave like #[prop_or_default] Option<Callback<EVENT>>.
The reason for this (apart from internal consistency) is that it helps move us toward a properly typed DOM (which will come into play when we switch to binding to properties instead of attributes [#1322]).
This comes with the breaking change that attributes no longer accept any old impl ToString type.

2. Pesky transformations

Rust really doesn't like it but now we do actually need to have a "transformer" trait to support implicit optional attributes.
Specifically, for optional props we need at least T -> Option<T> and Option<T> -> Option<T>.
Everything else is technically optional, but we probably want it for convenience.

The &T -> T transformation is desired because props currently have to own their data (with the exception of static references of course). However, a big problem with this transformation is that it hides a potentially very expensive operation which can lure the user into writing non-optimal code. My current implementation requires that T implements the marker trait ImplicitClone. It allows the user to easily pass down things like &Rc<T>, &Callback, and even &String (though that last one arguably shouldn't be implicit, we just don't really have a better solution right now) but stops them from accidentally cloning their ExpensiveState struct all over the app.

So far we haven't introduced any type conversions, we've basically only expanded on Rust's type coercions, but there's at least one area where we want to allow some type conversions: strings. Specifically, we want [&'static str | String] -> Cow<'static, str> and &'static str -> String so we can use string literals for props.

This is where the problem begins. In an ideal world we could just implement the clone conversion as &T -> V where T: ImplicitClone and [T -> V] and the two conversions for optional props as T -> Option<V> where [T -> V] and Option<T> -> Option<V> where [T -> V] but Rust isn't there yet. None of these can be implemented today.
Currently, in order to support &'static str -> Cow<'static, str> we have to manually implement:

  • &'static str -> Cow<'static, str>
  • &'static str -> Option<Cow<'static, str>>
  • Option<&'static str> -> Option<Cow<'static, str>>

If we want to support the ImplicitClone conversion for a type transformation, each of the three implementations also has to be implemented for the reference.
This isn't a big deal internally, we can use a macro. The problem is that, as a user, implementing this for a custom type is a nightmare.

Btw, the situation isn't all that different from before, it's just that the new optional attributes really makes this problem obvious.

@jstarry
Copy link
Member

jstarry commented Nov 5, 2020

@siku2 sorry for the delay. First off, I whole-heartedly agree with your stated goals. As for your two points, I just skimmed and your arguments seem reasonable but I haven't expended any serious thought into it. I trust your knowledge over the finer details of Rust's type system over my own at this point. That being said, I'd like to learn more and I can find time over the weekend to dive into this more if you would like to talk things out more. But if you're pretty comfortable with the current direction, by all means blaze ahead 👍

@siku2 siku2 mentioned this pull request Nov 5, 2020
3 tasks
@siku2
Copy link
Member Author

siku2 commented Nov 7, 2020

But if you're pretty comfortable with the current direction, by all means blaze ahead +1

@jstarry
Hah, that's the problem, I'm really not :D
I feel like the current solution is too complicated to easily explain (which isn't a good thing for something that happens behind the scenes) and it's probably not a good idea to fight the compiler this much in the first place.
Without the specialization feature I don't think it's possible to fully realise my approach without residing to some hacks. Since specialization seems to be nowhere near stabilization we'll just have to accept that for now.

I was hoping your input might give me some inspiration. If you do happen to come up with an idea, as crazy as it might be, I would love to hear it.
For now I'll just push ahead with the current approach, maybe I can come up with something while writing the documentation.

Oh and I'd also love to know which transformations (if any?) you think are essential.

@jstarry
Copy link
Member

jstarry commented Nov 15, 2020

This is where the problem begins. In an ideal world we could just implement the clone conversion as &T -> V where T: ImplicitClone and [T -> V] and the two conversions for optional props as T -> Option where [T -> V] and Option -> Option where [T -> V] but Rust isn't there yet

Yeah, I think it's ok to skip those.

The &T -> T transformation is desired because props currently have to own their data

I think it's better to have the clones be explicit and avoid adding the ImplicitClone trait market.

we need ... &'static str -> String

Do we?

@jstarry
Copy link
Member

jstarry commented Nov 22, 2020

I think it's better to have the clones be explicit and avoid adding the ImplicitClone trait market.

Actually this would be really nice for callbacks

@siku2
Copy link
Member Author

siku2 commented Nov 22, 2020

I think it's better to have the clones be explicit and avoid adding the ImplicitClone trait market.

Actually this would be really nice for callbacks

Yeah, I originally wanted to remove implicit cloning altogether but I think it's really nice for things like Rc which is why I came up with the ImplicitClone marker in the first place. I want to remove it from String though because there it's deceiving at best.

we need ... &'static str -> String

Do we?

We don't really, but I don't think it's harmful either. I added it because many components accept String props and if you want to pass down a string literal you have to use <Foo prop="value".to_owned() /> anyway. I'm perfectly happy to remove it but this is much less of a problem than the Option<_> stuff.

@jstarry
Copy link
Member

jstarry commented Jan 23, 2021

Hey @siku2 what are your latest thoughts on this PR? Think it's ready to come out of draft mode? When we make a decision on it, I would like to have it cherry picked to the v0.18 branch after merging to master

@siku2
Copy link
Member Author

siku2 commented Jan 23, 2021

@jstarry It's been a while but I think the issue still lies with the ergonomics of it all. I think I was unhappy with how complex the rules still are... I'll try to get this up to speed as soon as possible but at this point I certainly wouldn't mind if someone else got there before me :P

@jstarry jstarry changed the base branch from master to v0.18 January 24, 2021 00:58
Comment on lines -115 to -153
## Transformers

Whenever you set a prop its value goes through a transformation step first.
If the value already has the correct type, this step doesn't do anything.
However, transformers can be useful to reduce code repetition.

The following is a list of transformers you should know about:

- `&T` -> `T`

Clones the reference to get an owned value.

- `&str` -> `String`

Allows you to use string literals without adding `.to_owned()` at the end.

- `T` -> `Option<T>`

Wraps the value in `Some`.

```rust
struct Props {
unique_id: Option<usize>,
text: String,
}

struct Model;
impl Component for Model {
type Properties = Props;

// ...
}

// transformers allow you to write this:
html! { <Model unique_id=5 text="literals are fun" /> };
// instead of:
html! { <Model unique_id=Some(5) text="literals are fun".to_owned() /> };
```

Copy link
Member

Choose a reason for hiding this comment

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

I removed the entire section here. I added a note about Into<Option<T>> later on.

<@{format!("h{}", level)} class="title">{ content }</@>
<@{format!("h{}", level)} class="title">{ text }</@>
Copy link
Member

Choose a reason for hiding this comment

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

Would be better if the tests were executed 😞

Comment on lines 24 to 25
Most HTML attributes can be marked as optional by placing a `?` in front of
the `=` sign. This makes them accept the same type of value as otherwise, but
wrapped in an `Option<T>`:
Most HTML attributes can use optional values (`Some(x)` or `None`). This allows
to omit the attribute if the attribute is marked as optional.
Copy link
Member

Choose a reason for hiding this comment

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

I tried to put things simply. But any advice are welcome 😅

Comment on lines +37 to +47
Please note that it is also valid to give only the value as properties behave
like `Into<Option<T>>`:

```rust
let id = "foobar";

html! {
<div id=id></div>
}
```

Copy link
Member

Choose a reason for hiding this comment

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

This is the remark I added about the optional attributes.


impl ImplicitClone for NodeRef {}
impl<Comp: Component> ImplicitClone for Scope<Comp> {}
// TODO there are still a few missing like AgentScope
Copy link
Member

Choose a reason for hiding this comment

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

I did the AgentScope... Can someone help me enumerate what else is missing? 😅

Copy link
Member

Choose a reason for hiding this comment

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

I can't think of anything else that is missing. In the worst case, the user will have to call clone if this trait isn't implemented. Let's just roll with it for now. We'll know what's missing once this code is merged and used.

Copy link
Member

Choose a reason for hiding this comment

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

Fine by me

Copy link
Member

@ranile ranile left a comment

Choose a reason for hiding this comment

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

It seems like the failing CI is because of new Clippy lints introduced in Rust 1.51. #1801 fixed those in master so I believe it can be rebased here too.

docs/concepts/html/elements.md Outdated Show resolved Hide resolved
packages/yew-macro/src/derive_props/field.rs Outdated Show resolved Hide resolved
Comment on lines +61 to +65
.pop("class")
.map(|prop| ClassesForm::from_expr(prop.value));
let value = props.pop("value");
let kind = props.pop("type");
let checked = props.pop_nonoptional("checked")?;
let checked = props.pop("checked");
Copy link
Member

Choose a reason for hiding this comment

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

Is there any reason why these 4 are special cased?

Copy link
Member

Choose a reason for hiding this comment

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

No idea...

// TODO there are still a few missing like AgentScope

/// A trait similar to `Into<T>` which allows conversion to a value of a `Properties` struct.
pub trait IntoPropValue<T> {
Copy link
Member

Choose a reason for hiding this comment

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

Can we implement this for T and &T where T: ToString?

Copy link
Member

Choose a reason for hiding this comment

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

No... I think it's because of specialization

@cecton cecton marked this pull request as ready for review April 25, 2021 08:09
cecton and others added 3 commits April 25, 2021 13:44
Co-authored-by: Muhammad Hamza <muhammadhamza1311@gmail.com>
…ests (yewstack#1801)

* update rust version for macro test to 1.51

* enable const generic tests

* run integration tests using MSRV

* am blind

* clippy, fmt

* apply suggestion
Co-authored-by: Muhammad Hamza <muhammadhamza1311@gmail.com>
@cecton
Copy link
Member

cecton commented Apr 25, 2021

It seems like the failing CI is because of new Clippy lints introduced in Rust 1.51. #1801 fixed those in master so I believe it can be rebased here too.

Thanks a lot!! I cherry-picked 🙏

@cecton cecton merged commit 99d5088 into yewstack:v0.18 Apr 30, 2021
@siku2 siku2 deleted the optional-attrs branch May 5, 2021 20:48
ranile pushed a commit to ranile/yew that referenced this pull request Jun 5, 2021
ranile pushed a commit to ranile/yew that referenced this pull request Jun 5, 2021
siku2 added a commit that referenced this pull request Jun 5, 2021
@voidpumpkin voidpumpkin added the A-yew Area: The main yew crate label Nov 26, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-yew Area: The main yew crate ergonomics macro Issues relating to our procedural or declarative macros
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Properties derive macro calls prop_or_else function even when prop is set Optional Attributes Chapter 2
6 participants