Search code examples
ocamlequalityreason

How do I compare values for equality by Type Constructor?


Background

I'm a relative newcomer to Reason, and have been pleasantly suprised by how easy it is to compare variants that take parameters:

type t = Header | Int(int) | String(string) | Ints(list(int)) | Strings(list(string)) | Footer;

Comparing different variants is nice and predictable:

/* not equal */
Header == Footer
Int(1) == Footer
Int(1) == Int(2)

/* equal */
Int(1) == Int(1)

This even works for complex types:

/* equal */
Strings(["Hello", "World"]) == Strings(["Hello", "World"])

/* not equal */
Strings(["Hello", "World"]) == Strings(["a", "b"])

Question

Is it possible to compare the Type Constructor only, either through an existing built-in operator/function I've not been able to find, or some other language construct?

let a = String("a");
let b = String("b");

/* not equal */
a == b

/* for sake of argument, I want to consider all `String(_)` equal, but how? */

Solution

  • It is possible by inspecting the internal representation of the values, but I wouldn't recommend doing so as it's rather fragile and I'm not sure what guarantees are made across compiler versions and various back-ends for internals such as these. Instead I'd suggest either writing hand-built functions, or using some ppx to generate the same kind of code you'd write by hand.

    But that's no fun, so all that being said, this should do what you want, using the scarcely documented Obj module:

    let equal_tag = (a: 'a, b: 'a) => {
      let a = Obj.repr(a);
      let b = Obj.repr(b);
    
      switch (Obj.is_block(a), Obj.is_block(b)) {
      | (true, true) => Obj.tag(a) == Obj.tag(b)
      | (false, false) => a == b
      | _ => false
      };
    };
    

    where

    equal_tag(Header, Footer) == false;
    equal_tag(Header, Int(1)) == false;
    equal_tag(String("a"), String("b")) == true;
    equal_tag(Int(0), Int(0)) == true;
    

    To understand how this function works you need to understand how OCaml represents values internally. This is described in the section on Representation of OCaml data types in the OCaml manual's chapter on Interfacing C with OCaml (and already here we see indications that this might not hold for the various JavaScript back-ends, for example, although I believe it does for now at least. I've tested this with BuckleScript/rescript, and js_of_ocaml tends to follow internals closer.)

    Specifically, this section says the following about the representation of variants:

    type t =
      | A             (* First constant constructor -> integer "Val_int(0)" *)
      | B of string   (* First non-constant constructor -> block with tag 0 *)
      | C             (* Second constant constructor -> integer "Val_int(1)" *)
      | D of bool     (* Second non-constant constructor -> block with tag 1 *)
      | E of t * t    (* Third non-constant constructor -> block with tag 2 *)
    

    That is, constructors without a payload are represented directly as integers, while those with payloads are represented as "block"s with tags. Also note that block and non-block tags are independent, so we can't first extract some "universal" tag value from the values that we then compare. Instead we have to check whether they're both blocks or not, and then compare their tags.

    Finally, note that while this function will accept values of any type, it is written only with variants in mind. Comparing values of other types is likely to yield unexpected results. That's another good reason to not use this.