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?