Illegal arguments come it great variety: the range is wider than what our programming languages' type systems can generally express. Our type systems are not general purpose constraint languages, and further, many conditions we might want to check for are dynamic, so must be checked at runtime.
Thus, in general, we still must check for and handle bad input, and, when the functions are properly documented, passing an illegal argument is the fault of the caller rather than the callee and throwing an exception is reasonable behavior.
However, that being said, fault does not necessarily determine how such issue is addressed, as it is reasonable from a technical perspective to fix either:
- the caller — to do what it takes to generate the proper, legal argument, or,
- the callee — to widen the interface, e.g. to allow and handle the (formerly illegal) argument
Your thought of changing the interface to use the type system to ensure the proper argument is super valid (especially in this case, but in general case of illegal arguments, not always possible to do). However, broadly speaking, that change will require making an engineering trade off in maintenance (and possibly performance) about changing all the callers, and against other possible approaches.
If most the callers are doing this,
[Collection of Elements] elements = [...]
[make sure the Collection only has unique elements]
Then you perhaps should provide a version of the interface to the algorithm that does the intermediate step (
make sure...) for them. This not only addresses the issue of (future) callers failing to honor the contract, but makes the code base more DRY.
To add to that, the algorithm might offer an interface having a number methods, with varying performance traded off against tolerance of argument. The version above that allows duplicates, and the version that doesn't (is well documented, and throws on violation) and let callers do what is natural for them. You might also include the Set version and see who uses it. It doesn't seem to me like a huge burden for the packaging of the algorithm to provide several such methods.
Can you consider a bug fixed when the underlying cause was not fixed?
Yes, if the immediate bug is fixed.
Engineering involves making trade-offs, some of which favor the short term situation and realities at the risk of technical debt that increases the burden of maintenance, cost of change, and correctness.
However, in part this goes to the notion of what the underlying cause actually is, which can be subjective. Was the underlying cause failure to create proper documentation? Failure to read/use the documentation? Failure to keep the code DRY? Failure to design a foolproof API?
You are right, of course, that whenever possible we should foolproof so the other programmers consuming our APIs (often they are us) fall into the "pit of success".
The module should should have only given unique elements to the algorithm, and as such the exception at that point is reported as a bug and should be fixed.
Should Fix #1 be rejected until it is implemented like Fix #2?
No, not necessarily. Fix #1 alone is a quick fix that accumulates a few grains of technical debt, but it addresses the bug in a valid way.
(To be clear I think there are better fixes, that are also cheap to implement.)
This short term decision (fix #1) can be partially mitigated, for example, by beefing up the documentation as this can usually be done at little expense. You should also code review (and/or test) other callers. Further, you can also add to your backlog a technical-debt refactor item to determine the costs (time & performance impact) to clean this up.