On Wed, Sep 26, 2018 at 04:03:16PM +0100, Rhodri James wrote:
Let's assume that the contracts are meaningful and useful (which I'm pretty sure won't be 100% true; some people are bound to assume that writing contracts means they don't have to think).
Contracts are a tool. We shouldn't refuse effective tools because some developers are too DailyWTF-worthy to use them. Why should the rest of us miss out because of their incompetence? Contracts are not rocket- science: there's nothing in them that most of us aren't already doing in an ad-hoc, clumsy manner by embedding the contracts in the docstring, inside the body of our methods, in external tests etc.
Assuming that you aren't doing some kind of wide-ranging static analysis (which doesn't seem to be what we're talking about), all that the contracts have bought you is the assurance that *this* invocation of the function with *these* parameters giving *this* result is what you expected. It does not say anything about the reliability of the function in general.
This is virtually the complete opposite of what contracts give us. What you are describing is the problem with *unit testing*, not contracts.
Unit tests only tell us that our function works with the specific input the test uses. In contrast, contracts test the function with *every* input the function is invoked with.
(Up to the point that you disable checking, of course. Which is under your control: you decide when you are satisfied that the software is sufficiently bug-free to risk turning off checking.)
Both are a form of testing, of course. As they say, tests can only reveal the presence of bugs, they can't prove the absence of bugs. But correctness checkers are out of scope for this discussion.
It seems to me that a lot of the DbC philosophy seems to assume that functions are complex black-boxes whose behaviours are difficult to grasp.
I can't imagine how you draw that conclusion. That's like saying that unit tests and documentation requires the assumption that functions are complex and difficult to grasp.
This introduction to DbC shows that contracts work with simple methods:
and here's an example:
put (x: ELEMENT; key: STRING) is -- Insert x so that it will be retrievable through key. require count <= capacity not key.empty do ... Some insertion algorithm ... ensure has (x) item (key) = x count = old count + 1 end
Two pre-conditions, and three post-conditions. That's hardly complex.
[Aside: and there's actually a bug in this. What if the key already exists? But this is from a tutorial, not production code. Cut them some slack.]
If I were writing this in Python, I'd write something like this:
def put(self, x, key): """Insert x so that it will be retrievable through key.""" # Input checks are pre-conditions! if self.count > capacity: raise DatabaseFullError if not key: raise ValueError # .. Some insertion algorithm ...
and then stick the post-conditions in a unit test, usually in a completely different file:
class InsertTests(TestCase): def test_return_result(self): db = SomeDatabase() db.put("value", "key") self.AssertTrue("value" in db.values()) self.AssertEqual(db["key"], "value") self.AssertEqual(db.count, 1)
Notice that my unit test is not actually checking at least one of the post-conditions, but a weaker, more specific version of it. The post-condition is that the count goes up by one on each insertion. My test only checks that the count is 1 after inserting into an empty database.
So what's wrong with the status quo?
- The pre-condition checks are embedded right there in the method implementation, mixing up the core algorithm with the associated error checking.
- Which in turn makes it hard to distinguish the checks from the implementation, and impossible to do so automatically.
- Half of the checks are very far away, in a separate file, assuming I even remembered or bothered to write the test.
- The post-conditions aren't checked unless I run my test suite, and then they only check the canned input in the test suite.
- The pre-conditions can't be easily disabled in production.
- No class invariants.
- Inheritance is not handled correctly.
The status quo is all so very ad-hoc and messy. Design By Contract syntax would allow (not force, allow!) us to add some structure to the code:
- requirements of the function - the implementation of the function - the promise made by the function
Most of us already think about these as three separate things, and document them as such. Our code should reflect the structure of how we think about the code.
In my experience this is very rarely true. Most functions I write are fairly short and easily grokked, even if they do complicated things. That's part of the skill of breaking a problem down, IMHO; if the function is long and horrible-looking, I've already got it wrong and no amount of protective scaffolding like DbC is going to help.
That's like saying that if a function is horrible-looking, then there's no point in writing tests for it.
I'm not saying that contracts are only for horrible functions, but horrible functions are the ones which probably benefit the most from specifying exactly what they promise to do, and checking on every invocation that they live up to that promise.
It's the reason why type checking exists,
Except Python doesn't type check so much as try operations and see if they work.
Python (the interpreter) does type checking. Any time you get a TypeError, that's a failed type check. And with type annotations, we can run a static type checker on our code too, which will catch many of these failures before we run the code.
Python code sometimes does type checking too, usually with isinstance. That's following the principle of Fail Fast, rather than waiting for some arbitrary exception deep inside the body of your function, you should fail early on bad input.