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:
type private_type = ...
let private_val = ...
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
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.