-
Notifications
You must be signed in to change notification settings - Fork 146
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
Proposal: gloo-properties - mid-level API for getting and setting properties #53
Comments
Relatedly, we should have convenient APIs for looking up keys on existing objects (since that's actually a really common thing to do, and it's quite nasty right now!) |
Thanks for writing this up! Questions off the top of my head:
|
Here's an idea for a possibly better API: Create a trait Then lets create a function pub fn set_property<K: IntoJs, V: IntoJs>(object: &Object, key: K, value: V) {
Reflect::set(object, key.into_js(), value.into_js()).unwrap_throw();
} and a macro using this function that can be used like this: let my_obj = set_props! { Object::new(), with
"foo" -> "bar",
"quux" -> 123u8,
"b0rk" -> 123.1,
"meep" -> any_old_JsValue,
"blerg" -> set_props!(Object::new(), with "potato" -> "spaghetti"),
}; This can also be used to update to an existing object: set_props! { my_obj, with
"foo" -> 123u8,
"quux" -> "bar,
}; |
I tried it out, and it works with a recursive Show codepub trait IntoJs {
fn into_js(&self) -> JsValue;
}
pub fn set_property<K: IntoJs, V: IntoJs>(object: &Object, key: K, value: V) {
Reflect::set(object, &key.into_js(), &value.into_js()).unwrap_throw();
}
#[macro_export]
macro_rules! set_props {
( $obj:expr, with $($props:tt)* ) => {
{
let obj = $obj;
set_props_internal!(obj; $($props)*);
obj
}
};
}
macro_rules! set_props_internal {
( $obj:ident; $key:literal -> $val:expr, $($rest:tt)* ) => {
set_property(&$obj, $key, $val);
set_props_internal!($obj; $($rest)*);
};
( $obj:ident; $key:literal -> $val:expr ) => {
set_property(&$obj, $key, $val);
};
() => {};
}
impl IntoJs for &str {
fn into_js(&self) -> JsValue {
JsValue::from_str(self)
}
}
impl IntoJs for Object {
fn into_js(&self) -> JsValue {
JsValue::from(self)
}
}
impl IntoJs for JsValue {
fn into_js(&self) -> JsValue {
self.clone()
}
}
macro_rules! impl_into_js_num {
( $($t:ty)* ) => {
$(
impl IntoJs for $t {
fn into_js(&self) -> JsValue {
JsValue::from_f64(*self as f64)
}
}
)*
}
}
impl_into_js_num!(u8 i8 u16 i16 u32 i32 u64 i64 u128 i128 f32 f64); |
@Pauan yeah, absolutely. The initial ideas I have around @fitzgen very good points, let me address each one.
@Aloso that's very cool! That approach with a macro would probably also provide a cleaner way to address @fitzgen's question regarding support for let my_obj = set_props! { Object::new(), with
"foo" -> "bar",
Symbol("spaghetti") -> "potato",
42 -> "meaning",
}; I'm also curious to see what others think about macros vs. a more vanilla API comprised of functions. For me personally, I don't reach for macros unless they allow me to express something that is otherwise very difficult or verbose to express in normal Rust syntax. I don't think we need all those variants of fn int<T: Into<i64>>(val: T) -> JsValue {
JsValue::from_f64(val.into() as f64)
}
fn uint<T: Into<u64>>(val: T) -> JsValue {
JsValue::from_f64(val.into() as f64)
}
fn float<T: Into<f64>>(val: T) -> JsValue {
JsValue::from_f64(val.into())
}
// These all work:
uint(123u8);
uint(321u16);
int(666i32);
float(3.14159f32); |
@Aloso another great point you had is being able to augment properties onto existing Objects. If we opted to not go for the macro approach and stick with the existing let existing_obj = ObjectBuilder::new().build();
let same_obj = ObjectBuilder::from(existing_obj).string("foo", "bar").build(); |
Yes. |
I think there's a pretty simple solution: the #[inline]
fn unwrap_build(self) -> Object {
self.build().unwrap_throw()
}
Rust macros can do pretty much anything, since they just operate on a token stream. There's a few limitations, which you can read about here. For the most part I don't worry about it: I just make whatever syntax seems reasonable, and then afterwards fix up any compile errors that Rust gives me.
For performance reasons, it's probably best to handle each type separately (rather than using |
Are you getting The only edge-case I can think of is if one tried to add some properties onto an existing Object that was frozen/sealed, or had custom setters for property names you were trying to set. Those cases might be enough justification to not support feeding a pre-existing Objects into
Ah yes good point. I mean in that case we can lower it down to
Hmm, do you have more background on this concern? I'm not sure that widening an int is a particularly expensive operation, and it has to happen anyway since the only number primitive that Javascript supports is f64 anyway right? |
Note that this will be fixed and is tracked separately in rustwasm/wasm-bindgen#1258. I'm currently working on an [for now internal] solution for it, got it to the feature-complete stage, and now investigating performance. So far it looks like, even with pre-encoding object keys and so on, on some inputs it can be much slower than serde-json due to the overhead of frequent crossing of boundary and encoding lots of small strings as opposed to a single large one, but on other inputs involving more numbers than strings it can be much faster. But yes, hopefully:
Either way, I would bet on that existing serde integration rather than try and introduce yet another API for objects. |
Thanks for the feedback @RReverser - what you're working on sounds very interesting and I'll be sure to check it out when you put some code up
You might be right, although your feedback only touched on one of the points I raised. What are your thoughts on these other situations?
|
This is also covered by proper serde integration - it has dedicated methods for serializing / deserializing bytes (usually via https://github.com/serde-rs/bytes) and I'm already leveraging that for creating and passing back
This one seems pretty rare usecase and indeed harder to solve... I guess it could be possible to implement that somehow with |
Of course performance is an important aspect here, but when I think about this more I think the main purpose of The question is whether the situation I describe is considered common enough to warrant the API surface area in Gloo. I would make the case that it is - which is why I raised this RFC :). i.e if you're surgically inserting Rust WASM into parts of an existing codebase, you'll be dealing with all sorts of ad-hoc objects everywhere. needing to define Concrete example that I'm dealing with right now in my own project: passing messages from a Web Worker written in Rust. There's a bunch of messages that get exchanged between the main UI and the Rust Web Worker. Each message is different but only shows up once in the message handling code, at which point I scoop all the relevant data out and call into proper Rust methods. Using Serde or duck typed interfaces looks like this: struct MessageA {
blah: String
}
struct MessageB {
foo: u32
}
//..
struct MessageZ {
bar: bool,
}
fn send_message_a() {
let msg = MessageA{blah: "blorp"};
worker().postmessage(&JsValue::from_serde(msg));
}
fn send_message_b() {} // and so on Whereas with the proposed fn send_message_a() {
let msg = ObjectBuilder::new().string("blah", "blorp").build();
worker().postmessage(&msg);
}
fn send_message_b() {} // and so on |
I have a habit of being kinda verbose when I'm trying to get a point across. Let me try and distill it into a much easier TL;DR: I think that requiring users to crystallize every ad-hoc Object they need to build for interop between Rust and JS into a I'd like to know what others think! |
I see. It sounds like your main concern is the boilerplate associated with defining custom types for one-off objects, even if objects themselves are still static. If so, you should be able to hide implementation details in a macro like: macro_rules! object {
($($prop:ident: $value:expr),* $(,)*) => {{
#[derive(Serialize)]
pub struct Object<$($prop,)*> {
$($prop: $prop,)*
}
JsValue::from_serde(&Object {
$($prop: $value,)*
})
}};
} and then reuse it wherever you need ad-hoc objects like fn send_message_a() {
let msg = object! { blah: "blorp" };
worker().postmessage(&msg);
} This way you should get the best of both worlds (static serialization + no need to write out types) with relatively low efforts. What do you think? |
All Rust integers fit into a
I thought that objects such as ArrayBuffers can be passed between JS and Rust, without (de)serializing at all? I understand that strings need to be converted (UTF-8 <=> UTF-16), but not ArrayBuffers. So I'm guessing that (de)serializing an object containing a large ArrayBuffer is always slower than using Also, how does the (de)serialization work if the JsValue contains cycles?
This looks really nice. The only missing feature is numeric and symbol properties, like object! {
"foo" : bar,
4 : true,
Symbol("hello") : "world",
} And I still think that a macro (or similar API) would be handy to add or modify values of an existing JsValue, even if that requires you to call extend! { div, with
"id": "my-div",
"className": "fancy border red",
}.unwrap_throw(); |
Unfortunately not, you still need to copy the memory from JS to WASM or the other way around.
By default it doesn't, but then, it's not particularly easy to create cycles in Rust structures anyway (even with
I don't think these are common cases (you pretty much never should create an object with numeric properties), but they shouldn't be hard to add where necessary.
Any reason you can't use Object::assign(div, object! {
id: "my-div",
className: "fancy border red"
}) |
Not strictly true, see
Yeah, @Aloso suggested a macro also, and it looks similar to your suggestion. The difference is you're proposing to build a struct as part of that macro. My question is, are you suggesting that such a macro is something people should just be using themselves when needed, or are you suggesting that it's worth codifying here in Gloo? If the former, then yeah I think we can just find a nice place in the documentation to wedge that snippet and call it a day. If the latter, then we can keep iterating on your suggestion. Instead of using |
It can't be done (safely) as part of any object serialisation, because literally the next property can cause an allocation that will invalidate the view. It's meant to be used only as a temporary short-lived reference, which makes it suitable for copying data to JS.
I don't have particularly strong opinion. In my personal experience, arbitrary objects like these are pretty infrequent compared to repetitive typed objects, so it doesn't feel like something that belongs in the core yet, but maybe could be shared as a separate crate (especially since, as you noted, there are various potential implementations which could go into different crates). But, if most people disagree, I defer.
AFAIK these are not necessary and are the default these days, but otherwise yeah, that's a good idea too.
As mentioned above, this is still highly unsafe and likely won't work here. |
Okay so I wrote an entirely unscientific set of benchmarks to test the overhead of building a simple lib.rsuse js_sys::*;
use serde::Serialize;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
struct ObjectBuilder {
obj: Object,
}
impl ObjectBuilder {
fn new() -> ObjectBuilder {
let obj = Object::new();
ObjectBuilder { obj }
}
fn string(self, prop: &str, val: &str) -> Self {
Reflect::set(&self.obj, &JsValue::from_str(prop), &JsValue::from_str(val)).unwrap_throw();
self
}
fn build(self) -> JsValue {
self.obj.unchecked_into()
}
}
// Called by our JS entry point to run the example.
#[wasm_bindgen]
pub fn objectbuilder(array: &Array) {
for _ in 0..1000 {
array.push(&ObjectBuilder::new().string("foo", "bar").build());
}
}
#[wasm_bindgen]
pub fn noop(array: &Array) {
if array.length() != 0 {
panic!("preventing optimizations, I think? {:?}", array);
}
}
#[wasm_bindgen]
pub fn empty_obj(array: &Array) {
for _ in 0..1000 {
array.push(&Object::new());
}
}
#[wasm_bindgen]
pub fn serde(array: &Array) {
#[derive(Serialize)]
struct SerdeObject<'a> {
foo: &'a str,
}
for _ in 0..1000 {
let obj = SerdeObject { foo: "bar" };
array.push(&JsValue::from_serde(&obj).unwrap());
}
}
#[wasm_bindgen]
pub fn ducky(array: &Array) {
#[wasm_bindgen]
extern "C" {
type DuckObject;
#[wasm_bindgen(method, setter, structural)]
fn set_foo(this: &DuckObject, val: &str);
}
for _ in 0..1000 {
let obj = Object::new().unchecked_into::<DuckObject>();
obj.set_foo("bar");
array.push(&obj);
}
} index.jsfunction verifyResult(array) {
if (array.length !== 1000) {
throw new Error(`Invalid length: ${array.length}`);
}
for (const item of array) {
if (item.foo !== 'bar') {
throw new Error(`Bad item: ${JSON.stringify(item)}`);
}
}
}
function runTest(label, fn, skipVerify) {
const results = [];
let array = new Array(1000);
for (let i = 0; i < 100; i++) {
array.length = 0;
let start = performance.now();
fn(array);
let end = performance.now();
if (!skipVerify) {
verifyResult(array);
}
results.push(end-start);
}
return {
test: label,
avg: results.reduce((acc, val) => acc + val, 0) / results.length,
worst: Math.max.apply(Math, results),
best: Math.min.apply(Math, results),
};
}
import("../crate/pkg").then(module => {
const results = [
runTest('js', array => {
for (let i = 0; i < 1000; i++) {
const obj = new Object();
obj.foo = "bar";
array.push(obj);
}
}),
runTest('rustland: noop', module.noop, true),
runTest('rustland: empty_obj', module.empty_obj, true),
runTest('rustland: objectbuilder', module.objectbuilder),
runTest('rustland: serde', module.serde),
runTest('rustland: ducky', module.ducky),
];
console.table(results);
}); If you spot any glaring errors then please point them out. Even though this is a very coarse test, it's pretty revealing, I think. The results I get in Chrome look like this:
There's a lot of variance between runs because I have no idea how to write a good micro benchmark for the browser, but the variance isn't so much that is takes away from the general scale of difference between the classes of tests. Anyway, using a duck typed interface appears to be the clear choice when building small objects. ObjectBuilder dispatching through My conclusions from this are:
|
Okay slight addendum. I knew something felt a little weird about the results I saw seeing. I was compiling the crate in development mode. Heh. Results in releasemode:
So Serde is nowhere near as slow as I was stating in my previous comment. It's still (currently?) slower than duck-type interfaces though. And I still stand by my conclusion that right now, there's a large amount of overhead in building objects in Rust side, such that it should be avoided where possible. |
The common go-to library is Benchmark.js (that's what I used for years at least, and that's what's been used on JSPerf).
In case you're interested, one of the many small cuts I found and fixed as part of serde-wasm-bindgen work is that in V8
Depends on your type of data. If you have to deal with lots of strings in your objects, then in such benchmarks string encoding/decoding becomes the most expensive part of the profile and there is not much you can do there, unfortunately, either with host bindings or without them. But I agree with your conclusions - if you can define your object statically with setters, it's always going to be faster than serialisation. Would you be willing to adapt the macro to use your approach and publish it on crates.io for others? |
Ah yes of course. In the back of my mind I was thinking "I wish I could just use whatever JSPerf is using" but didn't bother to see if that was easily possible ;) Benchmarking is a fickle science for sure, but I wasn't looking for extreme accuracy here, just a reasonably apples-to-apples comparison of the different approaches. I think what I quickly whipped up does that reasonably well enough.
That's very interesting! And would probably go a long way in explaining the difference between the
No, I don't think so. If it's worth putting in a crate then it's worth putting in Gloo (IMO). But I just don't think it's worth putting in a crate. Building a struct for objects, even if used once, isn't that bad. I do wonder if there's some work that could be done in the documentation here though. Before I made this proposal I didn't know that the duck typing approach could be applied so easily to setting arbitrary object properties. I guess I didn't read the documentation well enough but maybe there's room for some kind of "cookbook" section that walk through common and simple use cases like this? 🤷♂️ |
No, they don't. The largest integer representable by an Above that number, floating points lose precision. For example, Floating points are complicated, but here is a good summary. The Wikipedia article is also rather good. It is simply not possible to fit an And no, silently truncating or rounding or causing other such lossiness isn't okay. That should require explicit instructions from the user, and shouldn't happen automatically.
Host bindings have support for directly passing UTF-8 strings, which (hopefully) will avoid encoding/decoding. |
Ah interesting. Need to see how it's going to become implemented, but sounds promising if they could create strings backed by WASM memory. |
@samcday I've read the entire thread and I don't fully understand how you came to this conclusion. I had an instance today where I needed to create a JSValue (in the form of a JS object) in a code path that wasn't hot. Having a convenience macro would have helped me out. It wasn't terribly inconvenient, but I don't believe the purpose of gloo is to provide "not terrible APIs" - I think web_sys/js_sys mostly already accomplish this. |
@rylev thanks for the feedback! I guess I'm not really that familiar with how the RFC process works just yet. My feeling was that if initial feedback was lukewarm then it's probably not something that should be pursued, especially considering there's approximately a trillion other useful things that could be added to Gloo right now. Have you got some tips on how to judge if/when an RFC should be pushed? I'm happy to keep going with this one if there's enough appetite for it. |
It's probably best to have @fitzgen weigh in here. I'm just of the opinion that creating ad-hoc JavaScript objects is something users may want to do. I can see that argument that as the use of web_sys goes down (because people use the gloo wrappers more often), there might be less of a need, but for quick usage, a simple macro is less boilerplate than creating a wasm-bindgen enabled struct. |
Yeah, for sure, it's one of the first things I bumped into when I started trying to write a Web Worker purely in Rust, which is why I opened the RFC.
SGTM. If there's still appetite I'm happy to rework the RFC to propose a macro approach to easily set properties on Objects. I think the utilities to query data out of Objects would also be quite useful. |
I don't want to set myself up as a BDFL for Gloo — I'd really prefer this is a more collective, community-driven project :) On that note, if y'all think it would be useful to formalize some of the design proposal acceptance rules (for example, potentially make a formal team roster with required number of +1s before considering a design "accepted"), we can spin off a new issue to discuss that. This could help avoid some of the "is this something we want or not? are we making progress here? what are the next steps?"-style limbo. But also, Gloo is a very new project and I hesitate to add more bureaucracy until we're sure we want it. Ok, so here are my actual thoughts on this issue:
|
I don't think that's a good assumption to make: I've been mostly silent because I only mentioned things when it was important to do so, and I'm still slowly digesting all of the various Gloo issues. So I wouldn't treat silence as being rejection. Sometimes it just takes a bit more time for people to read through the issue and think about it! Also, as @fitzgen said, we don't really have a way to "approve" ideas. So unless an idea is explicitly rejected, I think it has a fair chance.
It's not an either-or situation though. People will have different priorities: one person might really need WebSockets, so they push for that. Somebody else might really need the Audio API, so they might push on that... People are not some amalgamous resource that can just be assigned to tasks, people work on what they want to work on, so that means that pushing on one task does not necessarily "take away" resources from a different task. It's not a zero sum game! So rather than worrying about all that stuff, it's best to just focus on use cases: is this feature useful? Where? Why? What is the best way to solve those use cases? If there is a use case for a feature, and that use case isn't already handled by another feature, then I think it's good for us to explore it, and not preemptively shut it down. As such, I'm going to be reopening this issue. |
Okay so meta-level stuff first.
Personally, yeah. For design in particular I think it's better to be a little more explicit about how the process works. I've been "sorta doing" open source for years. And by that I mean I find something interesting and do a drive-by engagement where I drop a PR or two, join a conversation here or there, and so on. It's less often that I just invite myself into a project and start driving the actual design of it. As such I'm not sure how much expectation there is for me to push a proposal forward. Especially considering the Rust ecosystem already has a rather formalized stewardship (legions of talented Mozilla engineers and other corporate sponsorship, established working group committees, etc). Put differently I was just being cautious not to march in and start stepping on other people's toes :)
Good point. I'm more accustomed to working closely in an established team, or working on my own stuff. I forgot that OSS projects often times have a different time-scale for decision making!
I guess what happened was I wasn't 100% confident we need a convenient Object API, and then @RReverser weighed in somewhat opposed to the idea. For me that felt like enough to think I'm probably better off working on something else. Okay now for the issue at hand.
I can continue exploring the non-macro builder style API. My gut feel is that a macro would be better though, given that it's sort of the best of both worlds in this case: it's more expressive and it translates to static dispatches through duck-type interfaces. I'll convert that naive benchmark suite I wrote into something a little more scientific (using Benchmark.js) and expand the patterns a little and see where the numbers end up. If it's not much different to those early results I got (where
Yeah, you're right. It makes sense to return a let val = Reflect::get(&my_obj, &JsValue::from_str("foo")).unwrap_throw().dyn_into::<MyType>().unwrap_throw(); So I think there's definitely value in providing an API that simplifies that example to this: let val = ObjectProps::object::<MyType>(&my_obj, "foo").unwrap_throw(); All we're really doing is just collapsing the two different failure cases into one and making it generic, but that makes the user's code much more readable IMO. |
I'm sorry, I didn't mean to discourage or to look as if my suggestions have anything to do with project decisions! It just currently seemed like there are too many different ways to do this, and not enough clear usecases to choose one, and personally in these cases I feel like it's good to experiment in separate crates outside the core, and only then merge one that is a clear winner in terms of usability for most users. Otherwise it's easy to implement something less useful in core first, and then not be able to easily break backwards compat (this is already a problem with many APIs in Rust). That said, again, I'm not someone making any decisions :) |
No need to apologize! I didn't explain that point very well. What I meant was I proposed the design but wasn't particularly in love with it myself. Having someone else weigh in with good reasons why we might not need it was enough to tip me towards scrapping it altogether. I'm easily swayed like that. Sometimes I even catch myself randomly bleating like a sheep.
Absolutely. Good API design is a balance between spending the right amount of time thinking about how to future-proof something, and never shipping anything at all out of fear that you're drawing a line in the sand in the wrong place :) |
I think we should be clear about what use case we are trying to improve here. Originally, we were talking about building arbitrary JS objects with generic getters, setters, and builders for working with arbitrary properties and arbitrarily typed values. On the other hand, duck-typed interfaces are not for arbitrary JS objects with unconstrained sets of properties whose values can be any type. Instead, they are for when you know that you only care about a static set of properties, and you statically know those properties' types (with The talk of a macro emitting duck-typed interface code is surprising to me, given that my original understanding was that we were trying to address the ergonomics of working with arbitrary JS objects in this issue. These are two very different situations that call for two very different solutions, in my opinion. I think the current ergonomics for both could be improved, but we need to have clarity about what problems we are trying to solve here. |
Yep, I think you nailed it here. The ideas around a macro don't need to be mutually exclusive with something like the originally proposed |
And I got the opposite impression - that string-based property builder was just considered as a way to build arbitrary structures on the JS side, hence the alternative suggestion to make a way to create these structural objects statically, which would work better both in terms of typechecking and speed. That's why I wrote above:
which sounds the same to yours
And that's why I feel that it would be good to experiment outside the core first to identify and address these different usecases in different ways. |
I've completely reworked the proposal. Everyone who previously participated (@Aloso / @Pauan / @fitzgen / @RReverser / @rylev), feel free to take a look. And to anyone else lurking, please don't hesitate to weigh in here!
|
This new proposal is scoped much better, IMO! I think it is a great improvement. I really like that taking
Bike shedding: maybe name this crate "
I think the idiomatic Rust thing here would be to have the methods return a let s = property(&obj, "foo")?
.property("nested")?
.property("real")?
.property("deep")?
.string()?; This would simplify the implementation, and would be more familiar to most Rust users.
This What is the motivation for
Is there a reason to wait to set the properties until finish, rather than doing it as each method is defined? If both there is not strong motivation for the delayed setting, and we want to adopt the // Using an existing object:
Reflect::from(&obj)
.property("foo")?
.set("bar", value)?;
// Create a new object.
let obj = Reflect::new_object()
.set(some_key, 42)?
.set("bool", true)?
.set("string", "cheese")?
.done(); |
Thanks! 🎉
Yeah, I'm really not fussed on the naming.
My thought was that one may not always be able to return a
I mocked up an implementation while designing this API, and I'm not deferring the property sets there (doing them as part of each
I think we need to discuss the fail-fast thing a bit more. Looking at the last example you provided: let obj = Reflect::new_object()
.set(some_key, 42)?
.set("bool", true)?
.set("string", "cheese")?
.done(); Personally I prefer this: let obj = Reflect::new_object()
.set(some_key, 42)
.set("bool", true)
.set("string", "cheese")
.done()?; At least, for the API examples we're talking about here, I don't see the benefit in failing fast. All it does is add extra noise to the builder chain. Internally we can be doing something like this: struct Reflect {
err: Option<Err<JsValue>>,
//...
}
impl Reflect {
fn set(self) -> Self {
if self.err.is_some() {
return self;
}
//...
}
fn done(self) -> Result<T, JsValue> {
if self.err.is_some() {
return self.err.take();
}
//...
} Looking at your example chain again, in the worst case scenario the first set fails, in which case we let a couple more short-circuited calls go through until we finally return the error in the
Oops, it's there now. |
I think this is an intriguing direction, but I'm not 100% sold, for a couple of reasons that my brain is too tired to articulate tonight (and I've already written a veritable of wall of text above). I'll stew on it a bit and play around with it tomorrow. If we can collapse the |
Once try {
Reflect::new_object()
.set(some_key, 42)?
.set("bool", true)?
.set("string", "cheese")?
} And in the meantime you can use a local fn foo() -> Result<(), JsValue> {
Reflect::new_object()
.set(some_key, 42)?
.set("bool", true)?
.set("string", "cheese")?
}
foo() I'm ambivalent about fail-fast vs builder, I think they're both reasonable APIs (and both styles are pervasive in Rust, so neither is "more idiomatic"). Perhaps I lean slightly more toward builder, since it is a little cleaner, and as @samcday said, it gives us a bit more flexibility with controlling when things happen. With regard to the API: I really don't like that Also, I think I have the suspicion that it's possible to unify |
AFAICT, no. At least, with my limited understanding of the Rust type system. The reason log!("Thing? {}", property(&my_obj, "thing").get::<String>(); Which is worse than just having the static non-generic dispatch: log!("Thing? {}", property(&my_obj, "thing").string(); That said, I racked my brains trying to figure out how one can do this generically and came up empty. If someone else has a clear idea of how it can be done, please weigh in, I'd love to learn how to use Rust's type system better. |
That made me chuckle. I spent a solid 5 minutes debating in my head whether to call it
Yeah I'm warming up to the idea. I'll investigate further. |
I'm just going to take this quote out of context so I can further my agenda to keep the builder style :) But seriously though, I think I came up with a better way to verbalize my thoughts on it. doing something like I/O or a sequence of operations that can fail, it makes total sense to have each step return a |
We can use
Not if the type can be inferred (and most of the time it can be). And even in the cases where it can't be inferred, I still prefer the
I've used languages (such as Scheme) which have a philosophy of "name things verbose and precise". Rust is not one of those languages. It takes after C, where contractions and abbrevations are the norm. Hence
I think that's true for building an object, but I don't think it's as true for getting properties from an object (where each individual |
Unless I'm completely lost,
My API proposal already covers returning anything that is a |
I don't really buy into your neat explanation of how things are named in Rust. For starters there's no mention of the balance between precise+verbose and terse+readable in the Rust API naming guidelines. It's also quite arbitrary in the standard library. Yes, you have I'm not gonna lose any sleep over how anything is named, it really doesn't bother me. I just disagree with your assessment that it's clear how something should or shouldn't be named ;)
Sure, we're in complete agreement here, and that's why the proposed API already returns a |
Right, hence my "even if it requires a new trait" comment. Ideally But failing that, we can create a new trait which basically just does that.
Yeah, I know, and I'm saying we should push for that (and rename it to |
Summary
A mid-level API to set and query properties on arbitrary Javascript values.
Motivation
The js-sys crate already provides
Reflect
to get and set properties onJsValue
types, and wasm-bindgen provides Serde integration that allows to easily map an existing Rust type into Javascript land. So why do we need more?The use case we're addressing here is ad-hoc property access. This occurs often when writing Rust WASM code that interfaces with existing userland Javascript APIs. It's expected that as the Rust WASM ecosystem matures, this situation will decline in frequency. However that kind of shift is not going to happen overnight, and in the meantime we should provide better ergonomics for users that need to interface with raw Javascript values.
Detailed Explanation
gloo_properties::property
Used to get/set a single property on an existing
JsValue
.API:
Usage examples:
gloo_properties::setter
Used to quickly build up a set of properties on a value.
API:
Usage:
Drawbacks, Rationale, and Alternatives
Drawbacks
Currently, working with
Reflect
(which this crate would use) is slower than working with duck-typed interfaces, and in many cases is also slower than the Serde integration.Alternatives
The obvious alternatives are to use duck-typed interfaces or the Serde integration. Both of these approaches require one to write and annotate a
struct
with the desired fields. The proposed API is suitable for use when the user does not wish to define explicit types in order to grab properties out of an existing value in an ad-hoc manner.For example:
Unresolved Questions
Currently, none.
The text was updated successfully, but these errors were encountered: