How to improve equality comparison?

I want to revive a topic that has been quite a pain for me from a documentation perspective, and that is the confusing state of == and ===.

My past me wrote up a very concise summary about == vs === in ReScript and discusses some differences to JS: "Equality Comparison" section in the manual · Issue #503 · rescript-association/rescript-lang.org · GitHub

It seems that === does referential comparison which causes issues with complex types like nested variants. So this is definitely a footgun for JS devs that usually use ===, while they don’t have any concepts of nested variants (therefore instinctively using the wrong operator when comparing variants etc).

First of all I should probably write a dedicated doc section on our website about equality comparison, but I also wonder if ReScript could yield a warning when === is used for values that will most likely do the wrong thing when complex values are compared?

3 Likes

Do we have enough information for this already? It would be nice to expand on “this is definitely a footgun for JS devs” by getting a bunch of user experiences. Perhaps we have that already?
Definitely it makes sense to clearly describe what is true about what == and === in ReScript do. Then the more difficult next thing is figuring out what to change about that if anything.

I created this ticket back then due to confusions about equality while working on a production app.

Some other similar complaint was mentioned in another issue (w/ a similar opinion, even though created independently): Deep equal is not the `==` operator in JavaScript · Issue #461 · rescript-association/rescript-lang.org · GitHub

Not a significant amount of reports so far, but maybe some other folks here on the forum can chime in with their opinion here?

I think one point to consider is rescript-react and the dependency arrays. Hooks like useEffect mean shallow diffing which, in turn, means comparing a lot of values by reference. (E.g., back when I was with ahrefs, I remember some of us (me included) being tripped up by the fact that a value created by calling a constructor with an argument can cause infinite rerender when being used as a hook dependency.)

What I mean is, since many, if not the most, ReScript projects are also rescript-react projects, the mismatch between caml_equal and Object.is (which is used by React to compare individual dependencies) can trip people up or at least increase cognitive load.

So—a wild idea, but maybe not so wild given the general direction of ReScript evolution—we could consider ditching (or deprecating) caml_equal and thinking about what kind of equality operators we actually want to have?

Of course, having an operator to compare variants by value would be nice, but:

  1. One way to solve it is defining Module.(===) operators (yes, I know you still probably don’t like the idea)
  2. Again, what about hook dependencies? Could be solved by non-zero runtime wrappers, but you probably don’t like that idea either :see_no_evil:
1 Like

So since React made a design choice that is confusing to users is a good motivation for making that same confusing choice for the entire language, so React users are less surprised?

Apart from the joking style, there’s a question of what fits best with the language, balanced with what people’s expectations+assumptions are. And both ways have tradeoffs, which are not very easy to quantify as we don’t have A/B tests.

What about other frameworks, whether UI or not? If this is prevalent then it’s a stronger signal still.

My 5c is I think we should just document them properly. Maybe we could even have hover information for them in the editor tooling if we don’t already have that.

I don’t see a need for changing them, to me they seem to do reasonable things given how ReScript works. Just a matter of properly documenting and teaching them in my opinion.

It’s different in the React context though, and I agree it’s not perfect. I assume solving it properly would require rolling our own hooks with ReScript friendly equality checks, and that would indeed require runtime.

Not to turn your joke into a strawman, but I want to point out that React devs didn’t come up with the semantics of Object.is (or === in JS), TC39 did. And I think you’re bound to face the difference between those and caml_equal in a lot of places if you target JS. And JS equality is more familiar for people coming from JS/TS than caml_equal, so if anything, it’s the current behaviour of ReScript’s == that can confuse a lot of people.

Anyway, I’m not saying an attempt of comparison by value, be it caml_equal or something else, isn’t useful. My point is, even if we find a way to compare e.g. boxed variants that we like, I think there are some JS libraries out there that rely on comparison by reference, so we should expect mismatch in semantic between our code and the libraries that do any sort of diffing/memoization/caching. E.g., the first memoizing lib I’ve looked at, uses === to compare inputs. So I don’t think this concern is limited to React.

(Also, comparison by reference is just faster.)

Without going into React upsides and downsides, I still think it’s a good match for ReScript (both have roots in FP), it’s officially supported by ReScript, and more importantly, what other options for UI do we have? Full SolidJS support is currently blocked at leas by JSX transform, Vue has several languages inside a single file, Svelte has both that and compiled reactivity, like SolidJS (also, Svelte is for sites, not for apps, so less use case for smth. like ReScript), Angular needs classes and decorators. There’s also rescript-tea, but it’s not very popular or active and I don’t know how scalable it is.

Even if someone creates another UI lib/framework that is a perfect match for ReScript, it’s a long way before it gains enough traction to dethrone React—if it gains traction at all. So I think we’re stuck with React for quite a while.

Thanks, that’s the kind of observation I was after. It’s not reliant on some specific comparison in some hook in some specific UI framework, but a general expectation from JS.

Short of breaking changes, one possibility would be to rename == e.g. structural_equal or something. And use the formatter to format it away to the renamed thing.
So one is not going to use it by accident.

1 Like

There are not only ==, but also <, > It seems natural that this exists as a set and is an advantage for users who want it.

1 Like

Also JS has stage 2 proposal for records & tuples, which use comparison by value

I think it will eventually improve JS users’ awareness of immutability.

How we have referential concern on anything other than ref('a) when we dont have control of references in typical values?