Programming: The case for optionality
Ambiguity is a complexity

Suppose you are signing a contract with optional clauses. Would you care about them? Most of us would probably even bother reading them.
A contract is meant to enforce, to provide a guarantee. So by definition anything that is optional in it is irrelevant, and should just not be part of it.
Software programming is about writing contracts between clients calling code with required inputs and resulting service and output. Anything that is optional in those makes things fuzzy, complex to understand and to test.
Unspoken magic values
Actually optionality is about assigning values by default, when no explicit ones are provided. That way, it can be viewed as prototyping: you implicitly create the required set of values from a default one, and explicitly override some of the properties.
Let’s look at an example:
defaultOptions = { // Prototype
firstName: false,
lastName: true
}
formatPeople (people, options) {
options = {...defaultOptions, options}
const first = options.firstName ? people.firstName : ''
const last = options.lastName ? people.lastName : ''
formatted = `${first}${first && last ? ' ' : ''}${last}`
return formatted
}formatPeople (people1, {firstName: true})
Here a number of problems appear:
- the caller might not know which value is the default (a default that could even un-guessable if encapsulated or inlined in the code) and so not understand what that value implies. Actually the caller might not know the very existence of some option, as not constrained to provide it.In the example above, the caller might not know that
{gender: true}
can be specified or even thatlastName
can be unset. - contract becomes unclear as providing a value unset (unspecified key or key but empty value) can be interpreted in different ways: set the value to empty or use the default value. In the example above, specifying
{firstName: true}
could mean both concatenating both first and (default)lastName
, or just display thefirstName
. Similarly, providing a subset of values (i.e. a complementary subset of unset values) might be interpreted either as an override of all default values or just the ones from the subset. - you assume what should be the default, whereas it can depend on some context such as the use case. Maybe by making one client usage easier (by avoiding to provide a couple of arguments) you’re making another client usage harder. The truth is that you cannot decide and shouldn’t. Simplifying calls depending on the use case is a responsibility of the caller layer, not you.
Here is a better (more explicit, clearer and finally simpler) version:
formatPeople (people, options) {
const first = options.firstName ? people.firstName : ''
const last = options.lastName ? people.lastName : ''
return `${first}${first && last ? ' ' : ''}${last}`
}formatPeople (people1, {firstName: false, lastName: true})
The API that may do nothing
This one consist in executing the function code only if a parameter has a specific value (optionality here is a special case for value null
or undefined
):
function f (param?) {
if (param) { // Or if (!param) return
// All code
}
}
This is often used to avoid testing the value of a parameter at each call, but this can lead to a bunch of ambiguities:
f(x)
y = null
f(y) // How do I know if it won't do anything?
f() // Should not be allowed, but it is
All of this share the same drawback: you need to see the implementation to know how it will behave. Or read the docs, should you say, but checking the developer knows what (s)he is doing is not implemented by compilers yet.
To avoid all those drawbacks, just remove the optionality to make things clearer:
function f (param) {
// handle param
}if (x) {
f (x)
}
And, of course, you may even avoid the if (x)
check if your code flow (and so your compiler) knows that it cannot be the case.
Note that this can even get worse if the check can completely invalidates the building and convey of other parameters:
function f (param1, param2?) {
if (param2) {
// handle params
}
}x = buildX() // costly
y = undefined
f(x, y) // You built x for nothing
Signing two contracts at a time
Even now and then, I stumble on this type of code:
function f (param1, param2?) {
handleParam1(param1) // Could be at function end as well
if (param2) {
handleParam2(param2)
}
}
This has the following drawbacks for the function:
- the contract is more obscure. Notably, as for any function with optional parameter, this allows calling
f (p1)
without even knowing that ap2
was allowed and, more dangerously, without knowing what not havingp2
implies. - the implementation is more complex because of the case management (you can imagine more complex handling and more diverse cases values).
To refactor this, we have to identify that this is actually an attempt to implement two use cases in one function. The author probably found it “simple” as a “if” patch seemed easy to write, but the result is something that is not easy to read or even use. Always be wary about adding “ifs”.
So let’s write the two functions instead:
function f1 (param1) {
handleParam1(param1)
}function f2 (param1, param2) {
f1(param1)
handleParam2(param2)
}
Et voilà! Two clear contracts so you know which one suits your need and (provided you don’t evaluate simplicity through the number of lines), the code is now simpler:
- no more optionality
- no more case management
Conclusion
Optionality is just a shorthand for undefined values as default. It is easier to write at first but on the long run leads to unclear contracts and more complex code to handle all cases.
The examples depicted above show that simplicity is a local virtue in software engineering. It is not related to a global number of lines of even global number of functions, but rather to the clarity and unicity of contracts.