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

Equate non-native functions that have the same code. #12

Merged
merged 1 commit into from
Jul 16, 2020

Conversation

benjamn
Copy link
Owner

@benjamn benjamn commented Jul 16, 2020

Previously, function objects were regarded as equal by @wry/equality only if they were === to each other. This policy was overly restrictive in many common situations, such as when comparing functions that are repeatedly passed as fresh function expressions within objects that are otherwise deeply equal.

Thanks to this PR, the @wry/equality package now considers non-native functions equal if they are === (as before) or if they have the same code according to Function.prototype.toString. For native functions, we must check === because their code is censored, e.g. Object.assign.toString() === "function assign() { [native code] }".

Note that this behavior is not entirely sound, since !== function objects with the same code can behave differently depending on their closure scope. However, any function can behave differently depending on the values of its input arguments (including this) and its calling context (including its closure scope), even though the function object is obviously === to itself; and it is entirely possible for functions that are not === to behave exactly the same under all conceivable circumstances.

Because none of these factors are statically decidable in JavaScript (or any Turing-complete programming language, probably?), JS function equality is simply not well-defined. This ambiguity allows us to consider the best possible heuristic among various imperfect options, and equating non-native functions that have the same code has enormous practical benefits (see apollographql/apollo-client#6588, for example).

I considered making this behavior configurable, until I realized that strict === equality was never an absolute guarantee of behavioral equivalence, anyway. Ambiguities can be liberating!

@benjamn benjamn self-assigned this Jul 16, 2020
@benjamn benjamn force-pushed the equate-same-code-functions branch from ec4a2a8 to bc65262 Compare July 16, 2020 21:56
@benjamn benjamn merged commit 60b7f60 into main Jul 16, 2020
benjamn added a commit to apollographql/apollo-client that referenced this pull request Jul 16, 2020
benjamn added a commit to apollographql/apollo-client that referenced this pull request Jul 20, 2020
@benjamn benjamn deleted the equate-same-code-functions branch October 1, 2020 17:00
benjamn added a commit that referenced this pull request Oct 1, 2020
Previously, an object `a` with a key `k` whose value is undefined (that
is, `a[k] === void 0`) would not be considered deeply equal to another
object `b` that is missing that key, but is otherwise deeply equal to the
first object, even though `a[k] === b[k]`.

This commit makes these two different kinds of undefined-ness equivalent,
by efficiently ignoring undefined-valued keys when comparing objects. This
allows more objects than before to be considered equivalent, but the
equal(a, b) function still obeys the rules of equivalence relations:
reflexivity, symmetry, and transitivity.

This change is similar in spirit to the way we relaxed the strict ===
requirement for function objects in #12: more functions can be considered
equivalent now, but the newly equivalent functions still obey the rules of
equality, and thus the change is logically valid, a decision we are free
to make but not forced to make.

The additional leniency will be useful in situations where (for example)
multiple options objects are combined by application code and then passed
to an API, perhaps using ...spread syntax or Object.assign, and the API
needs to decide if the new options are the same as previous options. This
change allows the application code to avoid worrying about the difference
between undefined and missing object properties, a discipline that can
unnecessarily complicate the way the options need to be combined.
benjamn added a commit that referenced this pull request Oct 1, 2020
Previously, an object `a` with a key `k` whose value is undefined (that
is, `a[k] === void 0`) would not be considered deeply equal to another
object `b` that is missing that key, but is otherwise deeply equal to the
first object, even though `a[k] === b[k]`.

This commit makes these two different kinds of undefined-ness equivalent,
by efficiently ignoring undefined-valued keys when comparing objects. This
allows more objects than before to be considered equivalent, but the
equal(a, b) function still obeys the rules of equivalence relations:
reflexivity, symmetry, and transitivity.

This change is similar in spirit to the way we relaxed the strict ===
requirement for function objects in #12: more functions can be considered
equivalent now, but the newly equivalent functions still obey the rules of
equality, and thus the change is logically valid, a decision we are free
to make but not forced to make.

The additional leniency will be useful in situations where (for example)
multiple options objects are combined by application code and then passed
to an API, perhaps using ...spread syntax or Object.assign, and the API
needs to decide if the new options are the same as previous options. This
change allows the application code to avoid worrying about the difference
between undefined and missing object properties, a discipline that can
unnecessarily complicate the way the options need to be combined.
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

1 participant