Having used both stricter (Rust) and more loose programming languages (JS), I can add that stricter APIs have made me think more about edge cases than the less-strict APIs. In the less-strict languages I often find myself debugging some edge case, whereas in the stricter languages I am more often required to think about edge cases up front (including those that I know will not be relevant, so in some cases this causes more work than necessary!). Still I much prefer the stricter APIs.
I like Rusty Russell's classic "Hard to Misuse Interface Levels". The first couple of items are<p>0: Impossible to get wrong/DWIM<p>1: Compiler/linker won't let you get it wrong<p>...through<p>9: Read the correct LKML thread and you'll get it right - SET_MODULE_OWNER<p>...all the way up to 17. I'll let you discover the rest yourself :-)<p><a href="https://ozlabs.org/~rusty/ols-2003-keynote/img39.html" rel="nofollow">https://ozlabs.org/~rusty/ols-2003-keynote/img39.html</a>
I think that's a trick question. If "substring()" has "substring(int start, int end)" semantics, then I prefer the strict version: it must be 0 <= start < len(s), start <= end <= len(s), otherwise it'll throw an exception.<p>But if the semantics is "substring(int start, int length)", then I prefer the <i>partial</i> forgiveness <i>for the length</i> parameter: if start + length > len(s), then assume length was actually len(s) - start; but if length < 0, still throw an exception.
In my experience forgiving APIs don't play well together. I've inadvertently tried it and wind up having to make everything 'forgiving in the same way'. Otherwise out-of-range inputs mean different things to different functions, and using multiple APIs requires tracking all the forgiving behavior.
Its not always binary. You want to throw an error at even the slight amount of error. This will annoy users, but over time as bugs and edge cases are fixed - it will lead to a very robust program.<p>Meanwhile you want to save time/work for the caller. You dont want the caller to need many lines of boilerplate just to setup the call.
For reference, this is what String.substring(int, int) does[1] in Java:<p><pre><code> public String substring(int beginIndex, int endIndex) {
int length = length();
checkBoundsBeginEnd(beginIndex, endIndex, length);
...
</code></pre>
Where checkBoundsBeginEnd[2] does:<p><pre><code> static void checkBoundsBeginEnd(int begin, int end, int length) {
if (begin < 0 || begin > end || end > length) {
throw new StringIndexOutOfBoundsException(
"begin " + begin + ", end " + end + ", length " + length);
}
}
</code></pre>
(as it should, in my opinion).<p>[1] <a href="https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/f0ef2826d2116f4e0c0ed21f8d54fe9d0706504e/src/java.base/share/classes/java/lang/String.java#L1872-L1881" rel="nofollow">https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/f0ef2826d...</a><p>[2] <a href="https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/f0ef2826d2116f4e0c0ed21f8d54fe9d0706504e/src/java.base/share/classes/java/lang/String.java#L3317-L3322" rel="nofollow">https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/f0ef2826d...</a>
As someone who writes APIs for a living, this is often something interesting that I find a lot of division on:<p>Say you have an API that is documented to accept params A,B or C i.e /v1/api?A=1&B=2&C=3<p>What should happen if you pass a param D to it? i.e. /v1/api?A=1&B=2&C=3&D=4<p>The two most common schools of thought are:<p>1) Ignore D<p>2) Throw an error<p>Both present their own problems, esp. when D may be closely related to A,B,C. Interesting how API design also tends to side with personal preferences for strictness or leniency
This may be unrelated but every time I see a timestamp as a string in json I wonder why. There is so much less to go wrong with a numeric unix timestamp yet 9 times out of 10 a developer will use a string.<p>If you are going to make things strict at least make it hard to mess up.
Something I read in the RFC for email message format (or maybe elsewhere, but I have it associated with email for some reason) is: to be strict in output that you produce, and lenient in the input you accept.<p>I think it makes some sense for data formats, if you want max compatibility of independently developed software that processes the data. Not sure about APIs though. Having them fail fast on unexpected input is pretty valuable.
It seems like "forgiving with warnings" should be another option listed here. This also lets API creators do neat things like warn people ahead of time of future deprecations, etc.
If I had to choose I'd say prefer strict... But reading this made me wonder: what if you could be strict during development but switch to forgiving for deployment?
obligatory link to postels law[1] and criticism<p><a href="https://en.wikipedia.org/wiki/Robustness_principle" rel="nofollow">https://en.wikipedia.org/wiki/Robustness_principle</a>
> Suppose it's the early 1990's and you're James Gosling implementing String.substring(int, int) for the first time. What should happen when the index arguments are out-of-range? Should these tests pass? Or throw?<p>It depends entirely on what the rules of the API are. Function signatures in most languages lack any form of compiler enforcement of rules, so you must implement them in code, and then list the rules in the function's description. The strictness you apply doesn't matter as much as your description of what argument range is allowed, and how the behaviour is affected.<p>For example, substring could allow overshoot with the description "If the substring would go beyond the end of the string, the remainder of the string is returned".<p>What you should be concentrating on is the 80% use case of your API. What will 80% of your users need? If the lack of length overshoot support would be cumbersome to the 80%, you support overshoot. If it's useless to the 80%, you leave it out. You can also implement things as layered APIs, with more general lower level functions, and then higher level functions that are more strict. Then the 20% can use the lower level functions for their esoteric use cases, and the 80% can stick to your easy-to-use and hard-to-screw-up high level API.