Are there use cases for single case variants in Ocaml?

390 Views Asked by At

I've been reading F# articles and they use single case variants to create distinct incompatible types. However in Ocaml I can use private module types or abstract types to create distinct types. Is it common in Ocaml to use single case variants like in F# or Haskell?

4

There are 4 best solutions below

1
On BEST ANSWER

For what it's worth it seems to me this wasn't particularly common in OCaml in the past.

I've been reluctant to do this myself because it has always cost something: the representation of type t = T of int was always bigger than just the representation of an int.

However recently (probably a few years) it's possible to declare types as unboxed, which removes this obstacle:

type [@unboxed] t = T of int

As a result I've personally been using single-constructor types much more frequently recently. There are many advantages. For me the main one is that I can have a distinct type that's independent of whether it's representation happens to be the same as another type.

You can of course use modules to get this effect, as you say. But that is a fairly heavy solution.

(All of this is just my opinion naturally.)

0
On

Yet another case for single-constructor types (although it does not quite match your initial question of creating distinct types): fancy records. (By contrast with other answers, this is more a syntactic convenience than a fundamental feature.)

Indeed, using a relatively recent feature (introduced with OCaml 4.03, in 2016) which allows writing constructor arguments with a record syntax (including mutable fields!), you can prefix regular records with a constructor name, Coq-style.

type t = MakeT of {
  mutable x : int ;
  mutable y : string ;
}

let some_t = MakeT { x = 4 ; y = "tea" }
(* val some_t : t = MakeT {x = 4; y = "tea"} *)

It does not change anything at runtime (just like Constr (a,b) has the same representation as (a,b), provided Constr is the only constructor of its type). The constructor makes the code a bit more explicit to the human eye, and it also provides the type information required to disambiguate field names, thus avoiding the need for type annotations. It is similar in function to the usual module trick, but more systematic.

Patterns work just the same:

let (MakeT { x ; y }) = some_t
(* val x : int = 4 *)
(* val y : string = "tea" *)

You can also access the “contained” record (at no runtime cost), read and modify its fields. This contained record however is not a first-class value: you cannot store it, pass it to a function nor return it.

let (MakeT fields) = some_t in fields.x (* returns 4 *)
let (MakeT fields) = some_t in fields.x <- 42
(* some_t is now MakeT {x = 42; y = "tea"} *)

let (MakeT fields) = some_t in fields
(*                             ^^^^^^
   Error: This form is not allowed as the type of the inlined record could escape. *)

0
On

Another specialized use case fo a single constructor variant is to erase some type information with a GADT (and an existential quantification). For instance, in

type showable = Show: 'a * ('a -> string) -> showable
let show (Show (x,f)) = f x
let showables = [ Show (0,string_of_int); Show("string", Fun.id) ]

The constructor Show pairs an element of a given type with a printing function, then forget the concrete type of the element. This makes it possible to have a list of showable elements, even if each elements had a different concrete types.

0
On

Another use case of single-constructor (polymorphic) variants is documenting something to the caller of a function. For instance, perhaps there's a caveat with the value that your function returns:

val create : unit -> [ `Must_call_close of t ]

Using a variant forces the caller of your function to pattern-match on this variant in their code:

let (`Must_call_close t) = create () in (* ... *)

This makes it more likely that they'll pay attention to the message in the variant, as opposed to documentation in an .mli file that could get missed.

For this use case, polymorphic variants are a bit easier to work with as you don't need to define an intermediate type for the variant.