4

Reading the Real World OCaml book I came across the following type declaration (Chapter 6: Variants):

# type color =
  | Basic of basic_color * weight (* basic colors, regular and bold *)
  | RGB   of int * int * int       (* 6x6x6 color cube *)
  | Gray  of int                   (* 24 grayscale levels *)
;;

type color =
    Basic of basic_color * weight
  | RGB of int * int * int
  | Gray of int

I thought the RGB and Gray variants could be constrained further. For example, each int in the RGB tuple should only be able to have the values 0-5.

In Erlang I'd do that like this:

-type rgbint() :: 0 | 1 | 2 | 3 | 4 | 5.
-type rgb() :: {rgb_int(), rgb_int(), rgb_int().

However, when I tried this in OCaml (in utop), it complained:

# type rgbint = 0 | 1 | 2 | 3 | 4 | 5 ;;

Error: Parse error: [type_kind] expected after "=" (in [opt_eq_pluseq_ctyp]) 

Questions:

  • In OCaml, is it not allowed to use literals on the RHS of a type definition?
  • How in OCaml would I do something like the Erlang rgbint() definition above?

With thanks and best wishes

Ivan

Ivan Uemlianin
  • 953
  • 7
  • 21
  • In erlang, the range checking is dynamic, right ? – Drup Jan 30 '16 at 22:57
  • Could also use poly variants to easy the annoyance, ```let v_w = function | 10 -> `_10 | 11 -> `_11 | 12 -> `_12 | 13 -> `_13 | 14 -> `_14 | 15 -> `_15 | 16 -> `_16 | 17 -> `_17 | 18 -> `_18 | 19 -> `_19 | 20 -> `_20 | 21 -> `_21 | 22 -> `_22 | 23 -> `_23 | 24 -> `_24 | _ -> assert false``` –  Jan 31 '16 at 03:46
  • @Drup yes, but ... yes erlang uses dynamic types, but with type annotations like the above you can type-check code at compile time (e.g. using dialyzer). – Ivan Uemlianin Feb 01 '16 at 10:55
  • @Edgar thanks but no understand. I am only on Chapter 6 of RWO :) – Ivan Uemlianin Feb 01 '16 at 10:56

2 Answers2

9

This is a typical use-case for private types

OCaml allows you to mark a type as private in a signature which makes it something between in a concrete and an abstract type:

  • like values of a concrete type, values of a private type can be deconstructed using am match pattern;

  • like values of an abstract type, values of a private type can only be constructed using functions from the module defining this type.

For instance, your code snippet could be translated as

module Color : sig
  type t =
  | Basic of basic_color * weight   (* basic colors, regular and bold *)
  | RGB of rgbint * rgbint * rgbint (* 6x6x6 color cube *)
  | Gray of int                     (* 24 grayscale levels *)
  and basic_color =
   | Black | Red | Green | Yellow | Blue | Magenta | Cyan | White
  and weight = Regular | Bold
  and rgbint = private int
  val rgb : int * int * int -> t
end = struct
  type t =
  | Basic of basic_color * weight
  | RGB   of rgbint * rgbint * rgbint
  | Gray  of int
  and basic_color =
   | Black | Red | Green | Yellow | Blue | Magenta | Cyan | White
  and weight = Regular | Bold
  and rgbint = int

  let rgb (r, g, b) =
    let validate x =
      if x >= 0 && x < 6 then x else invalid_arg "Color.rgb"
    in
    RGB (validate r, validate g, validate b)
end

With this definition, we can, of course, create Color.RGB values with the Color.rgb function:

# Color.rgb(0,0,0);;
- : Color.t = Color.RGB (0, 0, 0)

It is not possible to self-assemble a Color.RGB value out of its components:

# Color.RGB(0,0,0);;
Characters 10-11:
  Color.RGB(0,0,0);;
            ^
Error: This expression has type int but an expression was expected of type
         Color.rgbint

It is possible to deconstruct values of type Color.rgbint as integers, using a type coercion:

# match Color.rgb(0,0,0) with
  | Color.RGB(r,g,b) ->
    if ((r,g,b) :> int * int * int) = (0, 0, 0) then
      "Black"
    else
      "Other"
  | _ -> "Other";;      
- : string = "Black"

Yaron Minsky wrote two blog posts about private types, they are worth a read:

Michaël Le Barbier
  • 6,103
  • 5
  • 28
  • 57
  • 2
    So, at bottom you're saying, "use runtime validation"? – Ivan Uemlianin Feb 01 '16 at 10:53
  • Do you refer to the type coercion? Type coercion is controlled by the compiler and is type-safe and is guaranteed not to break at run time. Type coercion is a hint for a type solver, not an open door to undefined behaviour. – Michaël Le Barbier Feb 01 '16 at 11:13
  • 1
    No, I refer to your validate function. – Ivan Uemlianin Feb 01 '16 at 16:47
  • Oh okay! :) Yes, but it is not as bad as it looks like, because it is clear who is in charge to do this validation. If we serialise and deserialise data, then runtime validation is non-optional. Using a finite enumeration is doable, but only if the terms in the enumeration is small enough. – Michaël Le Barbier Feb 01 '16 at 16:57
2

It makes sense to define a type by a list of values from some base type, but OCaml doesn't have types like that. It has a set of primitive types like int and char. You can define your own new primitive types whose values are literals like Yes and No. (When you define such a literal it looks like a capitalized identifier.) You can combine those with parameterized types like lists, arrays, and so on.

If you really want an int value restricted to a certain range, you can define it as an abstract type hidden by a module interface. In the module you would need to define all the operations you want to support on your restricted range of ints. (Note that such types aren't closed under the usual arithmetic operations.)

You can also define:

type rgbint = RBG0 | RGB1 | RGB2 | RGB3 | RGB4 | RGB5

In practice this might be what you would end up doing, though such a type feels cumbersome when you're thinking of the underlying values as numbers.

Jeffrey Scofield
  • 65,646
  • 2
  • 72
  • 108
  • Thanks. This is what I feared --- and only good for symbolic types, no good for e.g. float ranges, but I suppose we're getting into dependent types territory. Cumbersome for this example yes: otoh I might like a way to "add" rgb triples together to create a new rgb triple – Ivan Uemlianin Feb 01 '16 at 10:57