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

Fix Immutable-merge for arrays #20

Merged
merged 13 commits into from
Sep 9, 2019
Merged

Conversation

JasonVMo
Copy link
Contributor

@JasonVMo JasonVMo commented Sep 2, 2019

This fixes the immutable merge code to handle arrays correctly for standard handling (which means arrays overwrite) while still allowing them to be processed by custom handlers.

This change addresses Issue #9

Custom handlers now don't have array values filtered by type. This allows merging of arbitrary values with the same keys.

The signature of the handler is now aligned with immutableMerge itself as no one was using the key parameter so it was just dead weight.

@JasonVMo JasonVMo changed the title Merge fix Fix Immutable-merge for arrays Sep 2, 2019
const setToMerge = objs.filter((value: object | undefined) => {
return value && Object.getOwnPropertyNames(value).length > 0;
});
export function immutableMerge(options: IMergeOptions, ...objs: any[]): any {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm still a little confused by the processSingles property on IMergeOptions. Are clients, in this case theming and styling code, supposed to call immutableMerge on a single object if they want to run custom handlers? Maybe makes sense to separate the functionality out into an immutableProcessHandlers or something.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is confusing but convenient. Let's say we want to run this with a custom style handler that creates the css classes and replaces the objects with strings that reference the classes. In the case where it is already a class it should not be changed. The styles in this case are deep in the tree and you want to ensure you make the minimal mutation to the tree.

A helper function isn't a bad idea. It might actually be good to create wrapping functions for common scenarios. I would maybe suggest moving the functions as follow:

immutableMerge() -> immutableMergeCore
immutableMerge(...objs[]) // just do a simple recursive immutable merge
immutableTransfform(handlers: { }, ...objs[]);  // guarantee full processing on the objects

I'm open to suggestions here. The code should be as tight as possible (for bundle size) but usage could be improved.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in latest iteration, feedback welcome.

return value && Object.getOwnPropertyNames(value).length > 0;
});
export function immutableMerge(options: IMergeOptions, ...objs: any[]): any {
const setToMerge = objs.filter(v => _isObject(v, true));
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we plan to eventually add support for top level merging of arrays with a handler being passed in? Something like

immutableArrayMerge(handler: IArrayMerger, targets: any[])

This feels like it may be useful given that styles can be provided as arrays of style objects and that we may want to do merging on before letting the framework (e.g. React Native) process.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This sounds like a good idea. The main concern is having code in a really core bundle that is not being used. We want this as small as possible. It's like I want to publish the equivalent of a header only C++ library, might be worth talking to David about philosophy here.

@PPatBoyd
Copy link
Contributor

PPatBoyd commented Sep 3, 2019

    }

I'm interested in better understanding how undefined values are processed. Am I understanding correctly that:

  1. Any default merges with key->undefinedObject as a result of Object.assign(...), which would only occur when at least one object has said key mapped to undefined and no object has said key mapped to a real value, has said key deleted from the final merge object.

  2. Custom handlers are allowed to merge real values to undefined, resulting in key deletion that wouldn't happen with the default merge -- what's the use case around this behavior?

It appears that Case (1) can be short-circuited and avoid the if (nextOptions) {...} logic, unless custom handlers are allowed to map keys->undefinedObjects to real values? if (2) is also true I understand leaving the key deletion to after, but would still skip the recursing logic if possible


Refers to: packages/immutable-merge/src/Merge.ts:79 in 1d0d6bf. [](commit_id = 1d0d6bf, deletion_comment = False)

@@ -19,7 +19,7 @@ export interface IMergeOptions {
recurse?: { [key: string]: boolean | IMergeRecursionHandler };
}

export type IMergeRecursionHandler = (key: string, options: IMergeOptions, ...objs: (object | undefined)[]) => object | undefined;
export type IMergeRecursionHandler = (options: IMergeOptions, ...objs: any[]) => any;
Copy link
Contributor

Choose a reason for hiding this comment

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

IMergeRecursionHandler [](start = 12, length = 22)

Seems like this could benefit from a type parameter T.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was changed to any to allow custom aggregations of things like styles. In web they might have a signature of something like:

// this is pseudo code just to communicate the idea
type IComplexStyle = string | ICSSStyle;
type IStyle = IComplexStyle | IStyle[];

The any type basically says that you are responsible for checking and handling the types that you are handed.

const setToMerge = objs.filter((value: object | undefined) => {
return value && Object.getOwnPropertyNames(value).length > 0;
});
export function immutableMerge(options: IMergeOptions, ...objs: any[]): any {
Copy link
Contributor

Choose a reason for hiding this comment

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

immutableMerge [](start = 16, length = 14)

What are the use cases where the objects are not of the same type/interface. I suspect all this could be type parameterized.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could type parameterize the easy helper functions. For the core function it doesn't really work in practice because of the deep objects. I found that trying to extend complex typing to core layers has the following problems:

  • it is misleading in a way because the core layers are treating things as objects
  • recursion rules can have unpredictable inferences
  • It causes type errors that are incredibly hard to diagnose at the endpoints. By this I mean that the core code will work but trying to use the types at the endpoints are prone to failure. So you can have two objects that are really the same type will require that one be typecast to be accepted as an input because of differences in the inference rules.
  • This is similar to the approach for the component library. Make it more generic internally and let the types be defined at the endpoints.

I do like the idea of having something like:

function immutableMerge<T extends object>(...objs: T[]): T {
  return immutableMergeCore({ depth: -1 }, ...objs);
}

@JasonVMo JasonVMo merged commit a1ceff0 into microsoft:master Sep 9, 2019
@JasonVMo JasonVMo deleted the merge-fix branch September 9, 2019 22:36
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.

None yet

3 participants