Programming in D – Tutorial and Reference
Ali Çehreli

Other D Resources

More Ranges

We used mostly int ranges in the previous chapter. In practice, containers, algorithms, and ranges are almost always implemented as templates. The print() example in that chapter was a template as well:

void print(T)(T range) {
    // ...
}

What lacks from the implementation of print() is that even though it requires T to be a kind of InputRange, it does not formalize that requirement with a template constraint. (We have seen template constraints in the More Templates chapter.)

The std.range module contains templates that are useful both in template constraints and in static if statements.

Range kind templates

The group of templates with names starting with is determine whether a type satisfies the requirements of a certain kind of range. For example, isInputRange!T answers the question "is T an InputRange?" The following templates are for determining whether a type is of a specific general range kind:

Accordingly, the template constraint of print() can use isInputRange:

void print(T)(T range)
        if (isInputRange!T) {
    // ...
}

Unlike the others, isOutputRange takes two template parameters: The first one is a range type and the second one is an element type. It returns true if that range type allows outputting that element type. For example, the following constraint is for requiring that the range must be an OutputRange that accepts double elements:

void foo(T)(T range)
        if (isOutputRange!(T, double)) {
    // ...
}

When used in conjunction with static if, these constraints can determine the capabilities of user-defined ranges as well. For example, when a dependent range of a user-defined range is a ForwardRange, the user-defined range can take advantage of that fact and can provide the save() function as well.

Let's see this on a range that produces the negatives of the elements of an existing range (more accurately, the numeric complements of the elements). Let's start with just the InputRange functions:

struct Negative(T)
        if (isInputRange!T) {
    T range;

    bool empty() {
        return range.empty;
    }

    auto front() {
        return -range.front;
    }

    void popFront() {
        range.popFront();
    }
}

Note: As we will see below, the return type of front can be specified as ElementType!T as well.

The only functionality of this range is in the front function where it produces the negative of the front element of the original range.

As usual, the following is the convenience function that goes with that range:

Negative!T negative(T)(T range) {
    return Negative!T(range);
}

This range is ready to be used with e.g. FibonacciSeries that was defined in the previous chapter:

struct FibonacciSeries {
    int current = 0;
    int next = 1;

    enum empty = false;

    int front() const {
        return current;
    }

    void popFront() {
        const nextNext = current + next;
        current = next;
        next = nextNext;
    }

    FibonacciSeries save() const {
        return this;
    }
}

// ...

    writeln(FibonacciSeries().take(5).negative);

The output contains the negatives of the first five elements of the series:

[0, -1, -1, -2, -3]

Naturally, being just an InputRange, Negative cannot be used with algorithms like cycle() that require a ForwardRange:

    writeln(FibonacciSeries()
            .take(5)
            .negative
            .cycle        // ← compilation ERROR
            .take(10));

However, when the original range is already a ForwardRange, there is no reason for Negative not to provide the save() function as well. This condition can be determined by a static if statement and save() can be provided if the original range is a ForwardRange. In this case it is as trivial as returning a new Negative object that is constructed by a copy of the original range:

struct Negative(T)
        if (isInputRange!T) {
// ...

    static if (isForwardRange!T) {
        Negative save() {
            return Negative(range.save);
        }
    }
}

The addition of the new save() function makes Negative!FibonacciSeries a ForwardRange as well and the cycle() call can now be compiled:

    writeln(FibonacciSeries()
            .take(5)
            .negative
            .cycle        // ← now compiles
            .take(10));

The output of the entire expression can be described as take the first five elements of the Fibonacci series, take their negatives, cycle those indefinitely, and take the first ten of those elements:

[0, -1, -1, -2, -3, 0, -1, -1, -2, -3]

With the same approach, Negative can be made a BidirectionalRange and a RandomAccessRange if the original range supports those functionalities:

struct Negative(T)
        if (isInputRange!T) {
// ...

    static if (isBidirectionalRange!T) {
        auto back() {
            return -range.back;
        }

        void popBack() {
            range.popBack();
        }
    }

    static if (isRandomAccessRange!T) {
        auto opIndex(size_t index) {
            return -range[index];
        }
    }
}

For example, when it is used with a slice, the negative elements can be accessed by the [] operator:

    auto d = [ 1.5, 2.75 ];
    auto n = d.negative;
    writeln(n[1]);

The output:

-2.75
ElementType and ElementEncodingType

ElementType provides the types of the elements of the range.

For example, the following template constraint includes a requirement that is about the element type of the first range:

void foo(I1, I2, O)(I1 input1, I2 input2, O output)
        if (isInputRange!I1 &&
            isForwardRange!I2 &&
            isOutputRange!(O, ElementType!I1)) {
    // ...
}

The previous constraint can be described as if I1 is an InputRange and I2 is a ForwardRange and O is an OutputRange that accepts the element type of I1.

Since strings are always ranges of Unicode characters, regardless of their actual character types, they are always ranges of dchar, which means that even ElementType!string and ElementType!wstring are dchar. For that reason, when needed in a template, the actual UTF encoding type of a string range can be obtained by ElementEncodingType.

More range templates

The std.range module has many more range templates that can be used with D's other compile-time features. The following is a sampling:

The following example takes advantage of isInfinite to provide empty as an enum when the original range is infinite, making it known at compile time that Negative!T is infinite as well:

struct Negative(T)
        if (isInputRange!T) {
// ...

    static if (isInfinite!T) {
        // Negative!T is infinite as well
        enum empty = false;

    } else {
        bool empty() {
            return range.empty;
        }
    }

// ...
}

static assert( isInfinite!(Negative!FibonacciSeries));
static assert(!isInfinite!(int[]));
Run-time polymorphism with inputRangeObject() and outputRangeObject()

Being implemented mostly as templates, ranges exhibit compile-time polymorphism, which we have been taking advantage of in the examples of this chapter and previous chapters. (For differences between compile-time polymorphism and run-time polymorphism, see the "Compile-time polymorphism" section in the More Templates chapter.)

Compile-time polymorphism has to deal with the fact that every instantiation of a template is a different type. For example, the return type of the take() template is directly related to the original range:

    writeln(typeof([11, 22].negative.take(1)).stringof);
    writeln(typeof(FibonacciSeries().take(1)).stringof);

The output:

Take!(Negative!(int[]))
Take!(FibonacciSeries)

A natural consequence of this fact is that different range types cannot be assigned to each other. The following is an example of this incompatibility between two InputRange ranges:

    auto range = [11, 22].negative;
    // ... at a later point ...
    range = FibonacciSeries();    // ← compilation ERROR

As expected, the compilation error indicates that FibonacciSeries and Negative!(int[]) are not compatible:

Error: cannot implicitly convert expression (FibonacciSeries(0, 1))
of type FibonacciSeries to Negative!(int[])

However, although the actual types of the ranges are different, since they both are ranges of int, this incompatibility can be seen as an unnecessary limitation. From the usage point of view, since both ranges simply provide int elements, the actual mechanism that produces those elements should not be important.

Phobos helps with this issue by inputRangeObject() and outputRangeObject(). inputRangeObject() allows presenting ranges as a specific kind of range of specific types of elements. With its help, a range can be used e.g. as an InputRange of int elements, regardless of the actual type of the range.

inputRangeObject() is flexible enough to support all of the non-output ranges: InputRange, ForwardRange, BidirectionalRange, and RandomAccessRange. Because of that flexibility, the object that it returns cannot be defined by auto. The exact kind of range that is required must be specified explicitly:

    // Meaning "InputRange of ints":
    InputRange!int range = [11, 22].negative.inputRangeObject;

    // ... at a later point ...

    // The following assignment now compiles
    range = FibonacciSeries().inputRangeObject;

As another example, when the range needs to be used as a ForwardRange of int elements, its type must be specified explicitly as ForwardRange!int:

    ForwardRange!int range = [11, 22].negative.inputRangeObject;

    auto copy = range.save;

    range = FibonacciSeries().inputRangeObject;
    writeln(range.save.take(10));

The example calls save() just to prove that the ranges can indeed be used as ForwardRange ranges.

Similarly, outputRangeObject() works with OutputRange ranges and allows their use as an OutputRange that accepts specific types of elements.

Summary