Suggested Change to Contract Syntax

Jonathan M Davis via Digitalmars-d digitalmars-d at puremagic.com
Thu Mar 10 16:07:45 PST 2016


On Thursday, 10 March 2016 at 23:31:14 UTC, jmh530 wrote:
> On Thursday, 10 March 2016 at 22:57:41 UTC, Jonathan M Davis 
> wrote:
>> IMHO, at this point, inheritance is the only reason they're 
>> worth
>> having in the language. [snip]
>>
>
> I created a simple example to understand your point about 
> contracts only really mattering for inheritance, but my example 
> is giving assertion errors for the inherited class the same way 
> as the base class. What would I need to do for this issue to 
> become apparent?
>
> class A
> {
> 	int a;
> 	
> 	this(int x)
> 	{
> 		a = x;
> 	}
> 	
> 	int foo(int x)
> 	{
> 		assert(x != 0);
> 		scope(exit) assert((this.a - x) != 0);
>
> 		return this.a - x;
> 	}
> }
>
> class B : A
> {
> 	this()
> 	{
> 		super(4);
> 	}
>
> }
>
> void main()
> {
> 	import std.stdio : writeln;
> 	
> 	auto a = new A(2);
> 	//writeln(a.foo(0)); //causes assertion failure
> 	//writeln(a.foo(2)); //causes assertion failure
> 	
> 	auto b = new B();
> 	//writeln(b.foo(0)); //causes assertion failure
> 	//writeln(b.foo(4)); //causes assertion failure
> }

You're not using in or out contracts here at all, so so of 
course, you're not going to see how in/out contracts work with 
inheritance in this example. To quote 
http://dlang.org/spec/contracts.html:

============
If a function in a derived class overrides a function in its 
super class, then only one of the in contracts of the function 
and its base functions must be satisfied. Overriding functions 
then becomes a process of loosening the in contracts.

A function without an in contract means that any values of the 
function parameters are allowed. This implies that if any 
function in an inheritance hierarchy has no in contract, then in 
contracts on functions overriding it have no useful effect.

Conversely, all of the out contracts need to be satisfied, so 
overriding functions becomes a processes of tightening the out 
contracts.
============

So, it's essentially

assert(baseInContract || derivedInContract);

and

assert(baseOutContract && derivedOutContract);

And here is a totally contrived example:

============
import core.exception;
import std.exception;

class Base
{
     int foo(int i)
     in
     {
         assert(i > 0 && i < 10, "base in failed");
     }
     out(result)
     {
         assert(result % 2 == 0, "base out failed");
     }
     body
     {
         return i;
     }
}

class Derived : Base
{
     override int foo(int i)
     in
     {
         assert(i < 50, "derived in failed");
     }
     out(result)
     {
         // Combined with the Base.foo out contract, this ends up
         // being equivalent to assert(result % 6).
         assert(result % 3 == 0, "derived out failed");
     }
     body
     {
         return i;
     }
}

void main()
{
     Base base = new Base;
     Base derived = new Derived;

     assertNotThrown!AssertError(base.foo(4));
     assertNotThrown!AssertError(base.foo(6));
     assert(collectExceptionMsg!AssertError(base.foo(0)) == "base 
in failed");
     assert(collectExceptionMsg!AssertError(base.foo(12)) == "base 
in failed");
     assert(collectExceptionMsg!AssertError(base.foo(100)) == 
"base in failed");
     assert(collectExceptionMsg!AssertError(base.foo(5)) == "base 
out failed");
     assert(collectExceptionMsg!AssertError(base.foo(9)) == "base 
out failed");

     // Combining the out contracts of the Base and Derived make 
this fail for
     // Derived when it succeeded for Base alone.
     assert(collectExceptionMsg!AssertError(derived.foo(4)) == 
"derived out failed");

     assertNotThrown!AssertError(derived.foo(6)); // same as with 
Base
     assertNotThrown!AssertError(derived.foo(0)); // Derived 
allows <= 0
     assertNotThrown!AssertError(derived.foo(12)); // Derived 
alows >= 10 && < 50

     // Whether it's the Base or Derived that fails here is 
implementation defined,
     // since it fails for both.
     assert(collectExceptionMsg!AssertError(base.foo(100)) == 
"base in failed");

     // This fails the contracts of both Base and Derived
     assert(collectExceptionMsg!AssertError(derived.foo(5)) == 
"base out failed");

     // This passes Derived's contract, but it still doesn't pass 
Base's contract.
     assert(collectExceptionMsg!AssertError(derived.foo(9)) == 
"base out failed");
}
============

In order to get this same behavior without it being built into 
the language, all of the in contracts and out contracts from base 
classes would have to be repeated in the derived classes and be 
||ed or &&ed appropriately. It's feasible, but it's error-prone 
and not particularly maintainable. And considering how easily 
this subject tends to confuse folks and how hard it can be to get 
it right - especially with more complicated contracts - I 
seriously question that it's going to be done right except rarely 
if the programmer doesn't have help like we do by having the 
contracts built into the language in D.

- Jonathan M Davis


More information about the Digitalmars-d mailing list