-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Allow using Option<VNode> in html!() macro #2792
Conversation
Visit the preview URL for this PR (updated for commit 1dabd9c): https://yew-rs-api--pr2792-cecton-option-vnode-1xcjpour.web.app (expires Tue, 20 Sep 2022 20:27:50 GMT) 🔥 via Firebase Hosting GitHub Action 🌎 |
Size Comparison
✅ None of the examples has changed their size significantly. |
If you want to follow the paths of yew/packages/yew/src/utils/mod.rs Line 26 in c28a71e
|
Thanks @WorldSEnder !! .... 🤔 looks like type erasure or something..... tbh I'm not sure to understand the sorcery but I'm open for a better option than what I'm doing now (enumerating the types that are "string"). (Though what I'm doing doesn't have huge drawbacks imo... worst case scenario, someone would need to make |
I actually think it would strain some users.
Maybe the following playground can help https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=4dbe080258bd7da4175566d886e1899c. Try to remove the bounds on Each child expression of a html node (element or component) is converted via |
u8, u16, u32, u64, u128, | ||
i8, i16, i32, i64, i128, | ||
f32, f64, | ||
bool, | ||
usize, isize, |
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.
This adds something that #1637 tried to get rid of (for props). We don't want to be implicitly allocating String
s.
With this change, if someone were to pass in a number, it would automatically get converted to String
, incurring a heap allocation. That should be explicit, just like it is props
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'm not sure I'm following... before when you passed a primitive like that into the html!{} macro it would call ToString::to_string on it 🤔 am I missing something?
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.
Alright sorry I misread what you said!!
Ok so I agree with you and I'm all in favor of removing those! If I do so I will need to update a lot of tests (I can do it, np).
In the current implementation of VNode, ToString is used and string are allocated implicitly. But personally I do think it's best to be explicit. Especially when it involves allocating strings on the heap like that.
(Though this idea is conflicting with what @WorldSEnder point of view)
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.
With this change, if someone were to pass in a number, it would automatically get converted to String, incurring a heap allocation. That should be explicit, just like it is props
(so it's already how it works actually. I did not introduce that. I added this for compatibility because the tests were failing)
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.
To clarify my viewpoint: The current behavior is to convert via T: ToString
which has a blanket impl<T: Display> ToString for T
. Arguably, the String
will get converted to a JsString
soon after. That second allocation can not be avoided. I wonder if it was possible to use the Display
impl to go directly to the JsString
but that allocation (being part of FFI) is neither surprising nor avoidable. That being said, if a user would have to write .to_string()
manually, that would lead to (potentially) double the work, actually, since the resulting String
would still need to be converted to a JsString
as part of FFI.
As an aside, I think it's worthwhile to at least support fmt::Arguments
(which is currently automatic through Display -> ToString
).
Tl;dr: Custom logic that avoids the current String
allocation welcome, but pushing it down to the user will only result in more allocations.
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 have an(other) idea! 💡
What if we make our own syntax for string formatting. Maybe something like this:
<div>format!(....)</div>
(notice the lack of {}
around format! This is because it is not the actual format macro.
With that, instead of storing strings directly, we can store: the formatting string (static str) + the arguments (must implement ImplicitClone).
When the node is rendered, we compare all the arguments for equality.
This would avoid string heap allocation.
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.
@WorldSEnder @futursolo @hamza1311 I think I can make an implementation for this syntax:
fn compile_pass() {
::yew::html! { <div>format!("Hello {}!", "World")</div> };
}
It converts format!(...)
to a VFormat
type that holds all the arguments ("Hello {}!"
and "World"
) and does a PartialEq
between those arguments and the ones from another VFormat.
// https://users.rust-lang.org/t/partialeq-eq-hash-any/70495
// https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=c6be9c99223bb83fefc3094d42724f8b
#[derive(Clone)]
pub struct VFormat {
arguments: Rc<Vec<Box<TypeTagged<dyn TaggedEq>>>>,
}
impl PartialEq for VFormat {
fn eq(&self, other: &VFormat) -> bool {
self.arguments == other.arguments
}
}
However, it's rather complicated... if you don't mind I would prefer to do this in a separate PR and get this one merged. If this PR has no impact on performance at the moment, I think it's safe to merge. Let me know what you think.
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 think format!
and html!
are very similar which both creates an owned instance (String
vs VNode
) based on formatting a certain representation with variables and can involve allocations if needed.
I am in favour of merging current implementation of From<_> for VText
.
arguments: Rc<Vec<Box<TypeTagged<dyn TaggedEq>>>>,
This means that for each VFormat, we have an allocation overhead of:
- Rc<_>.
- Vec<_> for each argument.
- Box<_> for each argument.
I feel it might be faster to use std::format!
as it involves a single allocation overhead of creating a String.
In addition, types that implements std::fmt::Display
are not required to implement PartialEq
(nor does PartialEq
sound for certain types like f64
).
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.
Very good point... I think the VFormat thingy will reduce string formatting and conversion to JS but will increase the number of allocations. I'm not sure it's worth it.
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.
@futursolo I think I can reduce this to a single box for the entirety of the arguments. (But that won't solve the issue where PartiallEq is unsound (like for floats).
nahh. In Rust you always have to call Let me know your thought |
Benchmark - SSRYew Master
Pull Request
|
I think this PR is ready so please let me know if there is anything I need to fix. |
I think the point of @WorldSEnder still stand:
If we think that this PR will degrade the current status I really don't mind scrapping it, no worry. |
I still think the original goal of just allowing |
@WorldSEnder that would be great! I did try what you proposed in here #2792 (comment) but I couldn't make it work... feel free to use the branch and keep the PR if you want! I will add you to the fork |
31b161e
to
1dabd9c
Compare
Alright, got it to work. Can someone else review this, I don't want to "approve" my own code :) |
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.
Wow it was pretty simple! I don't know what blocked me last time... Thanks though!
(I can't approve as I am the author of the PR)
impl<IN: Into<OUT>, OUT> From<Option<IN>> for NodeSeq<IN, OUT> { | ||
fn from(val: Option<IN>) -> Self { | ||
Self( | ||
val.map(|s| vec![s.into()]).unwrap_or_default(), |
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 think you can actually iterate over options:
val.map(|s| vec![s.into()]).unwrap_or_default(), | |
val.into_iter().collect(), |
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 tested this on godbolt.org, and the two versions produce different assembly, where the former is slightly smaller (llvm seems to optimize the case that val = None
differently) than using collect
, so I stuck with that one.
Good catch though, do you think I should add a comment explaining that?
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 don't know 😅 my experience is more in high level languages...
Generally the rule is: if the intention is not obvious in the code, then it should be documented. So I would tend to say yes.
Long story:
Personally I wouldn't optimize that way because of my lack of experience. I just can't reason with it, I have too many questions in my mind. For example, is the code in assembly better for x86 or for WASM? Could it be different? Will that change in future releases of Rust/LLVM? I'm not asking you like they are relevant questions, I really don't know if those questions even make sense at all. I'm not even sure how to measure/evaluate which assembly code is faster (in terms of cycles or something I guess). So yeah I'm a total noob sorry xD I just can't answer for sure.
But I think that's the good part of the Rust community, we come with very different backgrounds. I prefer the readability of the iterator version but I totally agree it's more important to have fast code. The only reason for me to be against faster code is if it's very difficult to maintain (like if you would do everything unsafe (extreme scenario)). But here it's simple in both cases, the later code is more elegant which is a weak advantage compared to speed and size.
@hamza1311 @futursolo do you want to approve this one? Seems trivial thanks to @WorldSEnder |
Description
Quality of life improvement that allows using
Option<html! {}>
insidehtml! {}
macros.Example:
Checklist