7

I want to define a type so that all construction goes through module members that can preserve invariants, but allow destructuring for pattern matching.

I'm just learning OCaml but the following almost works for an int pair with the invariant that the left should be strictly less than the right

module Range : sig
  type t = private { left:int; right:int }
  exception InvalidRange of (int*int)
  val make : int -> int -> t
end = struct
  type t = { left:int; right:int }
  exception InvalidRange of (int*int)
  let make left right = if left < right
    then { left; right }
    else raise (InvalidRange (left, right))
end

which works thus

# let p = Range.make 1 2;;
val p : Range.t = {Range.left = 1; Range.right = 2}
# let q = Range.make 2 1;;
Exception: Range.InvalidRange (2, 1).

and destructuring works after a fashion

# let {Range.left=x; Range.right=y} = p;;
val x : int = 1
val y : int = 2

while constructing fails

# let badp = {Range.left = 2; Range.right = 1};;
  let badp = {Range.left = 2; Range.right = 1};;
Error: Cannot create values of the private type Range.t
# open Range;;
# let badp = {left = 2; right=1};;
  let badp = {left = 2; right=1};;
Error: Cannot create values of the private type Range.t

but what I would really like to do is have the syntactic convenience of destructuring tuples. The below does not work:

module Range : sig
  type t = private int*int
  exception InvalidRange of (int*int)
  val make : int -> int -> t
end = struct
  type t = int*int
  exception InvalidRange of (int*int)
  let make left right = if left < right
    then (left, right)
    else raise (InvalidRange (left, right))
end

but then I can't destructure it using a tuple pattern:

# let r = Range.make 1 2 ;;
val r : Range.t = (1, 2)
# let (a, b) = r;;
  let (a, b) = r;;
Error: This expression has type Range.t
       but an expression was expected of type 'a * 'b

I could change the type to type t = R of (int * int) but I need these to be as light-weight memory-wise as possible. Any ideas?

Mike Samuel
  • 118,113
  • 30
  • 216
  • 245

5 Answers5

9

As explained in the manual, you need an explicit coercion:

# let (a, b) = (r :> int*int);;
val a : int = 1
val b : int = 2
esope
  • 760
  • 3
  • 12
  • Thanks for the pointer to the manual. It's going to take me a while to wrap my head around when coercion between nominal and erased types happens automatically and when it doesn't. Does the presence of the type coercion operator mean that destructuring is always possible -- so there's no real information hiding (e.g. of mutables or secrets) possible except via values closed over by functions? – Mike Samuel May 22 '12 at 21:09
  • 2
    There is a difference between 1) transparent types, 2) abstract types, 3a) private variant or record types 3b) private type abbreviations. For 1) coercion is possible both ways and is implicit. For 2) no coercion is possible at all. For 3a) coercion is possible one way and seems to be implicit. For 3b) coercion is also possible one way and seems to be explicit. It is important to note that coercions have no computational contents (they are erased at runtime). – esope May 22 '12 at 22:42
  • 1
    thanks. So the type system is two-tiered -- a fundamental one that has a distinction between concrete and abstract types that affects message dispatch and a superficial one subject to erasure that can rule out dispatch but does not affect it. – Mike Samuel May 22 '12 at 23:14
4

A simple way to do that is to add a to_tuple function and make tour type abstract.

module Range : sig
  type t
  exception InvalidRange of (int*int)
  val make : int -> int -> t
  val to_tuple : t -> (int * int)
end = struct
  type t = { left:int; right:int }
  exception InvalidRange of (int*int)

  let make left right = if left < right
    then { left; right }
    else raise (InvalidRange (left, right))

  let to_tuple t = t.left, t.right

end

then you can do

let (a, b) = to_tuple range
Thomash
  • 6,339
  • 1
  • 30
  • 50
  • 2
    Your solution works. The difference with the one using private types is that `to_tuple` has a computational contents, whereas the coercion (_ :> _) does not have one. To understand that, consider a value of type `Range.t list` and try to convert it to a list of type `(int*int) list`. With your solution you need to use `List.map to_tuple _` (which copies the list); with the private type solution, you only write `(_ :> (int*int) list)` which does nothing (coercions are erased). – esope May 23 '12 at 12:44
3

Your solution of type t = R of (int * int) will be lightweight in terms of memory, and is syntactically a bit lighter-weight than the coercion solution. OCaml optimizes the case of a single-constructor datatype so you don't pay for it. (I don't have an official reference for this claim, but Adam Chlipala (an OCaml expert) mentions it here: http://adam.chlipala.net/cpdt/html/Subset.html).

2

I just ran this test with Objsize (reports sizes of OCaml values).

# type fancy = R of int * int;;
type fancy = R of int * int
# Objsize.objsize (R (3, 5));;
- : Objsize.info = {Objsize.data = 2; Objsize.headers = 1; Objsize.depth = 0}
# Objsize.objsize (3,5);;
- : Objsize.info = {Objsize.data = 2; Objsize.headers = 1; Objsize.depth = 0}

If you believe these values (which I do), there's no size penalty for using your own single-constructor type instead of a tuple.

Jeffrey Scofield
  • 65,646
  • 2
  • 72
  • 108
  • I don't think you answer the question. – Thomash May 22 '12 at 22:38
  • 3
    I was trying to answer the part where he says "I could change the type to type t = R of (int * int) but I need these to be as light-weight memory-wise as possible. Any ideas?" My claim is that `R of int * int` is not a heaviweight type memory-wise. – Jeffrey Scofield May 22 '12 at 23:55
1

Actually OCaml Runtime encoding is very simple. Here's a tool you may find useful https://github.com/bobzhang/caml-inspect

bobzhang
  • 1,771
  • 3
  • 17
  • 19