We use OCaml’s optional arguments a fair bit at Jane Street. One nagging problem has been coming up with a good way of documenting in the mli for a library what the default value for an optional argument is. Of course, one could state the default value in a comment, but one is not forced to do so; also the comment may become stale and incorrect, so to be a sure a reader has to look at the ml file to be sure what the actual default is.

It would be nice if at least for constants, one could specify the default in the type of a function, and have the default enforced by the type checker. Something like:

module M : sig
  val f : ?(i:int = 13) -> ?(b:bool = false) -> unit -> int * bool
end = struct
  let f ?(i = 13) ?(b = false) () = i, b
end

The following program would cause a type error due to the default for i not matching the type.

module M : sig
  val f : ?(i:int = 14) -> ?(b:bool = false) -> unit -> int * bool
end = struct
  let f ?(i = 13) ?(b = false) () = i, b
end

I’ve recently been trying to come up with an acceptably terse way of doing something like this without changing the language or type system. I have an approach that I’ll now explain.

Start by defining a type constructor for a family of “singleton” types, each instance of which has a single value:

type ('phantom, 'real) singleton

The 'real type argument is the actual type of the value (e.g. bool, int, etc.). The 'phantom type is used to distinguish every singleton type from every other singleton type. Here are some instances of the singleton type family.

type thirteen = (phantom_thirteen, int) singleton
type true_ = (phantom_true, bool) singleton
type false_ = (phantom_false, bool) singleton

Each of these singleton types is inhabited by a single value.

val thirteen : thirteen
val true_ : true_
val false_ : true_

Next, define a type constructor for optional arguments that have a default, where the type argument is the singleton type that identifies what the default value is.

type 'singleton is_the_default

Using this we can write the type of our f function as:

val f : ?(i:thirteen is_the_default) -> ?(b:false_ is_the_default) -> unit -> int

So that we can call such functions, we define an override function that allows one to override a default value with an actual value at a call.

val override : 'real -> (_, 'real) singleton is_the_default

Because override can produce any phantom type, it can override any singleton type, so long as the real types agree.

Here are some example calls to f.

f ~i:(override 17) ()
f ~b:(override false) ()
f ~i:(override 17) ~b:(override false) ()

For convenience, we bind override to a prefix operator !!. Then usage is quite concise.

let (!!) = override
f ~i:!!17 ()
f ~b:!!false ()
f ~i:!!17 ~b:!!false ()

To implement f in such a way that the type system enforces that the default is what the type says it is, we need another function:

val defaults_to :
    ('phantom, 'real) singleton is_the_default option
  -> ('phantom, 'real) singleton
  -> 'real

defaults_to returns the value of the override if it is provided, else it returns the (only) value in the singleton type if not.

Now we can define f.

let f ?i ?b () =
  let i = defaults_to i thirteen in
  let b = defaults_to b false_ in
  i, b

The last piece we need is a way to define new singleton types. For that we use a function that takes the representative value and produces a module with a new phantom type, along with the the single value of the new singleton type.

module type Singleton = sig
  type phantom
  type real
  type t = (phantom, real) singleton
  val t : t
end

val singleton : 'a -> (module Singleton with type real = 'a)

We can now use singleton to define the singleton types and values that we need:

module Thirteen = (val singleton 13 : Singleton with type real = int)
module True_ = (val singleton true : Singleton with type real = bool)
module False_ = (val singleton false : Singleton with type real = bool)
type thirteen = Thirteen.t let thirteen = Thirteen.t
type true_ = True.t
let true_ = True.t
type false_ = False.t
let false_ = False.t

That’s the entire interface to optional arguments with default in their type. For completeness, here is the interface in one place.

module type Optional = sig
  type ('phantom, 'real) singleton
  type 'singleton is_the_default

  val override : 'real -> (_, 'real) singleton is_the_default

  val defaults_to :
    ('phantom, 'real) singleton is_the_default option
    -> ('phantom, 'real) singleton
    -> 'real

  module type Singleton = sig
    type phantom
    type real
    type t = (phantom, real) singleton
    val t : t
  end

  val singleton : 'a -> (module Singleton with type real = 'a)
end

The implementation of Optional is trivial. Singletons and defaults are just the underlying value.

module Optional : Optional = struct
  type ('phantom, 'real) singleton = 'real
  type 'singleton is_the_default = 'singleton

  let override x = x

  let defaults_to opt default =
    match opt with
    | None -> default
    | Some x -> x
  ;;

  module type Singleton = sig
    type phantom
    type real
    type t = (phantom, real) singleton
    val t : t
  end

  let singleton (type t) (t : t) =
    (module struct type phantom
      type real = t
      type t = real
      let t = t
    end : Singleton with type real = t)
  ;;
end

And here’s some example code to test the new module.

include struct
  open Optional
  type 'a is_the_default = 'a Optional.is_the_default
  let defaults_to = defaults_to
  let (!!) = override
  let singleton = singleton
  module type Singleton = Singleton
end

module Bool : sig
  type t = bool
  module True_ : Singleton with type real = t
  module False_ : Singleton with type real = t
end = struct
  type t = bool
  module True_ = (val singleton true : Optional.Singleton with type real = t)
  module False_ = (val singleton false : Optional.Singleton with type real = t)
end

include struct
  open Bool.True_
  type true_ = t
  let true_ = t
end

include struct
  open Bool.False_
  type false_ = t
  let false_ = t
end

module Test_bool : sig
  val f :
    ?x:true_ is_the_default
    -> ?y:false_ is_the_default
    -> unit -> bool * bool
end = struct
  let f ?x ?y () =
    let x = defaults_to x true_ in
    let y = defaults_to y false_ in
    x, y
  ;;
end

let () =
  let f = Test_bool.f in
  assert ((true , false) = f ());
  assert ((false, false) = f ~x:!!false ());
  assert ((false, true ) = f ~x:!!false ~y:!!true ());
;;

module Int : sig
  type t = int
  module N_zero : Singleton with type real = t
  module N_one : Singleton with type real = t
  module N_million : Singleton with type real = t
end = struct
  type t = int
  module N_zero = (val singleton 0 : Optional.Singleton with type real = t)
  module N_one = (val singleton 1 : Optional.Singleton with type real = t)
  module N_million = (val singleton 1_000_000 : Optional.Singleton with type real = t)
end

module Test_int : sig
  val f :
    ?x:Int.N_zero.t is_the_default
    -> ?y:Int.N_one.t is_the_default
    -> ?z:Int.N_million.t is_the_default
    -> unit
    -> int * int * int
end = struct
  let f ?x ?y ?z () =
    let x = defaults_to x Int.N_zero.t in
    let y = defaults_to y Int.N_one.t in
    let z = defaults_to z Int.N_million.t in
    x, y, z
  ;;
end

let () =
  let f = Test_int.f in
  assert ((0, 1, 1_000_000) = f ());
  assert ((0, 1,        13) = f ~z:!!13 ());
  assert ((1, 2,         3) = f ~x:!!1 ~y:!!2 ~z:!!3 ());
;;

With 3.13, the usage will be nicer because we won’t have to state the redundant package type when we define new singletons. E.g. we will be able to write the following:

module True_ = (val singleton true)

But even without that, it doesn’t seem too painful to start using this right now, since one only needs to define a few singleton types for the common default values, and the actual definition and use of functions with optional arguments is pretty syntactically lightweight.

Comments or suggestions for improvement anyone?