On Fri, Sep 28, 2018 at 01:49:01PM +0100, Paul Moore wrote:
There's clearly a number of trade-offs going on here:
* Conditions should be short, to avoid clutter * Writing helper functions that are *only* used in conditions is more code to test or get wrong
This is no different from any other refactoring. If you have a function that checks its input: def spam(arg): if condition(arg) and other_condition(arg) or alt_condition(arg): raise ValueError and refactor it to a helper: def invalid(arg): return condition(arg) and other_condition(arg) or alt_condition(arg) def spam(arg): if invalid(arg): raise ValueError how is that a bad thing just because we call it a "precondition" instead of calling it "error checking"? Of course we don't necessarily want the proliferation of masses and masses of tiny helper functions, but nor should we fear them. Helpers should carry their own weight, and if they do, we should use them. Whether they are used for contracts or not makes zero difference.
* Sometimes it's just plain hard to express a verbal constraint in code
Indeed. People seem to be arguing against some strawman of "Design By Contract is a magic bullet that solves every imaginable problem". Of course it doesn't. Some constraints are too hard to specify as code. Okay, then don't do that. DbC isn't "all or nothing". If you can't write a contract for something, don't. You still get value from the contracts you do write. [...]
But given that *all* the examples I've seen of contracts have this issue (difficult to read expressions) I suspect the problem is inherent.
Are you specifically talking about *Python* examples? Or contracts in general? I don't know Eiffel very well, but I find this easy to read and understand (almost as easy as Python). The trickiest thing is the implicit "self". 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 https://www.eiffel.com/values/design-by-contract/introduction/ Here are a couple of examples from Cobra: def fraction( numer as int, denom as int) as float require numer > 0 denom <> 0 body ... def bumpState( incr as int) as int require incr > 0 ensure result >= incr .state = old.state + incr body .state += incr return .state http://cobra-language.com/trac/cobra/wiki/Contracts If you find them difficult to read, I don't know what to say :-)
Another thing that I haven't yet seen clearly explained. How do these contracts get *run*? Are they checked on every call to the function,
Yes, that's the point of them. In development they're always on. Every time you run your dev code, it tests itself.
even in production code?
That's up to you, but typical practice is to check pre-conditions (your input) but not post-conditions (your output) in production.
Is there a means to turn them off? What's the runtime overhead of a "turned off" contract (even if it's just an if-test per condition, that can still add up)?
Other languages may offer different options, but in Eiffel, contracts checking can be set to: no: assertions have no run-time effect. require: monitor preconditions only, on routine entry. ensure: preconditions on entry, postconditions on exit. invariant: same as ensure, plus class invariant on both entry and exit for qualified calls. all: same as invariant, plus check instructions, loop invariants and loop variants. You can set the checking level globally, or class-by-class. The default is to check only preconditions. That is, for methods to validate their inputs. Quoting from the Eiffel docs: When releasing the final version of a system, it is usually appropriate to turn off assertion monitoring, or bring it down to the ``require`` level. The exact policy depends on the circumstances; it is a trade off between efficiency considerations, the potential cost of mistakes, and how much the developers and quality assurance team trust the product. When developing the software, however, you should always assume -- to avoid loosening your guard -- that in the end monitoring will be turned off. https://www.eiffel.org/doc/eiffel/ET-_Design_by_Contract_%28tm%29%2C_Asserti... The intention for Python would be similar: - we ought to be able disable contract checking globally; - and preferrably on a case-by-case basis; - a disabled contact ought to be like a disabled assertion, that is, completely gone with no runtime effect at all; - but due to the nature of Python's execution model, there will probably be some (small) overhead at the time the function is created, but not when the function is called. Of course the overhead will depend on the implementation.
And what happens if a contract fails - is it an exception/traceback (which is often unacceptable in production code such as services)?
What happens when any piece of error checking code fails and raises an exception? In the case of Python, a failed contract would be an exception. What you do with the exception is up to you. The *intention* is that a failed contract is a bug, so what you *ought to do* is fix the bug. But you could catch it, retry the operation, restart the service, or whatever. That's worth repeating: *contracts aren't for testing end-user input* but for checking internal program state. A failed contract ought to be considered a bug. Any *expected error state* (such as a missing file, or bad user input, or a network outage) shouldn't be treated as a contract. -- Steve