Idiomatic optional flags as arguments to functions?

Suppose I have a function which has, amongst its arguments, an optional flag that mildly changes its behaviour if present: something like

val of_int : ?atomic:bool -> int -> Initialiser.t
(** [of_int ?atomic value] creates a C initialiser with an integer type and value;
    if ?atomic is present and true, the type will be `atomic_int`, else `int`. *)

Usually, if I’m making these flags, I’ll encode them just as above: as optional Booleans with a default value false. This makes it easy to delegate the decision as to which value to pass in further up the call chain, or to run-time, but gives no hints in the type system as to whether the default is true or false.

I saw, in some Jane Street codebase I think, a different approach:

val of_int : ?atomic:unit -> int -> Initialiser.t
(** [of_int ?atomic value] creates a C initialiser with an integer type and value;
    if ?atomic is present, the type will be `atomic_int`, else `int`. *)

This seems a bit clearer: since there’s only one value atomic can be if present, it stands to reason that its presence must mean true and its absence must mean false. However, we’ve lost the ability to just pass in ~atomic:some_other_boolean - we’d have to do ?atomic:(Option.some_if some_other_boolean ()), which is quite clunky.

Are either of these particularly more idiomatic/encouraged in OCaml?

(Of course, the winning move here might be to split function in two:

val of_int : int -> Initialiser.t
val of_atomic_int : int -> Initialiser.t

This feels a bit awkward, as the underlying records that this function is a wrapper over do treat atomicity as a boolean, and so we’re turning a boolean into a pair of functions and then, if we need to make a runtime choice between the two, turning it back into a boolean. Hmm.)

FWIW, both optional bools and optional unit arguments are in use in Jane Street’s code. Optional bools are somewhat more common but both are used. They each have trade-offs like you described.

A third option, which does not really answer your question, is to use a required bool instead. Here’s something I learned from somebody else: a good way of thinking about whether you should use optional arguments might be to consider—if the user needs to know (and think about) what the default argument is in order to make a choice about whether to provide the optional argument, then that argument probably shouldn’t be optional, because it would be unsafe to change the default later.

4 Likes

I dislike the unit style. It has been described to me as an acquired taste but I doubt.

Changing the default will likely always be problematic so in my opinion this is not exactly the right perspective but almost.

It’s not so much about the person writing the code (“user”) it’s more about the person reading it. Each optional argument is a potential cognitive burden on the reader in that the reader has to 1) know it exists and 2) know what it’s default value is.

A good example of a badly designed API is the path optional argument of Bos.OS.Dir.create. When I re-read code I do remember it may create the path or not but that’s only because I designed the API, that’s already a bad point. But moreover I personally never remember the default.

From an ergonomic point of view it would have been better not to make that an optional argument:

  1. It makes the writer think about whether the full path needs to be created or only the directory.
  2. A reader not too familiar with the API can understand what is going by simply reading the code.

I redesigned that API in another context and made that argument non-optional.

When you design APIs you should always think about how it reads/understands and if it makes the writer ask herself the right questions.

3 Likes