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

Custom Immutable Classes, Round 2 #368

Closed
wants to merge 18 commits into from

Conversation

tgriesser
Copy link
Contributor

In case anyone wants to kick the tires on this a bit, here's another pass at custom classes, actually with really great performance... this is the syntax:

// Note this is Immutable.Classes.Map not Immutable.Map
// This class is not meant to be called directly, but via the factory created below:
class UserClass extends Immutable.Classes.Map {
  fullName() {
    return this.get('first_name') + ' ' + this.get('last_name')
  }
}

// Make the Immutable factory
var User = Immutable.createFactory(UserClass)

// For better debugging, you can also create it with a named function:
// var User = Immutable.createFactory(function Immutable_User() {
//    UserClass.apply(this, arguments)
// }, UserClass)

Now you've got a nice new Immutable "User" type:

var u = User({first_name: 'Joe', last_name: 'User'})
var u2 = u.set('first_name', 'Jane')
u.fullName() // Joe User
u2.fullName() // Jane User
var u3 = u.set('first_name', 'Joe')
u3 === u // true

With the following guarantees:

u instanceof User // true
Immutable.Map.isMap(u) // true
u instanceof Immutable.Classes.Map // true

// false... though Immutable.Map() instanceof Immutable.Map is still true
u instanceof Immutable.Map 

Still a WIP, any feedback is appreciated!

@leebyron sent more details via email

/cc @janmarek, @vkurchatkin, @dandelany

@tgriesser tgriesser mentioned this pull request Mar 5, 2015
@dandelany
Copy link

Cool @tgriesser thanks for the update! I will play with this branch when I have time.

This adds the ability to provide factory functions at each key in a record. Those factory functions will produce values: both default values and any value provided to `set` is first passed into the factory function. You could also think of these as per-field value mappers.

Most importantly, this allows for the ability to define a Record in terms of its types rather than in terms of its default values and use those types to parse incoming JSON (or any other iterable structure) in a deeply nested way.

This also adds the `Nullable` function in the exports. `Nullable` is a factory function transform. It takes a function which accepts a value T and returns a value R (generalizing Record functions taking an object and returning a Record), and returns a new function which accepts a value T|null and returns a value R|null. When you pass null to this function, you get null back. This is useful for the case where you want to type a field, but want the default value to be `null`.

Finally, this allows you to define the fields of a Record in a thunk (inline function). Evaluation of the thunk will be deferred until the creation of the first Record instance, which allows a Record to define circular types.

These concepts come together like so:

```
var Point = Record({ x: Number, y: Number });
var Line = Record({ from: Point, to: Point });
Line(); // Record { "from": Record { "x": 0, "y": 0 }, "to": Record { "x": 0, "y": 0 } }
Line({from:{y:123}}); // Record { "from": Record { "x": 0, "y": 123 }, "to": Record { "x": 0, "y": 0 } }
```

The nullable and lazy thunk definitions come in handy for recursive types. We can use the ES6 Arrow function to define our thunk.

```
var TreeNode = Record(()=>({ left: Nullable(TreeNode), right: Nullable(TreeNode) }));
TreeNode(); // Record { "left": null, "right": null }
TreeNode({left:{right{}}}); // Record { "left": Record { "left": null, "right": Record { "left": null, "right": null } }, "right": null }
```

This change is a breaking change for a couple reasons:

* A function with own properties can no longer be used to define the initial keys. This seems like a very dubious thing to be doing in the first place.

* A function can no longer be stored as a default value in a Record. This also seems like something you should not do, however I'm aware that there are some who use the Record slots as a way to store methods for computed properties and update functions. These use cases will break.

As an example of the second point:

This bad pattern:

```
var MyType = Record({
  x: 3,
  y: 4,
  sum: function() { return this.x + this.y; }
});
var mine = MyType();
mine.sum();
```

Will cease to work after this diff. Instead, the function will be expected to be a value factory. This bad pattern can be turned into a good pattern by making use of the prototype:

```
var MyType = Record({ x: 3, y: 4 });
MyType.prototype.sum = function() { return this.x + this.y };
var mine = MyType();
mine.sum();
```
@kuraga
Copy link

kuraga commented Mar 31, 2015

@tgriesser I has started to use this patch and it's awesome!
How do I change constructor here? I want:

var u = User({first_name: 'Joe', last_name: 'Green'})
// constructor changes arguments, and
// {first_name: 'cool Joe', last_name: 'Mr. Green'}
// will being received by Map

Can I do it? Thanks!

@tgriesser
Copy link
Contributor Author

I suppose you could provide a custom __factory implementation to handle that, but I don't see that as necessarily a good idea.

* master: (42 commits)
  ensure mergeIn family works correctly if deep position is not mergable
  build
  hasIn does not take a notSetValue
  Fix issue where slice of unknown length Seq can throw
  Release 3.7.2
  Update to Patents grant v2
  Fix readme
  restrict to typescript 1.4.x until 1.5 is out of alpha
  Release 3.7.1
  use ES6 naming "includes" - alias "contains" to be non-breaking
  Fix link to type defs
  Release 3.7.0
  merge/union dont work correctly inside withMutations when the mutable is empty.
  better name for constructor
  Allow Record factories to be used as a type cast pass through
  Lazily initialize Record types
  Ensure default name for Record in toString
  Shave another couple seconds off test runtime
  cleanup pre-processor and build dist files from recent pulls
  Update type definitions in dist/ from the type-definitions/ folder
  ...

Conflicts:
	dist/immutable.js
	dist/immutable.min.js
	src/Map.js
	src/Record.js
	src/Set.js
@dashed
Copy link
Contributor

dashed commented May 7, 2015

This looks neat. Hopefully this lands.

@chicoxyzzy
Copy link

I like this too 👍

@tgriesser
Copy link
Contributor Author

Updated this against 4.0, even though apparently GH doesn't let you change the target branch in a pull request.

As fun as it's been to use in my own projects, I'm a bit wary this will open pandora's box a bit, people might start trying to do things with this that they really shouldn't be doing in general.

Wouldn't mind just keeping this as an upstream fork for awhile to see if anyone has other opinions or ideas.

@leebyron
Copy link
Collaborator

leebyron commented May 7, 2015

Yeah - the more I'm thinking about it, the more I'm concerned about the pandora's box that you brought up just now (and before) @tgriesser.

I agree with your conclusion that we should keep floating this as a fork to investigate the compelling use cases.

In previous conversations you compelled me to include a metadata API which I think handles a lot of the cases that subclassing was desired for - and that's still on the docket.

@tgriesser
Copy link
Contributor Author

In previous conversations you compelled me to include a metadata API which I think handles a lot of the cases that subclassing was desired for - and that's still on the docket.

Awesome to hear

@vtambourine
Copy link

In my project on React.js I want to wrap certain data in type classes to perform strict datatype checking using propTypes. For example, to check if property cargo if array of Cargo I want to use:

propTypes: {
    cargos: PropTypes.arrayOf(PropTypes.instanceOf(Cargo))
}

But since all props came from store they must be considered as immutables. So, to typify raw data I intend to use this fork of ImmutableJS:

class Cargo extends Immutable.Classes.Map { }

Can you, please, comment on this pattern. Is it appropriate use of proposed feature or there exist simpler solution?

@leebyron
Copy link
Collaborator

@vtambourine I'm a little confused about your prop-types example. Are you saying that you have arrays of Immutable Maps? Or do you have Immutable Lists of Immutable Maps? Or Records?

Also note that React propTypes are just functions which do or do not return an Error, you can always write your own propType function that will do the custom validation you want.

@vtambourine
Copy link

Yes, you're right about propType custom validation functions. I can use it in my project, but how to handle situations with passing certain props deep into children components? For example, consider following structure:

class CargoList extends React.Component {
    render() {
        return <div>{this.props.cargos.map(cargo => <CargoItem cargo={cargo} />}</div>
    }
}

class CargoItem extends React.Component {
    render() {
        return <div>{this.props.cargo.getName()}</div>
    }
}

Note, that in CargoList component I have property holding array of Cargo-type objects, and in CargoItem component I expect the single Cargo-type property. As far as I understand React approach, first array should be treated as immutable array of immutable object. How to handle props validation on each components?

First method is to have a custom validation function. I can have some kind of AppPropTypes.cargo validator and use it in a way:

CargoList.propTypes = { cargos: React.PropTypes.arrayOf(AppPropTypes.cargo) }
CargoItem.propTypes = { cargo: AppPropTypes.cargo }

But this may be quite verbose. I need to maintain type structure both in creator and validator.

Much plainer might be to validate props by their types using React.PropTypes.instanceOf:

CargoList.propTypes = { cargos: React.PropTypes.arrayOf(React.PropTypes.instanceOf(Cargo)) }
CargoItem.propTypes = { cargo: React.PropTypes.instanceOf(Cargo) }

// somewhere in store
_cargos = update(_cargos, { $push: new Cargo(data) })

Where Cargo extends Immutable Record.

Am I correct in my conclusions or I'm going totally misconcept?

@leebyron
Copy link
Collaborator

leebyron commented Jun 1, 2015

You're correct that if you're using Record, you can use PropTypes.instanceOf(MyRecord) without any issue.

As far as I understand React approach, first array should be treated as immutable array of immutable object. How to handle props validation on each components?

There's no such thing as an immutable array in JavaScript, but you could certainly use an Immutable.js List collection here if you wish. You could use PropTypes.instanceOf(List) as your propTypes for this property, there's no need to test for the contents of the list since your CargoItem propTypes will already be doing this and there's no need to do so twice.

@leebyron leebyron closed this Jun 1, 2015
@leebyron leebyron reopened this Jun 1, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants