The gist if the question seems to be "How do I return two unrelated pieces of information from a method which returns a single int? I never want to check my return values, and nulls are bad, don't use them."
Let's look at what you are wanting to pass. You are passing either an int, or a non-int rationale for why you can't give the int. The question asserts that there will only be two reasons, but anyone who has ever made an enum knows that any list will grow. Scope to specify other rationales just makes sense.
Initially, then, this looks like it might be a good case for throwing an exception.
When you want to tell the caller something special which isn't in the return type, exceptions are often the appropriate system: exceptions are not just for error states, and allow you to return a lot of context and rationale to explain why you just can't int today.
And this is the ONLY system which allows you to return guaranteed-valid ints, and guarantee that every int operator and method that takes ints can accept the return value of this method without ever needing to check for invalid values like null, or magic values.
But exceptions are really only a valid solution if, as the name implies, this is an exceptional case, not the normal course of business.
And a try/catch and handler is just as much boilerplate as a null check, which was what was objected to in the first place.
And if the caller doesn't contain the try/catch, then the caller's caller has to, and so on up.
A naive second pass is to say "It's a measurement. Negative distance measurements are unlikely." So for some measurement Y, you can just have consts for
- -1=unknown,
- -2=impossible to measure,
- -3=refused to answer,
- -4=known but confidential,
- -5=varies depending on moon phase, see table 5a,
- -6=four-dimensional, measurements given in title,
- -7=file system read error,
- -8=reserved for future use,
- -9=square/cubic so Y is same as X,
- -10=is a monitor screen so not using X,Y measurements: use X as the screen diagonal,
- -11=wrote the measurements down on the back of a receipt and it was laundered into illegibility but I think it was either 5 or 17,
- -12=... you get the idea.
This is the way it is done in a lot of old C systems, and even in modern systems where there is a genuine constraint to int, and you can't wrap it to a struct or monad of some type.
If the measurements can be negative, then you just make your data type larger (eg long int) and have the magic values be higher than the range of the int, and ideally begin with some value that will show up clearly in a debugger.
There are good reasons to have them as a separate variable, rather than just having magic numbers, though. For example, strict typing, maintainability, and conforming to expectations.
In our third attempt, then, we look at cases where it is the normal course of business to have non-int values. For example, if a collection of these values may contain multiple non-integer entries. This means an exception handler may be the wrong approach.
In that case, it looks a good case for a structure which passes the int, and the rationale. Again, this rationale can just be a const like the above, but instead of holding both in the same int, you store them as distinct parts of a structure. Initially, we have the rule that if the rationale is set, the int will not be set. But we are no longer tied to this rule; we can provide rationales for valid numbers too, if needs be.
Either way, every time you call it, you still need boilerplate, to test the rationale to see if the int is valid, then pull out and use the int part if the rationale lets us.
This is where you need to investigate your reasoning behind "don't use null".
Like exceptions, null is meant to signify an exceptional state.
If a caller is calling this method and ignoring the "rationale" part of the structure completely, expecting a number without any error handling, and it gets a zero, then it'll handle the zero as a number, and be wrong. If it gets a magic number, it'll treat that as a number, and be wrong. But if it gets a null, it'll fall over, as it damn well should do.
So every time you call this method you must put in checks for its return value, however you handle the invalid values, whether in-band or out of band, try/catch, checking the struct for a "rationale" component, checking the int for a magic number, or checking an int for a null...
The alternative, to handle multiplication of an output which might contain an invalid int and a rationale like "My dog ate this measurement", is to overload the multiplication operator for that structure.
...And then overload every other operator in your application that might get applied to this data.
...And then overload all methods that might take ints.
...And all of those overloads will need to still contain checks for invalid ints, just so that you can treat the return type of this one method as if it were always a valid int at the point when you are calling it.
So the original premise is false in various ways:
- If you have invalid values, you can't avoid checking for those invalid values at any point in the code where you're handling the values.
- If you're returning anything other than an int, you're not
returning an int, so you can't treat it like an int. Operator overloading lets you pretend to, but that's just pretend.
- An int with magic numbers (including NULL, NAN, Inf...) is no longer really an int, it's a poor-man's struct.
- Avoiding nulls will not make code more robust, it will just hide
the problems with ints, or move them into a complex
exception-handling structure.
0
,[]
, or{}
(the scalar 0, the empty list, and the empty map, respectively). Also, that "missing"/"unknown" value is basically exactly whatnull
is for -- it represents that there could be an object there, but there isn't.