You're Doing In-Conditions Wrong

FeepingCreature feepingcreature at gmail.com
Mon Jul 13 13:55:56 UTC 2020


Reposting my bug report 
https://issues.dlang.org/show_bug.cgi?id=20628 here to generate 
some discussion.

tl;dr: D is handling in-contracts in a way that causes bad 
outcomes both with and without debug mode, causing bizarre logic 
errors and even accepting invalid syntax.

---

Background:

An in-condition is part of D's implementation of Design by 
Contract. As such, their use on class methods acts as an 
extension of Liskov's Substitution Principle, which 
(generalizedly) states that class methods that override other 
methods may take more kinds of inputs, and return less kinds of 
outputs, than their parent method.

That is, a method that does not take 'null' values may be 
overridden by one that takes 'null' values, by the logic of "at 
least all parent's inputs are supported."

Reversedly, a method that may return 'null' values may be 
overridden by one that does not return 'null' values, by the 
inverse logic of "at most all parent's outputs are possible."

In other words, methods may, when inherited, *loosen* their input 
but *tighten* their output.

D lets us formalize this:

class A {
   Object method(Object obj)
   in (obj !is null)
   // no restriction on output
   ;
}

class B : A {
   override Object method(Object obj)
   // loosen restriction on input: null is allowed
   // tighten restriction on output: null is not allowed
   out (result; result !is null)
   ;
}

For inconditions, D implements this behavior as follows:

1. Check the superclass in-contract.
2. If the superclass contract throws an exception:
2.1. Then retry with the class's own in-contract.

This seems sensible. However, in practice it causes several 
problems.

Let's consider two cases: debug modes, where we want to look for 
and find logic errors, and non-debug mode, where we just want to 
run correctly.

Within debug mode, D should enforce that in contracts loosen the 
conditions. As such, it should always execute both superclass and 
subclass contract and Error if superclass-in passes but 
subclass-in does not.

In other words:

try {
   superMethod.runInCondition();
} catch (Exception) {
   // if this throws, all is well - the input is just not accepted.
   method.runInCondition();
   // if it didn't throw, all is well - the overridden in contract 
loosened the condition
   return;
}
// super method accepted this input - confirm that our logic holds
try {
   method.runInCondition();
} catch (Exception) {
   throw new LogicError("in contract was tightened in subclass - 
this is illegitimate");
}

But what to do outside debug mode? In my opinion, the reasonable 
thing to do is to only run the child contract.

Why? Well, consider the case that the parent contract is more 
tight than the child contract. In that case, the parent may throw 
an informative exception, but we don't care because we ignore it 
anyway; here, the parent's contract provides no information.

However, consider the case that the parent contract is more loose 
than the child contract. In that case, the child contract has 
*more* information than the parent. In the debug case, we want a 
LogicError in this scenario. However, the second-best thing is 
the exception that the child provides. Again, the parent contract 
has nothing to add, and we should just run the child contract.

Further benefits: this will also fix weirdnesses such as

interface I { void foo(); }
class C : I { void foo() in(this.is.never.compiled) { } }

or

interface I { void foo() in(true); }
class C : I { void foo() in(false) { } }

which would then be a compiletime error or runtime error, 
respectively.

Programming languages should do reasonable things. No reasonable 
programming language, when faced with this code,

void foo(int i)
in (i > 5)
{
   assert(i > 5, "this cannot happen");
}

should ever fail with "this cannot happen".



More information about the Digitalmars-d mailing list