ranges_more - D Programming Language

Menu

- Learn
- Documentation
  - Language Reference
  - Library Reference
  - Command-line Reference
  - Feature Overview
  - Articles
- Downloads
- Packages
- Community
  - Blog
  - Orgs using D
  - Twitter
  - Calendar
  - Discord (community-run)
  - Forums
  - IRC
  - Wiki
  - Issues
  - Contributing
  - Contributors
  - Foundation
  - Security Team
  - Donate
  - Sponsors
- Resources
  - Tour
  - Books
  - Tutorials
  - Tools
  - Editors
  - IDEs
  - run.dlang.io
  - Visual D
  - Acknowledgments
  - D Style
  - Glossary
  - Sitemap

Search
Report a bug
If you spot a problem with this page, click here to create a Bugzilla issue.
Improve this page
Quickly fork, edit online, and submit a pull request for this page. Requires a signed-in GitHub account. This works well for small changes. If you'd like to make larger changes you may want to consider using a local clone.

# ranges_more

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:

- `isInputRange`
- `isForwardRange`
- `isBidirectionalRange`
- `isRandomAccessRange`
- `isOutputRange`

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        // ← derleme HATASI
                .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:

- `isInfinite`: Whether the range is infinite
- `hasLength`: Whether the range has a `length` property
- `hasSlicing`: Whether the range supports slicing i.e. with `a[x..y]`
- `hasAssignableElements`: Whether the return type of `front` is assignable
- `hasSwappableElements`: Whether the elements of the range are swappable e.g. with `std.algorithm.swap`
- `hasMobileElements`: Whether the elements of the range are movable e.g. with `std.algorithm.move`

  This implies that the range has `moveFront()`, `moveBack()`, or `moveAt()`, depending on the actual kind of the range. Since moving elements is usually faster than copying them, depending on the result of `hasMobileElements` a range can provide faster operations by calling `move()`.

- `hasLvalueElements`: Whether the elements of the range are lvalues (roughly meaning that the elements are not copies of actual elements nor are temporary objects that are created on the fly)

  For example, `hasLvalueElements!FibonacciSeries` is `false` because the elements of `FibonacciSeries` do not exist as themselves; rather, they are copies of the member `current` that is returned by `front`. Similarly, `hasLvalueElements!(Negative!(int[]))` is `false` because although the `int` slice does have actual elements, the range that is represented by `Negative` does not provide access to those elements; rather, it returns copies that have the negative signs of the elements of the actual slice. Conversely, `hasLvalueElements!(int[])` is `true` because a slice provides access to actual elements of an array.

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();    // ← derleme HATASI

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

- The `std.range` module contains many useful range templates.
- Some of those templates allow templates be more capable depending on the capabilities of original ranges.
- `inputRangeObject()` and `outputRangeObject()` provide run-time polymorphism, allowing using different types of ranges as specific kinds of ranges of specific types of elements.

macros: TITLE=More Ranges DESCRIPTION=Many useful range templates of the std.range module. KEYWORDS=d programming language tutorial book ranges
Copyright © 1999-2026 by the D Language Foundation | Page generated by Ddoc on Wed Aug 5 15:38:52 2026