Bidirectional Range

BidirectionalRange provides two member functions over the member functions of ForwardRange: back and popBack. back is similar to front: it provides access to the last element of the range. popBack() is similar to popFront(): it removes the last element from the range.

Importing std.array automatically makes slices become BidirectionalRange ranges.

A good BidirectionalRange example is the std.range.retro function. retro() takes a BidirectionalRange and ties its front to back, and popFront() to popBack(). As a result, the original range is iterated over in reverse order:

writeln([ 1, 2, 3 ].retro);

The output:

[3, 2, 1]

Let’s define a range that behaves similarly to the special range that retro() returns. Although the following range has limited functionality, it shows how powerful ranges are:

import std.array;
import std.stdio;

struct Reversed
{
    int[] range;

    this(int[] range)
    {
        this.range = range;
    }

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

    int front() const
    {
        return range.back;  // ← reverse
    }

    int back() const
    {
        return range.front; // ← reverse
    }

    void popFront()
    {
        range.popBack();    // ← reverse
    }

    void popBack()
    {
        range.popFront();   // ← reverse
    }
}

void main()
{
    writeln(Reversed([ 1, 2, 3]));
}

The output is the same as retro():

[3, 2, 1]

Practice

Upgrade our LinkedList to a BidirectionalRange. A few notes:

  • a bidirectional range cannot be infinite, so you will have to transform LinkedList into a finite range.
  • you will need an extra pointer to save the address of the previous element so that you can iterate the range backwards. Alternatively, you can use a single pointer, however, that will be suboptimal from a time perspective.

Test this new functionality, by a adding a unittest that makes use of the isBidirectionalRange primitive.