use explicit type to limit visibility of method in object to the module defining it

Viewed 236

I have a coordinate "class" (implemented using a factory function returning an object literal) in OCaml and am trying to figure out how to restrict the visibility of a method on that object to the module it was defined in.

Here's the source file

(* coordinate.ml *)
type coordinate = < get_x : int; get_y : int >

let create_coord ~x ~y = object
  val x = x
  val y = y
  method get_x = x
  method get_y = y
  (* hide this method from public interface *)
  method debug_print = Printf.printf "(x=%d, y=%d)\n"
end

and here's the interface

(* coordinate.mli *)
(* the type synonym coordinate is compatible with the "real"
 * type, but does not expose the debug_print method *)
type coordinate = < get_x : int; get_y : int >

val create_coord : x:int -> y:int -> coordinate

My intent was to allow the object returned by create_coord to have all of methods visible inside the body of the Coordinate module. The only reason the coordinate type alias is repeated in the .ml file is to satisfy the compiler/allow it to be used in the .mli.

However, I want to prevent consumers of this module from using the debug_print method. I figured that, "since OCaml supports structural typing", an object type with strictly fewer methods would be a "compatible" type for the purposes of type ascription.

However, when I attempt to compile the files I get the following error:

$ ocamlc coordinate.mli
$ ocamlc coordinate.ml
File "coordinate.ml", line 1:
Error: The implementation coordinate.ml
       does not match the interface coordinate.cmi:
       Values do not match:
         val create_coord :
           x:'a ->
           y:'b ->
           < debug_print : int -> int -> unit; get_x : 'a; get_y : 'b >
       is not included in
         val create_coord : x:int -> y:int -> coordinate
       File "coordinate.ml", line 3, characters 4-16: Actual declaration
Exit 2

Is there a way to limit the visibility of debug_print outside of Coordinate while making it freely accessible inside?

3 Answers

I think the best solution is to use private row type:

module M : sig
  type t = private < x : int; y : int; .. >
  val make : int -> int -> t
  val f: t -> unit
end = struct
  type t = < x : int; y : int; z : [`Not_exposed] >

  let make x y = object
    method x = x
    method y = y
    method z = `Not_exposed
  end

  let f o =
    (* Access a method not exposed outside of the module *)
    assert (o#z = `Not_exposed)
end;;

This way, you don't need any coercion function. Still, any object of type t is seen with his private methods inside module M, but only the public one (xand y) can be accessed outside. Find more information in the manual.

You can make some of this work with an explicit coercion. A minimal example:

type t = < x : int; y : int >
let make x y : t =
  let o =
    object
      method x = x
      method y = y
      method z = `Not_exposed
    end
  in
  (o :> t)

(* val make : int -> int -> t = <fun> *)

That still hides z inside of the module. You can get a bit further by shadowing make:

module M : sig
  type t = < x : int; y : int >
  val make : int -> int -> t
end = struct
  type t = < x : int; y : int >

  let make x y = object
    method x = x
    method y = y
    method z = `Not_exposed
  end

  let f o =
    (* Access a method not exposed outside of the module *)
    assert (o#z = `Not_exposed)

  (* Shadow the make function, coercing the result into type t *)
  let make x y =
    (make x y :> t)
end

but this will not allow you to expose f as t -> unit as t does not have a z method.

You could also expose two types, keeping the full object type abstract outside of the module's context and providing a function to coerce/convert to the exposed, more restricted type:

module M : sig
  type t = < x : int; y : int >
  type opaque

  val make : int -> int -> opaque

  val f : opaque -> unit

  val of_opaque : opaque -> t
end = struct
  type t = < x : int; y : int >
  type opaque = < t; z : [`Not_exposed] >

  let make x y = object
    method x = x
    method y = y
    method z = `Not_exposed
  end

  let f (o : opaque) =
    assert (o#z = `Not_exposed)

  let of_opaque (o : opaque) : t =
    (o :> t)
end

For posterity, I have found that it is possible to use an explicit polymorphic type and an explicit upcast to convince OCaml that it is okay to "forget" a method.

type kata = < foo : int; bar : string >
type ana = < foo : int >

let to_ana (k : < foo : int ; .. >) : ana = (k :> ana)

This works well with polymorphic code.

to_ana (object method foo = 42 method bar = "asdf" method baz = [] end)
Related