Interface signature vs public/private keywords

I’ve seen two ways of specifying an interface: an interface signature, or public/private keywords.

Examples of the interface signature approach:

  • OCaml: literally a signature sig ... end, or a .mli file
  • C/C++: .h files

Examples of the public/private keywords approach:

  • Rust: pub for public types and public fields of a struct. Private by default.
  • Java: public/private for fields and methods
  • Go: If a function/field has an identifier that starts with an uppercase letter, then it’s visible from other packages (i.e. public). Vice versa.

What are the pros and cons of both approaches?

They’re not entirely equivalent, since you don’t have private let definitions in OCaml, and the only way to achieve this is by doing the opposite: mark what’s public, with an interface file.

So an OCaml module by default has everything set to public, however you name your values:

module Mod = struct
  let ___my_private_function () = print_string "not so private!"
end

let () = Mod.___my_private_function ()

If you wanted this to be private, you must specify an interface:

module Mod : sig
end = struct
  let ___my_private_function () = print_string "not so private!"
end

let () = Mod.___my_private_function ()
(* this will be an Unbound value error *)

Now for types its a little different.

The same rule as for values applies, but there is also a private keyword that you can use you say “this type is readable by other modules, but only this module can create values of this type”.

module Mod : sig
  type a = A
  type b = private B
  type c
end = struct
  type a = A
  type b = private B
  type c = C
  type d = D
end

let a = Mod.A 
(* this is ok! *)

let b = Mod.B
(* this fails with: Cannot create values of the private type Mod.b *)

let c = Mod.C
(* this fails with: Unbound constructor Mod.C
   because we just don't know anything about this type
   other than it exists
 *)

let d = Mod.D
(* in this case Mod.d is entirely private, but it fails just like Mod.C *)

Hope this helps!

2 Likes

Thank you for making me aware of the existence of the private keyword!

In your example, how are users of Mod supposed to use b and B?

Users could use b and B if they were given some way to use it. E.g.,

module Mod : sig
  type b = private B
  val b : b
end = struct
  type b = B
  let b = B
end

Now users can do, e.g.:

match Mod.b with Mod.B -> true
2 Likes

OCaml’s approach more-or-less forces you to fully define your interface in one place (barring inclusions etc), which IMO is a good thing for downstream consumers because this one place is also, conveniently, where the docs are most likely to be, by convention and tooling-friendliness.
That said, I do believe private to be the superior default scope. I like Rust & co’s field-level access control, OCaml has that, FWIW, in class/object definitions (method private ... see the manual).

OCaml also (as of 4.08) has another convenient way to have ad-hoc private defs:

open struct
  type private_type = ...
  let private_val = ...
end
...

the way it operates is explained in the manual


The short answer is pattern-matching. I’d rather the keyword for type definition be called readonly to better illustrate its functionality, but I believe the choice went with private to avoid breaking code by introducing another keyword.

You can’t pattern-match the internal structure of abstract types (a type defined type ... t without = ...), but you can private ones. You end up doing field accesses instead of calling functions, and you can naturally use match with variants. It saves you time writing logic-less accessors and saves the compiler effort hopefully optimizing them away.
They can also be used to achieve the smart-constructors pattern arguably better than what the linked Haskell is capable of, if you expect users of your API to construct a value of a type marked private in the interface (marking it private in the implementation makes you unable to construct it as well).

Taking (and simplifying) the example right from the manual:


module M : sig
  type t = private B of int
  val b : int -> t
end = struct
  type t = B of int
  let b n = assert (n > 0); B n
end

But that’s not really special to private types, you can also do it with abstract types. The win private types give you is again in consuming. Note that you don’t need to have constructors to have private types, you can also have private abbreviations e.g. type t = private string. This allows you to have for free a t -> string conversion (but not the other way) like so: (expr : t :> string) or (expr :> string).

Marking a type private instead of fully abstract has other benefits as well, highlighted in the manual.

2 Likes

The OCaml approach is more modular: the same underlying implementation can satisfy many different public interfaces, depending on the needs of the client.

2 Likes