Avoid optional arguments for explicitness?

Consider this function with optional arguments:

let greet ?(name="User") ?(emoticon=":-)") () : unit =
  print_endline ("Hello, " ^ name ^ " " ^ emoticon);;

greet ();;
greet ~name:"Alice" ~emoticon:":P" ();;

Is it a good idea to avoid optional arguments for explicitness? Version without optional arguments:

let greet (name: string) (emoticon: string) : unit =
  print_endline ("Hello, " ^ name ^ " " ^ emoticon);;
let default_name = "user";;
let default_emoticon = ":-)";;

greet default_name default_emoticon;;
greet "Alice" ":P";;

I think the latter is more explicit and less error-prone.

In the former version with optional arguments, if we forget to supply an optional argument to a function, the default value will be used silently. We won’t notice it until a bug occurs (or a test fails). This won’t happen in the latter version without optional arguments. If we choose to use the default values, we have to be explicit about our decision.

In addition, explicitness is good for reading others’ code or our old code. When we see a function call that leaves out some optional argument, we have to recall what that parameter and corresponding default value are. In contrast, we can take in everything at a glance with let () = greet default_name default_emoticon.

Except when you have something like:

let start_stardrive core_rods coolant_density neutron_polarity gravitometric_density warp_manifold_zurbness = ...

Now before I can do anything with it I need to go away and look up what a sensible value for all of those will be.

If you have sensible defaults than they can be really useful to provide.

Now, you can do both of course with start_default_stardrive as well as the version that provides all the parameters you might want to change.

Too many parameters are a code smell. I would create a record for these parameters, and a default value:

let start_stardrive config = ...
type config = {...}
let default_config = {}

Besides, I doubt that “sensible” is good enough when we provide arguments for function calls. Each argument we supply is a decision, based on what the program is supposed to do. Using a default value is also a decision, so I prefer it to be explicit.

A record value with defaults that you can reuse is semantically the same thing as a function with default values of optional arguments which constructs a value.

Semantically the same, but I think optional arguments are more error-prone and less readable.

I find optional arguments nice when they’re used in entrypoints, ie.
functions exposed to the outside where we want configurability but
decent defaults. Internally to a module they’re a footgun because
it’s too easy to forget to pass the optional argument along, so I use
labels. Make sure to always have the “unused variable” family of
warnings enabled, in any case.

1 Like

I’m not sure why a test would fail? Wouldn’t the test-writers be writing their tests on the assumption that the default is … whatever it is in the code?

I understand the idea that explicitness of all parameters has value. But the closer you get to “business logic” the less that’s the case, I’ve found. Sure, for reusable modules where you can imagine multiple use-cases (hence, multiple sensible defaults), it can be the case that the defaults need to be bundled together into a config. Heck, I’ve done it. But the closer you get to the “top of the application”, I’ve found, the more places where there’s only one sensible default.

All that said,

In addition, explicitness is good for reading others’ code or our old code.

this is always, always in tension with the simple fact that verbosity is its own barrier to comprehensibility.


The sentence before your quote:

In this case, I would create a wrapper function that uses the only sensible default without the corresponding parameters.

I’m not sure, but I feel this is necessary verbosity. This verbosity doesn’t carry new information, but it places important information at a convenient place. Similarly, it’s advisable to always write type annotations in function signatures, instead of relying on type inference.

Yes, but the idea is, test-writers are aware of default arguments. They’re the same people who write the library, typically.

I guess my wording was a little misleading.

What I actually mean is, suppose Bob writes function A that calls function B (which has optional parameters). He may forget to supply an argument different than the default. Then the default value is used unintentionally. In this case, Bob won’t be notified until a bug occurs or a test for function A fails.

I’ll respond with another scenario. But first, again, I do understand that we need to be careful with optional args. It’s not good to use 'em liberally.

Suppose that there’s a cfg value with the default arguments. So Bob passes that to fun_B. Bob doesn’t know what’s inside cfg (there are, like, 10 different optional-but-now-default arguments hiding inside cfg). So in this case also, we get the same result (a latent bug).

Either you make your developer aware of the value of the default, or you don’t. If you do, you’ve got hopelessly verbose code; if you do not, you have your latent bug scenario. The packaging of that default doesn’t change things.


My proposal against optional arguments can by no means help someone who doesn’t know what he’s doing.

However, for programmers who read the docs before using a value etc., optional arguments still lead to more error-prone and less obvious code. With optional arguments, people express their intention (ie, “use default values”) by not saying it (ie, not supplying optional arguments). I feel the opposite (say what you want) is always better.

But that’s pretty much every new user of any given library. It’s certainly me.

Why is Dream easy to get started with? Partly it’s the docs, sure. But it’s also that the simple things are made simple. By not requiring me to figure out all the different fiddly options before I can serve up some generated html.

So - if you are writing a security-sensitive library and your users are all security professionals being paid to do a careful job then you can expect them to take time to understand your library. If not, I’ve probably got 20 other things to do and your library is just a tiny unimportant part of that - I want it to work without making me think too much about it.

If your library isn’t easy to get started with, people will turn to another. If there isn’t another in ocaml they may well turn to another language - a lot of people are writing code to fulfill a purpose and then need to get on with something else.

No sensible default - don’t offer one.
Several parameters but no sensible set of defaults for them all as a set - don’t offer any.
Sensible default and you don’t offer one - not helpful.


It’s difficult to say without context, but if forgetting to override a default argument has the potential to cause a bug, then don’t use optional arguments.

Explicit is better than implicit in my book.

Maybe another way to put it would be: don’t use default arguments unless you have a strong reason to do so (inverse the thinking logic)

It’s all subjective though

1 Like

Dune’s internal codestyle advises against optional arguments and I tend to agree. They are fine in some places but can be really problematic when they spread.

Avoid optional arguments. They increase brevity at the expense of readability and are annoying to grep. Furthermore, they encourage callers not to think at all about these optional arguments even if they often should.

1 Like

Late to the party but: in general I do agree that optional arguments should be avoided when possible. However, in large code base, when adding an opt-in new behavior to an existing function, an optional argument is a great way to extend your code without having to propagate the change to multiple places or even break consumers of the API that live outside your codebase.