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

Introduce immutable string, array and map #2563

Merged
merged 40 commits into from
Jun 24, 2022
Merged

Conversation

cecton
Copy link
Member

@cecton cecton commented Mar 31, 2022

Description

Please check the example immutable in this PR to see how it is used.

I have started some time ago an experiment of porting Immutable.js to Yew. This basically leverage the use of ImplicitClone on 3 core types: String, Array and Map. And use garbage collection with Rc.

  • The immutable string type already exists in Yew for some time. It has been introduced in Add custom type for attribute values #1994 It's very handy for passing cheap strings around and it is also very useful for library maintainers who want to have string properties that work with &str and String indifferently and with Rc.
  • The immutable array type allows creating a cheap Rc'ed array of static size of any type. It's very similar to the immutable string type.
  • The immutable map type allows creating a cheap IndexMap. This is slightly more complicated because the map is not a primitive in Rust. Here we use either: an Rc<IndexMap<K, V>> or a &'static [(K, V)].

Following the discussion in #2448 I realized that it is better to create a more generic "immutable" crate rather than crating a "yew-immutable" crate. This is why I created this new project: https://github.com/rustminded/imut It's currently not published on crates.io yet because I prefer getting the green light here first. If my PR here is approved I will add the documentation and publish imut. (I couldn't take the crate immutable because it is already taken and I couldn't reach the owner.)

To avoid breaking compatibility I added a type alias from AttrValue to IString. Those types were identical anyway.

I also added a re-export of imut to yew::immutable so people can import them easily if they want without the need to add imut to their dependencies.

It is best for the reviewers to also review https://github.com/rustminded/imut and put their remarks in this PR. The most important changes are there after all.

Fixes #2448

Checklist

  • I have run cargo make pr-flow
  • I have reviewed my own code
  • I have added tests
  • I have added documentation to imut
  • I have added a similar API of immutable.js to imut [edit: conflicting too much with rust's api... not a good idea, it would be confusing]
  • I have published imut
  • Add doc to Yew's documentation web site

github-actions[bot]
github-actions bot previously approved these changes Mar 31, 2022
github-actions[bot]
github-actions bot previously approved these changes Mar 31, 2022
@github-actions
Copy link

github-actions bot commented Mar 31, 2022

Visit the preview URL for this PR (updated for commit 1e4b52a):

https://yew-rs--pr2563-immutable-gv5uzv7v.web.app

(expires Fri, 01 Jul 2022 14:51:15 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

github-actions[bot]
github-actions bot previously approved these changes Mar 31, 2022
@cecton cecton requested a review from ranile March 31, 2022 11:18
@cecton
Copy link
Member Author

cecton commented Mar 31, 2022

👋 @hamza1311 can I have your feedback on this before I complete the documentation and publish imut? Thanks

@github-actions
Copy link

github-actions bot commented Mar 31, 2022

Size Comparison

examples master (KB) pull request (KB) diff (KB) diff (%)
boids 172.582 172.725 +0.143 +0.083%
contexts 109.624 109.604 -0.020 -0.018%
counter 86.551 86.680 +0.129 +0.149%
counter_functional 87.201 87.335 +0.134 +0.153%
dyn_create_destroy_apps 89.664 89.792 +0.128 +0.143%
file_upload 102.626 102.796 +0.170 +0.166%
function_memory_game 166.895 167.106 +0.212 +0.127%
function_router 352.024 352.200 +0.176 +0.050%
function_todomvc 161.558 161.760 +0.202 +0.125%
futures 226.235 226.347 +0.111 +0.049%
game_of_life 107.187 107.318 +0.132 +0.123%
immutable N/A 208.365 N/A N/A
inner_html 83.618 83.743 +0.125 +0.149%
js_callback 112.854 112.830 -0.024 -0.022%
keyed_list 195.107 195.301 +0.193 +0.099%
mount_point 86.181 86.317 +0.137 +0.159%
nested_list 115.688 115.736 +0.049 +0.042%
node_refs 93.599 93.739 +0.141 +0.150%
password_strength 1538.156 1538.440 +0.284 +0.018%
portals 97.224 97.335 +0.111 +0.115%
router 320.810 320.996 +0.187 +0.058%
simple_ssr 154.489 154.474 -0.016 -0.010%
ssr_router 398.411 398.571 +0.160 +0.040%
suspense 110.538 110.519 -0.020 -0.018%
timer 89.261 89.369 +0.108 +0.121%
todomvc 142.617 142.820 +0.203 +0.142%
two_apps 87.161 87.330 +0.169 +0.194%
web_worker_fib 153.408 153.544 +0.136 +0.088%
webgl 87.438 87.562 +0.123 +0.141%

✅ None of the examples has changed their size significantly.

@ranile
Copy link
Member

ranile commented Mar 31, 2022

This looks fine for the most part. ImplicitClone should still be part of Yew. immutable doesn't use it whereas Yew does. It may also help us avoid issues with orphan rules down the line where we can't implement ImplicitClone for a type because we don't own the trait

@cecton
Copy link
Member Author

cecton commented Apr 7, 2022

ImplicitClone should still be part of Yew.

Actually that's not good. ImplicitClone means it's cheap to clone. From Yew's perspective it means we can pass it easily through props. But from imut's perspective it means the type doesn't need to be taken by reference but can be cloned easily.

This is why in IArray, for instance, there is a requirement that T implements ImplicitClone so when you call .iter() on IArray it yields owned types and not references. If you check the yew example I made here for IArray, in the method fn update() of the yew component, you will see that it is very convenient.

It may also help us avoid issues with orphan rules down the line where we can't implement ImplicitClone for a type because we don't own the trait

That's a Rust problem in general. But I think this is a special case. Usually it is solved by wrapping the type in a container and implementing Deref. But I can also imagine adding features to imut that would implement that trait for specific crates. I wouldn't worry about that.

@cecton
Copy link
Member Author

cecton commented Apr 7, 2022

This macro looks nice btw. It emulates the deconstructor of JS:

    #[test]
    fn imap_deconstruct() {
        let my_imap = [(IString::from("foo"), 1), (IString::from("bar"), 2)]
            .into_iter()
            .collect::<IMap<IString, u32>>();
        imap_deconstruct!(
            let { foo, bar, baz } = my_imap;
            let { foobarbaz } = my_imap;
        );
        assert_eq!(foo, Some(1));
        assert_eq!(bar, Some(2));
        assert_eq!(baz, None);
        assert_eq!(foobarbaz, None);
    }

@ranile
Copy link
Member

ranile commented Apr 7, 2022

I can't help but feel like we are going in the opposite direction of what Rust tells us to do

@cecton
Copy link
Member Author

cecton commented Apr 7, 2022

I can't help but feel like we are going in the opposite direction of what Rust tells us to do

That's because it is! Glad you see it too.

Props are propagated from parents to child, you can't pass reference in props of components because the children are updated later. You need garbage collection to propagate props or you'll be cloning a lot of memory. That's why we put those Rc everywhere.

github-actions[bot]
github-actions bot previously approved these changes Jun 16, 2022
@cecton
Copy link
Member Author

cecton commented Jun 16, 2022

@WorldSEnder done 👍 all tests are green

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.

This can be merged after #2740 because otherwise CI breaks

website/docs/advanced-topics/immutable.mdx Outdated Show resolved Hide resolved
@@ -29,6 +29,7 @@ thiserror = "1.0"

futures = { version = "0.3", optional = true }
html-escape = { version = "0.2.9", optional = true }
implicit-clone = { version = "0.2", features = ["map"] }
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to enable map feature? I don't think we use it.

Copy link
Member Author

Choose a reason for hiding this comment

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

No... I added it because there was the re-export and it felt like a core feature. But on the other hand, it's an invitation to use an anti-pattern that is not even useful for Yew. It's maybe best to remove it entirely from implicit-clone too

Copy link
Member

Choose a reason for hiding this comment

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

I agree that we should remove it:

Suggested change
implicit-clone = { version = "0.2", features = ["map"] }
implicit-clone = { version = "0.2" }

Copy link
Member Author

Choose a reason for hiding this comment

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

What do you think about removing it from implicit-clone too? You're also maintaining a UI framework so I guess you probably met a lot of use cases where we need to pass lists in properties and maybe different types.

On my side I didn't find any use of passing maps. The only component that does use a more complex structure for state is the tree but I opted for using a specialized tree library instead. The ones that use Vec will greatly benefit from IArray.

Copy link
Member

Choose a reason for hiding this comment

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

Attributes is more-or-less a map, although it's more specialized I guess. Overall, it makes sense to have such a thing in implicit-clone, though I confess I haven't looked at the implementation what you're currently using to implement it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah yes I forgot there is that Attributes thingy...

Well... yes probably Attributes is the same (at least same idea) than IMap but it shouldn't be a concern here. Attributes is used for internal machinery in Yew. Here we are talking about API and what tools we want to expose to the user. I don't know if anyone needs Attributes? Do they? Oh... it's public API... haha okay so maybe 😁 why is this public API?? xD (cc @siku2 maybe you know something? I think you've been maintaining Yew the longest)

Copy link
Member Author

Choose a reason for hiding this comment

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

@hamza1311 I forgot to mention this but the map feature brings zero dependencies as it depends on indexmap which yew depends on anyway.

...come to think of it... 🤔 even if I didn't find a use in my use cases, it seems that having an immutable map collection is still something useful. IMap has 2 advantages:

  1. it implements ImplicitClone: this means all its API yields clones instead of references which is very useful for properties
  2. you can create static collections (for small collections)

I still removed the feature in the dependencies because it's optional anyway. I did it because I originally re-exported but now that we don't re-export there is absolutely no point. (tbh... I really don't get why we ban re-export 🤷‍♀️)

@WorldSEnder I just checked Attributes and it has quite some big differences:

  • Attributes can only hold strings vs IMap can hold anything that implements ImplicitClone itself: it could be faster to use Attributes
  • Attributes has a variant Dynamic which allow using subslices of an existing set of keys/values": it's probably used to optimize Yew somewhere... I'm not sure if IMap should also implement this, I don't see much value tbh.

Overall I have the feeling that Attributes is really specialized for Yew. The reason why it is public API is because it is used by yew-macro so it has to be.

Copy link
Member Author

Choose a reason for hiding this comment

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

@hamza1311 oh nevermind what I said. We have to keep the feature map otherwise I can't make the impls for IntoPropValue which is a yew trait. Chicken and egg problem.

Unless there is a way to put a #[cfg()] on the features of a child dependency, I don't see any other way. It's not a big deal anyway since it brings no dependencies.

Copy link
Member

@ranile ranile Jun 24, 2022

Choose a reason for hiding this comment

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

It doesn't matter either way. I'm fine with keeping it or removing it. I brought it up because I didn't see any uses of it

Copy link
Member Author

Choose a reason for hiding this comment

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

Dropping this link to a comment here yewstack/implicit-clone#7 (comment)

It's interesting because @XX is making a widget library and he wants to allow the user to set/get attributes on a component.

cc @WorldSEnder

Co-authored-by: Muhammad Hamza <muhammadhamza1311@gmail.com>
github-actions[bot]
github-actions bot previously approved these changes Jun 20, 2022
github-actions[bot]
github-actions bot previously approved these changes Jun 21, 2022
github-actions[bot]
github-actions bot previously approved these changes Jun 21, 2022
@cecton
Copy link
Member Author

cecton commented Jun 21, 2022

I'm not sure what I need to do to fix the tests 🤷‍♀️

# Conflicts:
#	packages/yew-macro/tests/html_macro/component-fail.stderr
#	packages/yew-macro/tests/html_macro/element-fail.stderr
@ranile
Copy link
Member

ranile commented Jun 24, 2022

I'm not sure what I need to do to fix the tests 🤷‍♀️

I fixed them

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.

Finally! We're ready to merge this in

@ranile ranile requested a review from WorldSEnder June 24, 2022 14:47
@cecton
Copy link
Member Author

cecton commented Jun 24, 2022

Thanks a lot for the fixes @hamza1311 🤗

And thanks everybody for all your time on the matter! ❤️

@cecton cecton merged commit 7ddf267 into yewstack:master Jun 24, 2022
@cecton cecton deleted the immutable branch June 24, 2022 15:58
@WorldSEnder WorldSEnder mentioned this pull request Jun 26, 2022
29 tasks
@martinitus
Copy link

This seemed to be a pretty substantial change to yew, would be awesome to read about it somewhere in the "next" docs :)
Was really nice reading through this :)

@cecton
Copy link
Member Author

cecton commented Aug 13, 2022

Oh sorry I think the doc on that will be very succinct. It is worth a blog post in my opinion but every time I tried to explain the theory I feel like people did not get it. Maybe a post with an example that shows in which order the components are updated and when the memory is freed would be the best way to understand the concept.

To summarize, it's two-folded:

  1. React-like frameworks (components updating from root (top) to child (bottom)) work great when you have a garbage collector... but it's terrible in Rust because the child properties must outlive its parent properties. Very very succinct right? xD

  2. The other issue immutables is solving is for Yew libraries that require to pass arrays, strings and maps in properties because it can reduce the amount of allocations. Example of component that uses a list: the drop list https://yewprint.rm.rs/html-select Allocating vecs in the fn view() is not great at all.

@ranile
Copy link
Member

ranile commented Aug 21, 2022

It is worth a blog post in my opinion

@cecton do you want to write on up?

@cecton
Copy link
Member Author

cecton commented Aug 21, 2022

@hamza1311 sure! xD There are so many things I say I will/would do. I should stop saying things I would do and start doing haha I will put that on my TODO list

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 this pull request may close these issues.

6 participants