Why can’t you hide public or virtual members in classes via interfaces?

I haven’t seen a rationale for this restriction anywhere. Can someone explain?

In modules, you can hide anything via signatures, in classes you can’t. If you could, “friend” visibility would become more elegant: you could easily expose methods to entities inside a module and hide them from the outside.

Are you sure this is the case? From https://realworldocaml.org/v1/en/html/classes.html#class-types it seems that you can hide class type public members using an interface:

A class type specifies the type of each of the visible parts of the class, including both fields and methods. Just as with module types, you don’t have to give a type for everything; anything you omit will be hidden

It’s in the manual:

In addition to program documentation, class interfaces can be used to constrain the type of a class. Both concrete instance variables and concrete private methods can be hidden by a class type constraint. Public methods and virtual members, however, cannot.

@yawaramin, @gsg: You can also see it in how the manual implements friend visibility: http://caml.inria.fr/pub/docs/manual-ocaml/objectexamples.html#sec40

Namely, it does not hide friend members, it only makes their results abstract.

I’m not sure, but I suspect it is related to how object types are implemented in OCaml through row polymorphism with explicit subtyping. Essentially in the type system there is a mechanism of classes and class types intended to provide support for inheritance, but there are no actual separate types corresponding to class types. Instead, class types are all translated to object types at some point. As a result of this translation, private methods are all removed and are not included in resulting object types. This can be observed:

# class g = object method private u = 0 end;;
class g : object method private u : int end
# let h = new g;;
val h : g = <obj>
# let h : < > = new g;;
val h : <  > = <obj>
# let h : < u : int > = new g;;
Error: This expression has type g but an expression was expected of type
         < u : int >
       The first object type has no method u

Here we see that the type g is just a shorthand for an empty object type < >, all private methods just dropped. So the implementation of support for hiding private methods is neither related to the core type system not to the type inference and related issues, it only needs to be supported during translation of classes into objects and functions. Unlike private methods, public methods are exposed in the corresponding object types. So implementing hiding of public methods would mean that e.g. an object (not class) type < > should be considered included in e.g. object type < a : int > during signature inclusion check. But if I’m not mistaken in OCaml type inclusion is only supported through instantiation. So the type < > should be an instance of type < a : int >, which is not the case since the type <a : int > is not even parametrized and there seems to be even no way to express a notion of any supertype of <a : int> or some supertype of <a : int> to give to the corresponding value to make its type instantiable to the supertype <>. Such conversion is only supported explicitly with coercion annotations and so to hide a public method there should be an explicit coercion somewhere e.g.

module M : sig
  class type pub = object method vis : int end
  val pub : unit -> pub
end = struct
  class type pub = object method vis : int end
  class pri = object method vis = 0 method hid = ~-1 end
  let pri () = new pri
  let pub () = (pri () :> pub) (* Here it is *)
end

Hiding virtual members is clearly problematic, since a virtual member must be defined in order to complete a classes definition. For example you could write:

module M : sig
  class virtual c : object method foo : in end
end = struct
  class virtual c : object (self) virtual method bar : int method foo = self#bar end
end
class d = object inherit M.c end
let _ = (new d)#foo

which would clearly error due to a missing bar method. If you somehow kept track of the existence of the missing methods, then would just have a class that could not being inherited from – at which point you should just expose the class as an object type instead:

module M : sig
  type c = private < foo : int >
end = struct
  class virtual c : object (self) virtual method bar : int method foo = self#bar end
end

For public methods, the main issue is the one outlined by @schrodibear. That a class definition actually consists of three parts: a class definition, a class type definition and a type definition. Type definitions can be used in contravariant positions – which means it is not sound to subtype them. If you could hide the public methods then you could write:

module M : sig
  class c : object end
  val f : c -> int
end = struct
  class c = object method foo = 3 end
  let f c = c#foo
end
let _ = M.f (object end)

which again would error due to a missing foo method.

The code is fundamentally the same as:

module M : sig
  type c = < >
  val f : c -> int
end = struct
  type c = < foo : int >
  let f c = c#foo
end
let _ = M.f (object end)

which perhaps makes things clearer, and also hints towards an approach that would work, because what you can write is:

module M : sig
  type c = private < >
  val f : c -> int
end = struct
  type c = < foo : int >
  let f c = c#foo
end

which hides the existence of the foo method, by declaring the c type to be a subtype of < >, rather than declaring it to be equal to < > as we did in the previous example. So what OCaml could support, but doesn’t, would be code like:

module M : sig
  class c : private object method bar : int end
  val f : c -> int
end = struct
  class c = object method foo = 3  method bar = 4 end
  let f c = c#foo
end

This would declare that M.c was a subtype of object method bar : int end. Compared to just exposing type c = private < bar : int > this version would additionally allow you to inherit from c to override its exposed methods:

module M : sig
  class c : private object method bar : int end
  val f : c -> int
end = struct
  class c = object (self) method foo = self#bar  method bar = 4 end
  let f c = c#foo
end
class d = object
  inherit c
  method bar = 5
end

but you would not be able to extend it with new methods, since you would need to avoid accidentally overriding the hidden methods:

module M : sig
  class c : private object method bar : int end
  val f : c -> int
end = struct
  class c = object (self) method foo = self#bar  method bar = 4 end
  let f c = c#foo
end
class d = object
  inherit c
  method foo = "five"
end

which would leave the object type of d with two foo methods with incompatible types.

4 Likes