Structural equality for elements of abstract types

I understand that structural equality “=” is defined automatically for most types, but not for function types, or for variant or record types that contain a function. A quick experiment suggests that this is true even if the type is abstract. With

module Foo : sig 
  type t
  val mk : string -> t
end = struct
  type t = Name of string
  let mk str = Name str
end

I can compare two values of type Foo.t for structural equality:

let x = Foo.mk "hi" in
let y = Foo.mk "hi" in
x = y

returns true. But if I implement the same module type with a variant containing a function:

module Bar : sig 
  type t
  val mk : string -> t
end = struct
  type t = Name of string * (unit -> unit)
  let mk str = Name (str, fun x -> x)
end

then I can’t compare two elements of Bar.t for structural equality:

let x = Bar.mk "hi" in
let y = Bar.mk "hi" in
x = y

raises Invalid_argument.

My first question is whether I’m understanding what’s going on correctly. If so, doesn’t this partially violate the abstractness of an abstract type? I would expect to be able to change the definition of a type inside a module without breaking any code that uses it, as long as the observable behavior is identical. But it seems that if my original definition of the type didn’t involve functions, then a user might innocently have been comparing values of that type for structural equality, which would then break if the type definition is changed to involve functions.

1 Like

Structural equality is a “magic” function, it’s all done in the runtime.
It doesn’t know at all about types, let alone private/abstract types
:-).

If you want to make a type t comparable, expose val equal : t -> t -> bool (and you can start with let equal = (=) if you want). This way,
users will not be tempted with evil magic functions.

Well, I had thought that even if it’s implemented by magic at runtime, at compile time the compiler could still check that it’s being applied correctly. But I suppose the function (=) has to be given some type, and without something like typeclasses it’s hard to see what type it could have other than 'a -> 'a -> bool. I do find this somewhat disturbing though.

If the type happens to be comparable by = but I want to force the user to use my equal instead, or make the type not comparable at all, is there any better way than including a “dummy” function argument as in my initial example?

Nothing comes to mind. Some “alternative” standard libraries (such as base) make it impossible to use polymorphic comparison such as = in its scope. Also, there is a PR under discussion upstream https://github.com/ocaml/ocaml/pull/9928 that would allow triggering an error when using polymorphic comparison.

Cheers,
Nicolas