📜 ⬆️ ⬇️

Contracts in D

Good day, Habr!

Today I want to tell you about contract programming and its implementation in D. This is a very interesting concept of building an API. The point is in the formal specification of the function or class at the code level, not a comment.

An example of a similar specification for a function:

float foo( float value ) in //  ,  ,      { assert( 0 <= value && value <= 1, "value out of range [0,1]" ); //     "    " } out( result ) //            ,    void { assert( 0 <= result && result < 100, "bad result" ); //   : "  ,   ,     " } body //    (in,out)   body ,    { return value * 20 + sqrt( value ); } 

The code in the in and out blocks is a normal code, except that the restrictions of the nothrow modifier are not distributed to it, which is logical, since the meaning of the contract is to throw an exception if it is not followed.
')
It is important to understand that contracts do not replace the in-house verification of input data (inside the function body) and they should not affect the logic of work in any way (change any data), since they do not fall into release. This is an additional software verification tool, the “code documentation” of a specific API, so to speak. That is, the debug version of the program is run based on real data and, in case of non-compliance with any contract, terminates execution with a diagnostic error, which signals, first of all, about incorrect use of any API or incorrect algorithm, and not about an error in the input data. If there were no errors, then all calls go according to the contract specifications, and not the fact that the input data passed the test.

We dealt with the general moments, further more juicy: contracts and OOP.

Let's start with the simple:

 class Foo { float val; invariant { assert( val >= 0, "bad val" ); } } 

A new invariant keyword is added. The invariant-code block checks the state of an object 2 times - before and after calls to public and protected methods. The first time the invariant block is executed after the in block, before the method body, the second time immediately after the method body and before the out block. There are several invariant blocks, the order of the declaration does not matter. They can be used only within classes, structures, and unions (union); for interfaces, this is not allowed, as is the declaration of invariant blocks at the module level. The code of such blocks must use const methods to access the state of the object.

Calling private methods does not lead to invariant block calling, which is logical - inside public and protected methods, several private methods can be called that cause an object to be invalid, the main thing is that the state should become valid by the end of the calling public or protected method. Thus, the validity of the state of the object is always guaranteed when calling its methods from the outside, and the fact that it is done inside is implementation details, the object user will not be able to “catch the object” when it is in an invalid state between calls to private methods.

The invariant block is not called before the constructor is called, since an object that is waiting for construction is by definition not valid (otherwise there would be no need to write a constructor). The reverse point about the destructor is that the invariant is not called after it, which is also logical, since the object was not valid after the destruction. Also for constructors and destructors, the out block is forbidden (for lack of meaning).

During inheritance, invariant blocks of both the base and derived classes must be respected, that is, they must not be mutually exclusive. Also with out blocks for overridden methods: the result of the method operation must satisfy the contract of both the base and derived class.

The in contracts for redefined methods have a different logic, which for me personally was not at all obvious.

Example:

 import std.stdio; import std.string; void label(string f=__FUNCTION__,Args...)( Args args ){ writeln( f, ": ", args ); } class A { float calc( float val ) in { label; assert( val > 0 ); } out( res ) { assert( res > 0, format( "%f < 0", res ) ); label; } body { label; return val * 2; } } class B : A { override float calc( float val ) in { label; assert( val < 0 ); } body { label; return -val * 4; } } void main() { auto x = new B; x.calc( 10 ); } 

It can be assumed that such a code will fall down on checking the conditions in the in block of the overloaded function calc (B.calc), but we see a different picture:

 contract_in.A.calc.__require: //    in   contract_in.B.calc: //     calc   core.exception.AssertError@contract_in.d(10): -40.000000 < 0 //     out    

An in block for an overloaded method was not called at all! This is due to the fact that the in block expands the possible range of input options, that is, in the derived class inside in, we kind of write “and now I can also process this,” unlike other contracts (out and invariant), which are characterized with such a phrase: “this condition must also be fulfilled.” You can compare the behavior of the out and invariant block combinations with a logical evaluation of the AND operation (all must be true for the result to be true) and the in behavior with an OR operation (true must be at least one). Therefore, the in block for B.calc will be called only when the in block for A.calc has completed with the exception.

 ... x.calc( -10 ); ... 

 contract_in.A.calc.__require: //  in   -  contract_in.B.calc.__require: //  in   -  contract_in.B.calc: contract_in.A.calc.__ensure: //   -  

In fact, the code in the example above is not correct, it should be rewritten as follows:

 class B : A { override float calc( float val ) in { label; assert( val < 0 ); } //   body { label; if( val < 0 ) return -val * 4; //       (   ) else return super.calc( val ); //      } } 

Now when we call calc (10), we will see:

 contract_in.A.calc.__require: //  in   -  contract_in.B.calc: //       else    contract_in.A.calc.__require: //    in   contract_in.A.calc: //    contract_in.A.calc.__ensure: //    ,    contract_in.A.calc.__ensure: //    ,     (   out  ) 

If you specify a value that does not match any input contract (in this case, 0), the exception will be thrown from the in block of the derived class.

It seems that all aspects of contract programming in D that I know about. I hope you have learned something new.

I want to note that the embedded documentation system does not add the contract code to the documentation, only the signature of the function, unlike third-party documentation generation systems, for example hmod .

Source: https://habr.com/ru/post/260779/


All Articles