Programming in D – Tutorial and Reference
Ali Çehreli

Other D Resources

Contract Programming for Structs and Classes

Contract programming is very effective in reducing coding errors. We have seen two of the contract programming features earlier in the Contract Programming chapter: The in and out blocks ensure input and output contracts of functions.

Note: It is very important that you consider the guidelines under the "in blocks versus enforce checks" section of that chapter. The examples in this chapter are based on the assumption that problems with object and parameter consistencies are due to programmer errors. Otherwise, you should use enforce checks inside function bodies.

As a reminder, let's write a function that calculates the area of a triangle by Heron's formula. We will soon move the in and out blocks of this function to the constructor of a struct.

For this calculation to work correctly, the length of every side of the triangle must be greater than zero. Additionally, since it is impossible to have a triangle where one of the sides is greater than the sum of the other two, that condition must also be checked.

Once these input conditions are satisfied, the area of the triangle would be greater than zero. The following function ensures that all of these requirements are satisfied:

private import std.math;

double triangleArea(double a, double b, double c)
in {
    // Every side must be greated than zero
    assert(a > 0);
    assert(b > 0);
    assert(c > 0);

    // Every side must be less than the sum of the other two
    assert(a < (b + c));
    assert(b < (a + c));
    assert(c < (a + b));

} out (result) {
    assert(result > 0);

} do {
    immutable halfPerimeter = (a + b + c) / 2;

    return sqrt(halfPerimeter
                * (halfPerimeter - a)
                * (halfPerimeter - b)
                * (halfPerimeter - c));
}
Preconditions and postconditions for member functions

The in and out blocks can be used with member functions as well.

Let's convert the function above to a member function of a Triangle struct:

import std.stdio;
import std.math;

struct Triangle {
private:

    double a;
    double b;
    double c;

public:

    double area() const
    out (result) {
        assert(result > 0);

    } do {
        immutable halfPerimeter = (a + b + c) / 2;

        return sqrt(halfPerimeter
                    * (halfPerimeter - a)
                    * (halfPerimeter - b)
                    * (halfPerimeter - c));
    }
}

void main() {
    auto threeFourFive = Triangle(3, 4, 5);
    writeln(threeFourFive.area);
}

As the sides of the triangle are now member variables, the function does not take parameters anymore. That is why this function does not have an in block. Instead, it assumes that the members already have consistent values.

The consistency of objects can be ensured by the following features.

Preconditions and postconditions for object consistency

The member function above is written under the assumption that the members of the object already have consistent values. One way of ensuring that assumption is to define an in block for the constructor so that the objects are guaranteed to start their lives in consistent states:

struct Triangle {
// ...

    this(double a, double b, double c)
    in {
        // Every side must be greated than zero
        assert(a > 0);
        assert(b > 0);
        assert(c > 0);

        // Every side must be less than the sum of the other two
        assert(a < (b + c));
        assert(b < (a + c));
        assert(c < (a + b));

    } do {
        this.a = a;
        this.b = b;
        this.c = c;
    }

// ...
}

This prevents creating invalid Triangle objects at run time:

    auto negativeSide = Triangle(-1, 1, 1);
    auto sideTooLong = Triangle(1, 1, 10);

The in block of the constructor would prevent such invalid objects:

core.exception.AssertError@deneme.d: Assertion failure

Although an out block has not been defined for the constructor above, it is possible to define one to ensure the consistency of members right after construction.

invariant() blocks for object consistency

The in and out blocks of constructors guarantee that the objects start their lives in consistent states and the in and out blocks of member functions guarantee that those functions themselves work correctly.

However, these checks are not suitable for guaranteeing that the objects are always in consistent states. Repeating the out blocks for every member function would be excessive and error-prone.

The conditions that define the consistency and validity of an object are called the invariants of that object. For example, if there is a one-to-one correspondence between the orders and the invoices of a customer class, then an invariant of that class would be that the lengths of the order and invoice arrays would be equal. When that condition is not satisfied for any object, then the object would be in an inconsistent state.

As an example of an invariant, let's consider the School class from the Encapsulation and Protection Attributes chapter:

class School {
private:

    Student[] students;
    size_t femaleCount;
    size_t maleCount;

// ...
}

The objects of that class are consistent only if an invariant that involves its three members are satisfied. The length of the student array must be equal to the sum of the female and male students:

    assert(students.length == (femaleCount + maleCount));

If that condition is ever false, then there must be a bug in the implementation of this class.

invariant() blocks are for guaranteeing the invariants of user-defined types. invariant() blocks are defined inside the body of a struct or a class. They contain assert checks similar to in and out blocks:

class School {
private:

    Student[] students;
    size_t femaleCount;
    size_t maleCount;

    invariant() {
        assert(students.length == (femaleCount + maleCount));
    }

// ...
}

As needed, there can be more than one invariant() block in a user-defined type.

The invariant() blocks are executed automatically at the following times:

If an assert check inside an invariant() block fails, an AssertError is thrown. This ensures that the program does not continue executing with invalid objects.

As with in and out blocks, the checks inside invariant() blocks can be disabled by the -release command line option:

$ dmd deneme.d -w -release
Contract inheritance

Interface and class member functions can have in and out blocks as well. This allows an interface or a class to define preconditions for its derived types to depend on, as well as to define postconditions for its users to depend on. Derived types can define further in and out blocks for the overrides of those member functions. Overridden in blocks can loosen preconditions and overridden out blocks can offer more guarantees.

User code is commonly abstracted away from the derived types, written in a way to satisfy the preconditions of the topmost type in a hierarchy. The user code does not even know about the derived types. Since user code would be written for the contracts of an interface, it would not be acceptable for a derived type to put stricter preconditions on an overridden member function. However, the preconditions of a derived type can be more permissive than the preconditions of its superclass.

Upon entering a function, the in blocks are executed automatically from the topmost type to the bottom-most type in the hierarchy . If any in block succeeds without any assert failure, then the preconditions are considered to be fulfilled.

Similarly, derived types can define out blocks as well. Since postconditions are about guarantees that a function provides, the member functions of the derived type must observe the postconditions of its ancestors as well. On the other hand, it can provide additional guarantees.

Upon exiting a function, the out blocks are executed automatically from the topmost type to the bottom-most type. The function is considered to have fullfilled its postconditions only if all of the out blocks succeed.

The following artificial program demonstrates these features on an interface and a class. The class requires less from its callers while providing more guarantees:

interface Iface {
    int[] func(int[] a, int[] b)
    in {
        writeln("Iface.func.in");

        /* This interface member function requires that the
         * lengths of the two parameters are equal. */
        assert(a.length == b.length);

    } out (result) {
        writeln("Iface.func.out");

        /* This interface member function guarantees that the
         * result will have even number of elements.
         * (Note that an empty slice is considered to have
         * even number of elements.) */
        assert((result.length % 2) == 0);
    }
}

class Class : Iface {
    int[] func(int[] a, int[] b)
    in {
        writeln("Class.func.in");

        /* This class member function loosens the ancestor's
         * preconditions by allowing parameters with unequal
         * lengths as long as at least one of them is empty. */
        assert((a.length == b.length) ||
               (a.length == 0) ||
               (b.length == 0));

    } out (result) {
        writeln("Class.func.out");

        /* This class member function provides additional
         * guarantees: The result will not be empty and that
         * the first and the last elements will be equal. */
        assert((result.length != 0) &&
               (result[0] == result[$ - 1]));

    } do {
        writeln("Class.func.do");

        /* This is just an artificial implementation to
         * demonstrate how the 'in' and 'out' blocks are
         * executed. */

        int[] result;

        if (a.length == 0) {
            a = b;
        }

        if (b.length == 0) {
            b = a;
        }

        foreach (i; 0 .. a.length) {
            result ~= a[i];
            result ~= b[i];
        }

        result[0] = result[$ - 1] = 42;

        return result;
    }
}

import std.stdio;

void main() {
    auto c = new Class();

    /* Although the following call fails Iface's precondition,
     * it is accepted because it fulfills Class' precondition. */
    writeln(c.func([1, 2, 3], []));
}

The in block of Class is executed only because the parameters fail to satisfy the preconditions of Iface:

Iface.func.in
Class.func.in  ← would not be executed if Iface.func.in succeeded
Class.func.do
Iface.func.out
Class.func.out
[42, 1, 2, 2, 3, 42]
Summary