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

Introducing Stability and Structural Sharing to Microstates #113

Merged
merged 97 commits into from
May 14, 2018

Conversation

taras
Copy link
Member

@taras taras commented May 12, 2018

Microstates was conceived as a tool to allow developers to write composable data structures that followed the rules of functional programming and immutability. From functional programming perspective, a microstate is the result of a pure function of Type and value.

A Type represents the structure of the data. It describes the relationship between composed types, the operations that can be performed on the composted types, computations that are associated with each type and how the value should be interpreted to create state.

class Address {
  streetNumber = Number;
  street = String;
  city = String;
  country = String;

  get coordinates() {
    return lookupCoordinates({
      street: `${this.streetNumber} ${this.street}`,
      city: this.city,
      country: this.country
    });
  }
}

class Person {
  firstName = String;
  lastName = String;
  address = Address;
 
  get fullName() {
    return this.firstName + ' ' + this.fullName;
  }

  move(address) {
    return this.address.set(address);
  }
}

In this example, Person Type is composed of firstName and lastName which are String type and address which is an Address type. The Address is further composed of streetNumber, street, city and country. When we create a microstate using Microstate.create we tell Microstates to interpret the Type and associate the provided value with composted types.

For example,

let person = Microstate.create(Person, { firstName: 'Taras', lastName: 'Mankovski' } );

Inside of the Microstates object, we represent the Type as a tree structure. The tree consists of information about each type and types that are composed into it. The Person type would be roughly expressed as the following tree.

Tree
  - Type: Person
  - path: []
  - value: { firstName: 'Taras', lastName: 'Mankovski' }
  children:
    - firstName:
      - Type: String;
      - path: ['firstName']
      - value: 'Taras'
      - children: {}
    - lastName:
      - Type: String;
      - path: ['lastName']
      - value: 'Mankovski'
      - children: {}
    - address:
      - Type: Address;
      - path: ['address']
      - value: undefined   
      - children:
        - streetNumber:
          - Type: Number;
          - path; ['address', 'streetNumber' ]
          - value: undefined
          - children: {}
        - street:
          - Type: String;
          - path; ['address', 'street' ]
          - value: undefined
          - children: {}
        - city:
          - Type: String;
          - path; ['address', 'city' ]
          - value: undefined
          - children: {}
        - country:
          - Type: String;
          - path; ['address', 'country' ]
          - value: undefined
          - children: {}

A microstate is an object that is generated from this tree with two kinds of objects: transitions and state. Transitions are closure functions that allow to immutably change the value that was used to create the microstate. State are regular JavaScript objects that are instantiated from their respective ES6 classes.

Microstate object instances are created eagerly but the tree, the transitions and the state on the microstate are created lazily. Very little work is done when a microstate is created. This makes it possible for us to delay building large microstates until the time that the state and transitions on a microstate are needed by the application.

Lazy objects are awesome, but as we started to use Microstates in React applications we found that this was not enough. Our goal for Microstates is to make developer's lives easier. We want Microstates to be a tool that developers trust to lead them to success.

To achieve this goal, we have to make sure that Microstates scales well to whatever level of complexity that developers might require and make it easy to manage this complexity in a performant way. Microstates must make easy what is prohibitively difficult to do on any average project.

We found that the current version of Microstates did not live up to this standard when used with React. React uses exact equality on props to determine if a component needs to re-rendered. Microstate generated state was causing function components to re-render on every transition because microstate's state was not stable.

As I mentioned in the beginning of this PR's description, Microstates was conceived as a pure function of Type and value. A pure function that creates an object, will always return a new object regardless if you passed the same value to the object previously. This does not work well with React.

React uses references to determine if component props have changed. To prevent unnecessary re-renders, props must reference the same object if value used to create the referenced object did not change.

To make Microstates work well with React, we needed Microstates to re-use previously created state objects if the value for the state did not change. This PR introduces structural sharing between trees to allow us to reuse portions of state where value did not change.

This PR introduces internal changes to Microstates architecture to allow structural sharing. No external APIs are changed by this PR.

The biggest architectural change introduced by this PR is giving the Tree a much bigger responsibility. The tree is now the primary internal mechanism for changing the shape of the data that's used to build the microstate. The tree is now responsible for instantiating the microstate. The Microstate.create still accepts Type and value but it delegates to the Tree to construct the tree structure and instantiate the microstate for the Type and value.

The tree structure that is created by the Tree constructor is similar to previous tree but it only has 2 properties: meta and data. All other properties are getters that point to values on one of these objects. The meta object describes the information about this Tree. It has all of the information used to describe how the state and transitions for the microstate of this tree should be built.

data contains all of the data that has to be stable. Stable data is what is carried over if the value for a particular tree is not changed. When you invoke a state transition, the tree will receive a new tree and figure out how that tree should be applied to the current tree. The merged result will be a new Tree and from this tree the next microstate will be created.

This architectural change allows us to control the structural change of the microstate at a more granular level to allow state to be reused. It also allows us to perform more complex transitions by providing a private mechanism to immutably manipulate the tree structure.

I'm excited to see what this new version of Microstates will allow us to do. After this PR, we should be ready to share Microstates with the world. @cowboyd and I are happy about all of this.

@taras taras requested a review from cowboyd May 12, 2018 22:26
@coveralls
Copy link

coveralls commented May 12, 2018

Coverage Status

Coverage increased (+1.8%) to 90.219% when pulling 55fe103 on cl/a-tree-for-all-seasons into 9892229 on master.

Copy link
Member

@cowboyd cowboyd left a comment

Choose a reason for hiding this comment

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

Looks good. I think that I might be missing a few instances of where things are used because of GitHub collapsing the diff.

I think in parallel to updating microstates/react and debugging in React Native, we should begin to explore using Applicative as the primary method of structural sharing.

})

Keys.instance(Array, {
keys(array) { return [...array.keys()] }
Copy link
Member

Choose a reason for hiding this comment

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

Is this to clone the keys? Why not just return array.keys()

Copy link
Member Author

Choose a reason for hiding this comment

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

right...

@@ -0,0 +1,7 @@
// Pull from preact-shallow-compare
// https://github.com/tkh44/preact-shallow-compare/blob/master/src/index.js
export default function shallowDiffers (a, b) {
Copy link
Member

Choose a reason for hiding this comment

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

Are we using this function anywhere?

Copy link
Member Author

Choose a reason for hiding this comment

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

yes, to figure out if the the children changed

import { type } from 'funcadelic';

const Values = type(class Values {
values(holder) {
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 still use this type class?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, in desugar.

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.

4 participants