-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Collection records #407
Comments
I think you may be mis-interpretting the initial value for the Record class. It is intentional that you're unable to specify what type each field is because type checking will quickly expand into a very large (and possibly expensive) extension to this codebase in a really unrelated matter. What the object provided to Record describes is the list of valid keys on that Record and the default values for each key, not their type. For example: to articulate the difference between const Completer = Record({
selected: -1 // Number
entries: List() // List<Suggestion>
}) or const Completer = Record({
selected: -1 // Number
entries: null // ?List<Suggestion>
}) Does this make sense? |
I'm very interested in layering type information atop Record, I just don't think this library is the right place for this to live. I see a few ways forward:
|
I now understand from your pull-requests that you're interested not just in type validation but parsing an arbitrary JS body into a tree of Immutable collections. That's also super interesting and something I would love to see, but probably should not be specific to Record, but apply to all Immutable collections. Something like that would probably have a different sort of API from the Immutable collection constructors. For example, as you discuss here, you can't simply write: |
Indeed! I'm not actually interested in adding a type checking to the records, what I'm interested in is to allow polymorphic record transformation functions that don't need to be a record type specific.
I'm not sure I am following what you're trying to say here. Could you maybe explain it little differently ? Also would you mind proposing what the API for that could look like ? |
To put it from a different perspective I want to get following:
|
As of runtime costs I agree this is not free lunch, but I don't think in practice it is going to be different from a normal records use as is. Let me elaborate on this:
So unless I'm missing something only added runtime cost is |
I will understand if you'll say this is out of scope for this library and maybe defining whole new library would be a better way to go about it. Although I would still appreciate feedback from you on some of the points I tried to make above, as I could be missing some details. |
Putting a typecheker part of this aside, with this change const Point = Record({x: Record.Number(0),
y: Record.Number(0)}) That would add optional schema validation when desired. In fact I wanted to propose React like props validation as well: const Email = Record.Field(value =>
emailRegExp.test(value) ? value : TypeError('Invalid email address')
const Login = Record({
user: Email('user@gmail.com'),
...
}) |
BTW implementing this as third party library is not quite visible right now because:
If you'll be more in favour of having a separate library (which I'll understand) maybe we could at least make Immutable Types more subclassing friendly so one would not need to maintain a fork of |
@tgriesser thanks for pointing that out, indeed that would greatly simplify this pull request or allow defining this as a separate library. Any ideas when is it going to land ? |
@leebyron mentioned there's a better benchmarking system in the works internally so once that's available we can have more certainty there aren't any regressions in terms of performance across the board, though with the current There's one (minor) caveat, if you try to add something like a metadata API to an object, or any other case where empty state is signaled by more than just value equality of the Map, List, etc., calling something like I believe I have a good fix for this but it modifies even more of the internals and didn't want to go any further in the PR until there's more discussion. |
Thanks for all the detailed input @Gozala.
This is not possible in all cases because converting to JSON is lossy. In some simplified common cases it's possible though. I understand your overall point of wanting to convert from JSON in a deep way. This points to a need for a serialization proposal which I'm interested in, but is maybe out of scope for this task.
This is true, and @tgriesser has good ideas on how to improve this. However, I'm not sure that I'm convinced a subclass of List is necessary in order to properly parse JSON to Records and Lists. I understand you want this to be able to override the set/merge/etc methods, but I'm not sure this is strictly necessary.
const Point = Record({x: Record.Number(0),
y: Record.Number(0)}) Something along these lines is more interesting to me. I think we can probably get closer to the syntax of your original proposal by differentiating when providing a Record constructor instead of a Record value in the default argument position. For example: const Point = Record({ x: 0, y: 0 });
const Line = Record({ start: Point, end: Point }); In this case, the This would preclude the ability to define a record constructor as the default value for a Record field, but that use case is probably very rare. I cannot think of a case where that's what you would expect. My one real concern with an API like this is that it may be confusing to see an intermixing of default values and types. I foresee misreading examples like this and mistakenly writing: const Point = Record({ x: number, y: number }); which is a reference error, or: const Point = Record({ x: Number, y: Number }); Which simply wouldn't do what you expect (Number function as a default value). We can chip away at these cases, but it still makes me nervous. The remaining case is for Lists of a particular type, but by extension also Rough and dirty sketches of an API might look like: points: Record.Type(List, Point)
points: Record.Type(js => List(Seq(js).map(Point))
points: Record.Type(List, item => Point(item)) Not sure, exactly. This needs more exploration. |
For the first part of this: nested Records, I have a diff pushed to the 4.0 branch here: 5001072. This is based on your pull request, I actually borrowed the tests directly. I've made some generalizations to hopefully make this even more useful - not being limited to only Records as the type factories. This made using Number, Boolean, and String possible to use directly which provides some of the runtime type safety we were interested in without being a wart. It also let me add a The 4.0 branch was set up to start tracking changes like these that are going to be breaking. There's no set target date for launching 4.0, but having the space will allow for some exploration. Consider it an alpha branch. |
Could you please elaborate, example of where it's not going to work ? I understand that conversion to JSON is lossy, but (with proposed changes)
Parsing from serialised JSON is just one part of it. The other that I think you missed is ability to have a polymorphic functions that operate on the different RecordTypes. To give you more insight here is how I got to where I am now with this.
At the end of the day yes you can use
I hope lengthy story above explained why I feel this is necessary. |
Inference in the pull was pretty much doing that, it was looking at the |
Great thanks a lot! |
Your rationale is sound here. I totally agree that having a clean way to parse JSON into Immutable collections is really valuable. The trick is getting the semantics and API right so it doesn't preclude other valuable use cases and the API leads you to what you might expect. Hopefully the diff I pushed to 4.0 branch is on the right point of that scale, though it doesn't tackle non-Record types yet. I'd like to brainstorm a bit more on the appropriate API and architecture to enable this kind of factory function on input data across the suite of collections.
There are a ton of small edge cases where information is lost, but there are also larger representation issues. Certainly having a detailed tree of type information that mirrors a JSON body helps enormously, but the toJS() form on Maps produces an Object, but a serialization API would probably want to produce and consume a list of tuples as JS Object keys can only represent strings and their ordering semantics are awkward at best and ill-defined at worst. |
@leebyron I have few more questions:
Thanks! |
The need for a List that contains Records is pretty clear. The mechanism for doing so, less clear. I'd like to avoid subclassing if at all possible. I have some early thoughts, but nothing implementable yet.
I think I need more time to stew on possible ways to implement this that have the properties:
If you have thoughts on how to accomplish this, I'm happy to keep spit-balling.
I think fork and patch is probably going to work for you. You can unlock a working but maybe not final API, and by the time we agree on something that works best, it will probably be quite easy for you to code-mod your codebase to fit the new API. Building up a separate library sounds like a lot of work given that I really like the idea and want to see it happen here eventually. |
As long as you keep typed fields optional you can have 0 overhead in case of untyped records by using different
Assuming current implementation is considered highly performant, I'd say this requirement is same as one above it, isn't it ?
I'm not sure this a valid requirement. Each data structure comes with it's own pros and cons and if some data structures are not meant to be used with typed records I don't think there is anything wrong with that.
I have being meaning to take a stab at prototyping extensible records after elm's implementation that is based of this paper http://research.microsoft.com/pubs/65409/scopedlabels.pdf I wonder it could provide relevant input in this context, I'll need to re-read it. Without digging to much into library my guess would be that if you want to allow optional typing for all structures likely you'd need a parse function for the type construction that will be involved in leaf node creations for that structure. I'd also presume that you can have typed & untyped version of leaf node creations so that untyped versions won't get any performance overhead. |
Lately I have being thinking maybe it's best not to try changing a |
@leebyron I want to bring up a problem I've run into while making / using typed list that I can't see a solution for. The use case example is something along these lines: const Point = Record({ x: Number(0), y: Number(0) }, 'Point')
const PointList = List(Point, 'Points')
const graph = PointList(data)
const xs = graph.map(({x}) => x) This code is doomed to have one of the following problems:
I've being thinking that it may in fact be better to not do the typed list or records as I originally was proposing. Maybe indeed it would be the best to only do a type parsing during construction and leave it up to user to push / unshift / set right types during updates. Although I still think it would be nice to provide some feedback if a wrong data structures are inserted during update. |
Yeah, I see what you mean here. I'm glad you dug into this and figured out the boundaries. I think I agree that this is pretty dooming for typed lists. However typed records still seems valuable, perhaps there's a way to leverage "list" being part of a record type. Typed records won't have the same issue of the mapping functions desiring to produce different shaped records, so I'm not sure the same pitfall exists there. For example maybe very roughly like: var MyRecord = Record({
listOfFoo: values => List(values).map(Foo)
}); |
That is how I got to typed lists actually as you do need to have this special lists that are part of the record but can be operated just as same if passed to you without owner record. That being said I think I end up with pretty reasonable solution see next post. |
More updates on this front:
I do intend to add few more data structures |
Thank you for reporting this issue and appreciate your patience. We've notified the core team for an update on this issue. We're looking for a response within the next 30 days or the issue may be closed. |
Closing this aging issue - We're very often relying on Flow and Typescript for this kind of typing now, rather than runtime coercion. Though these ideas are still interesting for this and other future companion libraries. |
Hi @leebyron, you seem to say the problem is solved when relying on Flow, but I'm using Flow and Immutable JS v4.0.0-rc.5 and I still have the same problem of not being able to inflate a Record with a Map inside it from a json I get from a server. Could you point me to any example where nested immutable records/maps/lists can be instantiated in one operation using Flow to describe the schema? |
There is no such method at this time |
I just found a solution like this for
It works but writes annoying, any better solution or example ? :D |
I've being using Records for modelling application state and soon enough run into a case where I'd like to specify a record field that must be a collection of other Records. Something along these lines:
Here I'm unable to specify that
Completer
entries should be aSuggestion
records. I would love if there was a way to declare that somewhat along these lines:The text was updated successfully, but these errors were encountered: