Skip to content

Instantly share code, notes, and snippets.

@raichoo
Last active February 24, 2016 19:15
Show Gist options
  • Save raichoo/b5d2534c18eadbf9da8b to your computer and use it in GitHub Desktop.
Save raichoo/b5d2534c18eadbf9da8b to your computer and use it in GitHub Desktop.
Staring into the abyss of ===. TypeScript edition.
// 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.
@lewisje
Copy link

lewisje commented Feb 24, 2016

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):

console.log({} === {});
console.log([] === []);
console.log(/(?:)/ === /(?:)/);
console.log(function () {} === function () {});
console.log(NaN === NaN);
console.log(-0 != 0);

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 than length and the numeric properties, and are not cyclic (a.k.a. "plain arrays"), by comparing the outputs of JSON.stringify directly.

If you also want to allow plain objects other than arrays (that is, objects with the internal [[Prototype]] property equal to Object.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 of JSON.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 and retrocycle 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 into JSON.stringify, like this:

function regexReplacer(key, value) {
  if (value instanceof RegExp) {
    return 'new RegExp(' + value.toString() + ')';
  }
  return value;
}

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 into JSON.parse if you ever used this to pass around regexes would use the evil eval:

function regexReviver(key, value) {
  var re, len;
  if (typeof value === 'string' && len = value.length && // intentional assignment in `if` header
      value.indexOf('new RegExp(/') === 0 && value.lastIndexOf(')') === len - 1) {
    try {
      re = eval(value.substring(11, len - 1));
      if (re instanceof RegExp) return re;
    } catch (ex) {} // intentionally empty
  }
  return value;
}

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 the RegExp constructor, so if you needed a reviver, you wouldn't need to use eval.

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 for Error objects, you can compare the error types via instanceof and then compare their message values.

@Drezil
Copy link

Drezil commented Feb 24, 2016

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).

@coreh
Copy link

coreh commented Feb 24, 2016

=== 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.

@raichoo
Copy link
Author

raichoo commented Feb 24, 2016

This post is primarily mourning the loss of parametricity not the behavior of === nor the difference between comparing for equality or identity.

@macedigital
Copy link

@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");
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment