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:

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

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

  1. module M : sig
  2. val f : ?(i:int = 14) -> ?(b:bool = false) -> unit -> int * bool
  3. end = struct
  4. let f ?(i = 13) ?(b = false) () = i, b
  5. 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:

  1. 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.

  1. type thirteen = (phantom_thirteen, int) singleton
  2. type true_ = (phantom_true, bool) singleton
  3. type false_ = (phantom_false, bool) singleton

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

  1. val thirteen : thirteen
  2. val true_ : true_
  3. 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.

  1. type 'singleton is_the_default

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

  1. 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.

  1. 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.

  1. f ~i:(override 17) ()
  2. f ~b:(override false) ()
  3. f ~i:(override 17) ~b:(override false) ()

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

  1. let (!!) = override
  2. f ~i:!!17 ()
  3. f ~b:!!false ()
  4. 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:

  1. val defaults_to :
  2. ('phantom, 'real) singleton is_the_default option
  3. -> ('phantom, 'real) singleton
  4. -> '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.

  1. let f ?i ?b () =
  2. let i = defaults_to i thirteen in
  3. let b = defaults_to b false_ in
  4. 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.

  1. module type Singleton = sig
  2. type phantom
  3. type real
  4. type t = (phantom, real) singleton
  5. val t : t
  6. end
  7.  
  8. val singleton : 'a -> (module Singleton with type real = 'a)

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

  1. module Thirteen = (val singleton 13 : Singleton with type real = int)
  2. module True_ = (val singleton true : Singleton with type real = bool)
  3. module False_ = (val singleton false : Singleton with type real = bool)
  4. type thirteen = Thirteen.t let thirteen = Thirteen.t
  5. type true_ = True.t
  6. let true_ = True.t
  7. type false_ = False.t
  8. 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.

  1. module type Optional = sig
  2. type ('phantom, 'real) singleton
  3. type 'singleton is_the_default
  4.  
  5. val override : 'real -> (\_, 'real) singleton is\_the_default
  6.  
  7. val defaults_to :
  8. ('phantom, 'real) singleton is_the_default option
  9. -> ('phantom, 'real) singleton
  10. -> 'real
  11.  
  12. module type Singleton = sig
  13. type phantom
  14. type real
  15. type t = (phantom, real) singleton
  16. val t : t
  17. end
  18.  
  19. val singleton : 'a -> (module Singleton with type real = 'a)
  20. end

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

  1. module Optional : Optional = struct
  2. type ('phantom, 'real) singleton = 'real
  3. type 'singleton is_the_default = 'singleton
  4.  
  5. let override x = x
  6.  
  7. let defaults_to opt default =
  8. match opt with
  9. | None -> default
  10. | Some x -> x
  11. ;;
  12.  
  13. module type Singleton = sig
  14. type phantom
  15. type real
  16. type t = (phantom, real) singleton
  17. val t : t
  18. end
  19.  
  20. let singleton (type t) (t : t) =
  21. (module struct type phantom
  22. type real = t
  23. type t = real
  24. let t = t
  25. end : Singleton with type real = t)
  26. ;;
  27. end

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

  1. include struct
  2. open Optional
  3. type 'a is_the_default = 'a Optional.is_the_default
  4. let defaults_to = defaults_to
  5. let (!!) = override
  6. let singleton = singleton
  7. module type Singleton = Singleton
  8. end
  9.  
  10. module Bool : sig
  11. type t = bool
  12. module True_ : Singleton with type real = t
  13. module False_ : Singleton with type real = t
  14. end = struct
  15. type t = bool
  16. module True_ = (val singleton true : Optional.Singleton with type real = t)
  17. module False_ = (val singleton false : Optional.Singleton with type real = t)
  18. end
  19.  
  20. include struct
  21. open Bool.True_
  22. type true_ = t
  23. let true_ = t
  24. end
  25.  
  26. include struct
  27. open Bool.False_
  28. type false_ = t
  29. let false_ = t
  30. end
  31.  
  32. module Test_bool : sig
  33. val f :
  34. ?x:true_ is_the_default
  35. -> ?y:false_ is_the_default
  36. -> unit -> bool * bool
  37. end = struct
  38. let f ?x ?y () =
  39. let x = defaults_to x true_ in
  40. let y = defaults_to y false_ in
  41. x, y
  42. ;;
  43. end
  44.  
  45. let () =
  46. let f = Test_bool.f in
  47. assert ((true , false) = f ());
  48. assert ((false, false) = f ~x:!!false ());
  49. assert ((false, true ) = f ~x:!!false ~y:!!true ());
  50. ;;
  51.  
  52. module Int : sig
  53. type t = int
  54. module N_zero : Singleton with type real = t
  55. module N_one : Singleton with type real = t
  56. module N_million : Singleton with type real = t
  57. end = struct
  58. type t = int
  59. module N_zero = (val singleton 0 : Optional.Singleton with type real = t)
  60. module N_one = (val singleton 1 : Optional.Singleton with type real = t)
  61. module N_million = (val singleton 1_000_000 : Optional.Singleton with type real = t)
  62. end
  63.  
  64. module Test_int : sig
  65. val f :
  66. ?x:Int.N_zero.t is_the_default
  67. -> ?y:Int.N_one.t is_the_default
  68. -> ?z:Int.N_million.t is_the_default
  69. -> unit
  70. -> int * int * int
  71. end = struct
  72. let f ?x ?y ?z () =
  73. let x = defaults_to x Int.N_zero.t in
  74. let y = defaults_to y Int.N_one.t in
  75. let z = defaults_to z Int.N_million.t in
  76. x, y, z
  77. ;;
  78. end
  79.  
  80. let () =
  81. let f = Test_int.f in
  82. assert ((0, 1, 1_000_000) = f ());
  83. assert ((0, 1, 13) = f ~z:!!13 ());
  84. assert ((1, 2, 3) = f ~x:!!1 ~y:!!2 ~z:!!3 ());
  85. ;;

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:

  1. 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?