The best way to do this is case-by-case. Why is it best? Well, consider the two examples you gave. In the first example, a * b + c
, an empty b
should act like zero. Whereas in your second case, max(a, b, c)
, an empty b
should NOT act like zero. So you want different behaviours.
The reality is that the “expected behaviour” in these cases is completely idiosyncratic, and if you gather ten people and ask them, you’ll get 11 opinions. For example, I would argue that in the a * b + c
case, an empty b
should act like one (the multiplicative identity), not zero. But arguing this is pointless, because different defaults makes sense in different situations.
Concretely, to handle it, you have two options:
- Encode the potential ‘emptiness’ into the type system. So, you could have
b::Union{Float32, Nothing}
, and then handle the nothing
case explicitly, like this:
y = isnothing(b) ? c : a * b + c
This solution is less bug-prone and can be analyzed with static analysers. Though it can be a bit verbose.
- Pick a sentinel value of the same type as
b
, and check it. For example, you could pick typemin(typeof(b))
if b isa Integer
, or NaN32
if b isa Float32
. Then you would do:
y = isnan(b) ? c : a * b + c
This is usually more computationally efficient, because b
now has a single concrete type, which enables more memory optimisations. However, this approach has a greater risk of bugs, because there is nothing in the type system that keeps you from forgetting that b
may be empty.
Edit: Julia does have a built-in type for this, namely missing
. But I strongly dislike it, and think missing
is a huge misfeature. The problem is - you guessed it - that the behaviour of missing
is usually inappropriate in a given situation, because the umbrella concept of ‘missing data’ is really 100 subtly different things that need to be handled differently, and assigning it the same type with the same default behaviour doesn’t make sense, and only serves to misguide the user. Furthermore, missing
is particularly poorly designed for two reasons:
- It propagates across functions, meaning it causes bugs to appear far from where they originated. This is bad for debugging errors.
- By design, methods with
missing
violates the contracts of the function. For example, the documentation of isodd
gives its signature as isodd(x::Number)::Bool
, and yet we have isodd(missing)::Missing
. This causes real life bugs, and also overly defense programming in response.
So don’t use missing
.