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: add an option to use shallow partial #378
Comments
Hey @aikoven ; sorry for not replying sooner, I got behind on ts-proto email/issue notifications and am finally working my way through them. I was thinking exact types would solve your 2nd point, and while it's not yet built into TS, I think there is a sufficiently neat / works-for-us mapped typed that we can start using to finally have typo-free Do you think this would be good enough to alleviate your ask for shallow partial? If so, that's great, if not np, I'd be good with like a |
I also wonder if maybe this could be either a separate method, i.e. Tangentially, I've been wondering about renaming the |
My primary concern was the unneeded deep traversals, like when you already have the complete object (maybe received it from another service), but to pass it further you have to make a deep clone of it. However, the shallow partial has its drawbacks as well, and I still think that the Thanks for your comments, I'll work on a PR when I have some spare time. |
@aikoven cool, sounds good. Just talking out loud, but maybe we could detect "already a full message" and skip the deep traversal, i.e. like add a hidden Granted, lists are a little bit tricky because if you do:
Then we'd at least have to do a Coincidentally the type-registry feature's |
🎉 This issue has been resolved in version 1.92.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
I like the idea of renaming it to something more basic, like
This is an interesting idea, though I see how it could lead to non-obvious and surprising behavior. For example, consider the object destructuring: const {someField, ...rest} = fullProtoObject;
Message.fromPartial(rest) // rest has the $type filed, but it is not "full" |
Yeah, maybe I'll think about doing a breaking change that renames
Ah yeah, I didn't think of that... This is admittedly maybe "too cute", but the check could be:
Where we know So that would catch |
It still sounds kinda fragile to me, though right now I can't think of other cases where it could break in regular code.
There's also an option to make the export interface Message {
// ...
}
const create = (input: DeepPartial<Message>): Message => {
// ...
}
export const Message = Object.assign(create, {
encode, decode, ...
}) Then we can use const message: Message = Message({...}) |
I had another thought, what about using Granted, "just data instead of classes" is/was one of the main goals of ts-proto, so at first that seems kind of sacrilegious (and unless we got really lucky, would surely? be a breaking change). But, after thinking about it, TypeScript's structural typing already let's anyone "implement a class" with "just an object literal" by just matching the class's public interface, as long as there are not any private methods. So, in a way, I think if we flipped ts-proto messages to And, specifically the upshot would be that Three more musings, given your PR #457 :
Are faster than:
Because v8 effectively sees each property assignment as a potentially "class" changing operation (similarly, assigning all fields in a constructor, vs leaving some empty and lazy-assignment them is also "supposed to be better"). Anyway, given you're thinking of performance, I'd been meaning to make the methods The one method that is more tricky is
|
I'm usually opposed to classes, because almost always a problem can be solved using much simpler abstractions (i.e. plain objects). But in case of
This could go to the next major version along with other planned breaking changes, like changing option defaults, maybe cleaning up options.
Since my primary concern is performance of deep traversal, I'll try to assemble a benchmark that compares deep vs shallow.
Yeah, I heard about that too, though I feel like it should not be way too hard to optimize the second variant. Probably the first created object will be non-optimized, but for subsequent ones the v8 has all the info to build a pseudoclass.
In #457 all objects are constructed from a base instance that already has all fields initialized.
Not sure too. Intuitively a class should be more expensive than a plain object because classes is a heavier abstraction. I've got another idea: what if |
I'm not a fan of chaning objects to classes. Coming here from protobuf.js this was one of the best choices we saw here. In theory classes are objects anyways, but in practice the objects make things like equality checks very simple. I also don't see how classes would help here. Adding another variant of the fromPartial method should be straight forward. Ideally Adding a benchmarking framework here would be great. Then it would be possible to isolate and avoid regressions. Personally I don't have performance applications, so having someone pushing this would be great. |
Not an apology to classes, but for example Classes would let us distinguish message instances from other objects using |
I'm more thinking about testing frameworks where you don't really have the choice which equality implementation you want to use if you want to have nice test code like
I see. |
Ah shoot, I was just going to say, we could use prototype assignment w/o going all the way to
And then inside of I'm brainstorming "ways of observing an object is 'already fully-instantiated". We could:
I kinda like the But the weak set idea is the least invasive, i.e. it doesn't change the look/shape/protos of messages at all; I think the only downside would be potentially (?) increased GC overhead from the weak set? I really don't know if that'd have any effect on GC or not. Dunno, @aikoven / @webmaster128 wdyt about the weak set approach to short-circuiting deep traversals inside of I suppose we could also provide multiple options...like default to the weak set approach, but if we do actually see performance overhead from that (although maybe we wouldn't, I'm really just speculating), we could let users fall back to the |
Taking one step back, I wonder if this is really something that needs to be implemented here and create code and drawbacks for all other users. The proto
|
Yeah, good to sanity check that, but personally I'm pretty okay with trying to solve this in a general way / directly in ts-proto. My rationale is that I've made probably ~2-3 libraries to "create nested data" (one being an ORM creating backend entities, another for client-side GraphQL types, and also ts-proto's I don't know that I'd really articulated / realized it before now, but all three have this same problem of "ergonomically let the programmer pass 'just literals' / partials (that need defaults applied) or already instantiated 'blessed' objects / entities (that don't)". So, dunno, just given I've seen the same need in a few places, I think it's a pretty common need, and IMO, if we figure out how to do it well, can be a bit ergonomic win/pro for ts-proto users.
Yeah, I agree this can work / be a good approach, but I think can also end up being "every call site wants it's own new / dedicated constructor", i.e. test suites are a great example of where
I think it's less about shallow copies and more about "full messages", i.e. I think what you're creating is: // create a 'full' / 'trusted' message, sets a default `bar` field
const foo = FooMessage.fromPartial({ foo: 1 });
// I change bar to `someOtherBar`, should `foo` be considered "not full / trusted" anymore
foo.bar = someOtherBar; I'd assert this should be fine b/c (Granted, if you passed Back to @aikoven 's original ask, it was actually along your nudge @webmaster128 of "let the client code deal with it", insofar as client code that is currently passing nested levels of an object literal like:
Would have to put
Which, pros/cons:
Which, dunno, I'm admittedly biased b/c of similar approaches in my other ORM and GQL libraries, and ts-proto so far, that "con" of "keep re-typing So that's why when in @aikoven brought up the admittedly simple "just give me a non-deep (Just thinking of the other libraries, the ORM used classes, so could do So, dunno @aikoven this is your ask and likely your PR :-) so wdyt, i.e. it seems like leading options are now a) add a shallow partial and punt on "optimized deep partials" or b) use a weak set to mark ts-proto-created objects? (...or both, and let users choose). |
One of the things that led me to this proposal is this: I rarely create nested objects inline, more often they are created in separate functions. For example, consider this gRPC API: service Users {
rpc GetUsers(GetUsersRequest) returns (GetUsersResponse);
}
message User {
string id = 1;
string name = 2;
}
message GetUsersRequest {};
message GetUsersResponse {
repeated User users = 1;
}; Now imagine that we're implementing this API. Our const usersImpl = {
async getUsers(request: GetUsersRequest): Promise<GetUsersResponse> {
const usersFromDb = await getUsersFromDb();
return GetUsersResponse.fromPartial({
users: usersFromDb.map(item => ({...}))
});
}
} For now it's fine to do the conversion inline, and have the mapper function message GetUserByIdResponse {
User user = 1;
} The implementation of this new method requires the same mapper function, so we need to extract it and reuse: function convertUserToProto(userFromDb: UserFromDb): User It feels natural to return the full Theoretically it is fine to just always have "outgoing" objects be
The const nested = Nested.fromPartial({...})
const msg = Msg.fromPartial({nested});
msg.nested.field = ''; Currently, this code won't mutate the Also, it only helps with objects, but not with nested arrays: we still have to iterate over them. |
Deep partial is a convenient way to create objects, but it has some drawbacks:
It requires traversing the whole object tree, which may hurt performance e.g. for large arrays.
Sometimes you already have the complete (non-partial) object and want to put it to a field of a parent object created from partial. In this case, the child object is deep-cloned without need:
It makes type checks weaker. For example, given Protobuf message:
Create the
Response
object from deep partial:To fix it, we have to specify the return type:
My proposal is to add an option
useShallowPartial
, that when set totrue
makesfromPartial
methods accept the built-inPartial
instead ofDeepPartial
.The text was updated successfully, but these errors were encountered: