-
-
Save raichoo/b5d2534c18eadbf9da8b to your computer and use it in GitHub Desktop.
// Whenever I complain about == in JS people always tell me | |
// that I should use ===, as if that would make more sense. | |
// the `sel` function takes three values of type A | |
// we know nothing about, therefore we should not | |
// be able to compute anything with these values, | |
// the only thing we should be able to do is | |
// to constantly return one of them, their | |
// value should not be able to influence the | |
// result in any way. | |
function sel<A>(a: A, b: A, c: A): A { | |
// let's not look at the implementation of | |
// `sel` right now and bask in the guarantees | |
// given by its type signature. SPOILER ALERT: | |
// this will bite us. | |
return sel_impl(a, b, c); | |
} | |
// lets run some tests and find out how our function | |
// behaves. | |
// this returns the first argument | |
console.log(sel(666, 777, 888)); | |
// this returns the first argument | |
console.log(sel("foobar", "quux", "bla")); | |
// this returns the first argument | |
console.log(sel(false, true, true)); | |
// OK, looks like our function always returns the | |
// first argument. Why should it do anyting else? | |
// The type of `sel` certainly does not give any clue | |
// about possible operations that we can do on values | |
// of A. | |
// what the heck… this returns the third argument… | |
console.log(sel(false, false, true)); | |
// OK, let's take a look at the implementation of `sel`. | |
function sel_impl<A>(a: A, b: A, c: A): A { | |
// making the assumption that | |
// we can compare `a` and `b` | |
// in general makes little sense | |
// there are values which we cannot | |
// compare in JS in a meaningful way | |
// e.g. functions. | |
// Anyway, being able to do this breaks | |
// parametricity. | |
if (a === b) | |
return c; | |
else | |
return a; | |
} | |
// Now that we've said it, let's compare values | |
// where JS has no clue to compare them. | |
// Ah, the trusty `id` function. | |
function id<A>(a: A): A { | |
return a; | |
} | |
function dummy<A>(a: A) { | |
throw new Error("I'm just here as a dummy"); | |
} | |
// let's compare `id` with itself. | |
// Impressive, it seems to be able to compare functions, | |
// since this return `dummy`. | |
console.log(sel(id, id, dummy)); | |
// So this should do the same then. | |
// No… it doesn't… it returns the first argument. | |
console.log(sel((x) => x, (x) => x, dummy)); | |
// So, what === does is that it compares if `a` and | |
// `b` are stored at the same address… think about how | |
// often you want THAT kind of behavior instead of | |
// comparing the actual values? | |
// this behavoir becomes especially funny with arrays. | |
// nope these empty arrays are stored at different addresses, | |
// and are therefore not equal, therefore it returns the | |
// first argument. | |
console.log(sel([], [], [1,2,3])); | |
// so this should behave the same way with strings. | |
// No, it doesn't. this returns "bar". So with | |
// string this is a special case… gah. | |
console.log(sel("foo", "foo", "bar")); | |
// and no this does not happen because those two | |
// constants are stored at the same address. Let's | |
// just compute "foo" so we don't have a constant | |
// here. And it's "bar" again. | |
console.log(sel("f"+"oo", "foo", "bar")); | |
// programming languages really managed to pervert | |
// our understanding of what comparing values means. | |
// Happy bugfixing, y'all. |
Why is it then valid code to compare "non-primitive"-values with ===
? Why not refute that Code with a Parse-Error (or Type-Error or similar)?
Every code passing the compiler will sooner or later end up in production..
The Problem with sel
is that it does not have to state the Eq
-Constraint in the Type. So you cannot rely on the Types to infer the behaviour of the function - quite the opposite. The function does things on A
it is not supposed to do (here: using equality).
===
is an identity operator, not an equality operator. In JavaScript, strings and numbers are passed by value, while other types are passed by reference.
These two pieces of information together explain every single oddity observed in the post.
This post is primarily mourning the loss of parametricity not the behavior of ===
nor the difference between comparing for equality or identity.
@lewisje thanks for the very detailed comment. And for the record, I just had a flashback to the good ol' times, when this Java snippet would return false
😄
package com.company;
public class Main {
public static void main(String[] args) {
System.out.println("foo" == "foo");
}
}
This is a convoluted way to discuss the oddities of the JavaScript type system, in which, unlike some languages, strings are primitive values, arrays are non-primitive values, functions are values (also non-primitive), and there's no reliable way to compare two non-primitive values (a.k.a. "objects," another oddity being that there's only one non-primitive type) by value; something you didn't cover is that regexes are also objects, and therefore if you use the same regex literal twice, you create two separate regexes that are not equal.
That last example is the same as the second-to-last one, because the arguments passed into a function are evaluated before execution, so inside the
sel
function, the first argument will be the value"foo"
rather than the expression"f"+"oo"
.To make it more concrete, the following statements will all log
false
to the console (all examples ES3-friendly):Those last two are oddities about IEEE-754 binary floating point arithmetic, which JavaScript uses for most numeric operations (specifically, 64-bit arithmetic);
Object.is
was added to ECMAScript 6 to deal with them, but not the rest.Below, I get into an extended explanation of how to compare objects by value.
Also, with a JSON implementation, you can compare by value two arrays that contain only arrays or primitive values (other than symbols or
undefined
), don't have any instance properties other thanlength
and the numeric properties, and are not cyclic (a.k.a. "plain arrays"), by comparing the outputs ofJSON.stringify
directly.If you also want to allow plain objects other than arrays (that is, objects with the internal
[[Prototype]]
property equal toObject.prototype
, that have no symbol-keyed instance properties, no instance properties that are accessors as opposed to data properties, and no instance properties other than the aforementioned primitive types, plain arrays, or plain objects, and that are not cyclic), you'll need a sophisticated parser for the output ofJSON.stringify
because objects other than arrays are not guaranteed to be stringified in any particular order.What is meant by "cyclic" is that the directed graph of property accesses (the original object, directed to any properties of the object, then directed to any properties of those properties that were objects or arrays, and so on) has a cycle, that in some sense, it has an object that contains itself, either directly or several layers of property access down; Crockford's JSON library includes a couple optional functions called
decycle
andretrocycle
that respectively turn cycles into specially marked non-cycles for stringification, and turn those specially marked non-cycles back into cyclic objects when parsing back into an object.If you want to compare two regexes by value (like arrays, with no instance properties other than the ones that regexes normally have), use their
toString
methods; this can be used in the optional replacer function that can be passed intoJSON.stringify
, like this:This will, of course, clash with any object that has a string value that looks like
new RegExp(
followed by a regex literal followed by)
, but those cases are rare; the inverse reviver function that could be passed intoJSON.parse
if you ever used this to pass around regexes would use the evileval
:If you wanted to be more sophisticated, you could figure out how to turn the output of a regex's
toString
method into a string suitable for passing into theRegExp
constructor, so if you needed a reviver, you wouldn't need to useeval
.You can do something similar with a function's
toString
method, but it's only safe if it's a non-built-in pure function (built-in functions usually say[native code]
in their string representations, but this only causes problems when comparing functions across browser frames, because otherwise you can't duplicate a built-in function), not relying on the calling context or on variables in external scopes; if you're more sophisticated, you could convert their string representations into some standard form to eliminate differences caused by whitespace or comments, and if you were more sophisticated, you could essentially build your own JS parser to figure out whether a given function is pure, or even whether two pure functions with the same string representation actually do the same things and therefore should be considered value-equal.More simply, you can do this sort of thing with
Date
objects by working with their ISO representations, in UTC; as forError
objects, you can compare the error types viainstanceof
and then compare theirmessage
values.