土耳其版《d编程》range 翻译2

hqs7636 2011-11-19
Special feature of strings by the std.array module
std.array 模块字符串特殊功能


Being character arrays by definition, strings can also be used as ranges just by importing std.array. The two exceptions are, char and wchar strings cannot be used as RandomAccessRange.
字符数组的定义是,字符串也可以作为 range ,只需要导入 std.array。有两个例外是,char 和 wchar 字符串不能作为 RandomAccessRange 使用。

std.array provides a special functionality with all types of strings: iterating over strings becomes iterating over Unicode code points, not over UTF code units. As a result, strings appear as ranges of Unicode characters.
std.array 提供了与所有现有字符串类型不同的特殊功能:字符串迭代变为迭代 Unicode 代码点(???)不超过 UTF 编码单元(???)。因此,字符串以 Unicode 字符 range 出现。

The following strings contain d and g, which cannot be represented by a single char and the gothic letter ahsa (#), which cannot be represented by a single wchar (note that these characters may not be displayed correctly in the environment that you are reading this chapter):
以下字符串包含 d 和 g,不能由一个单一的字符和哥特字母 ahsa (#)???表示,这不能代表一个单一 WCHAR (请注意,如果你正在阅读本章,这些字符可能在你的环境无法正确显示)(俺用#号代替了,译注):
import std.array;
// ...
    print("abcdefg #"c);
    print("abcdefg #"w);
    print("abcdefg #"d);

d g # 原是土耳其文,d 和 g 是我猜着像是英文里的 d g,# 是随便拿来替的,d 和 g 可以把他们理解为中文的全角字符,#可以理解为或某个汉字。在中文双字节编码环境里可能会有不同的输出。译注

The output of the program is what we would normally expect from a range of letters:
我们通常会期望该程序会输出一些字母 range :

  a b c d e f g  #
  a b c d e f g  #
  a b c d e f g  #

As you can see, that output does not match what we have seen in the Characters and Strings chapters. Since the elements of string and wstring are char and whar respectively, one might expect to see UTF code units in the previous output.
正如你可以看到,输出不匹配,我们在字符和字符串章节看到, 由于元素 string 和 wstring 是 char 和 wchar(原文是 whar ) 的分别,人们可能会看到在前面的输出是 UTF 编码码。

As a reminder, let's use the following function to see how different the use of strings as ranges is different from using them as arrays:
作为提醒,让我们用下面的函数,以了解不同的字符串 range 是个怎样不同的数组:
void printElements(T)(T str)
{
    for (int i = 0; i != str.length; ++i) {
        write(' ', str[i]);
    }
    writeln();
}
// ...
    printElements("abcdefg #"c);
    printElements("abcdefg #"w);
    printElements("abcdefg #"d);

Indexing provides direct access to the elements of the arrays:
索引提供了直接访问数组中的元素:

a b c # # e f # # # # # #
a b c d e f g ### ###
a b c d e f g #

以上小节里原来有些乱码,以至于贴不上,我用#号代替了,估计不太影响阅读,这样整片文章就完整了。请参照原文


Ranges without actual elements
range 没有实际元素

The elements of the School objects did actually exist; School.front returned a reference to an existing Student object.
该 School 对象的元素实际上并不存在; School.front 返回一个引用到现有的 Student 对象。

One of the power of ranges is the flexibility of not actually owning elements. front need not return an actual element of an actual container. The returned element can be calculated each time when popFront() is called, and can be used as the value that is returned by front.
range 的强大之一就是并不拥有实际元素而有的灵活性。 front 不需要返回一个容器中的实际元素。 当 popFront() 被调用,返回元素可以被推测出,并且 front 可以作为返回值使用 。

We have already seen a range without actual elements above: Since char and wchar cannot represent Unicode characters, the Unicode characters that appear as range elements cannot be actual elements of any char or wchar array. In the case of strings, front returns a dchar that is constructed by the corresponding UTF code units of arrays:
我们已经看到上面没有实际内容的 range:由于char和wchar不能代表 Unicode 字符,作为范围元素出现的 Unicode 字符,不能作为任何 char 或 wchar 数组的实际元素。在字符串的情况下,front 返回一个 dchar 这是构建相应的 UTF 码数组:
import std.array;

void main()
{
    dchar letter = "g".front; // The dchar that is returned by
                              // front is constructed from the
                              // two chars that represent g
                              //front 返回由两个 g 字符表示的 dchar
}
  g 原文是土耳其文,这里将之理解为中文全角g可能会有不同的输出,译注

Although the element type of the array is char, the return type of front above is dchar and that dchar is not an element of the array, but is made up of the elements of the array.
虽然数组的元素类型是 char ,front 返回类型是 dchar,而这 dchar 不是一个元素,而是一个数组(多个元素)。

Similarly, some ranges don't own any elements but are used to provide access to elements of other ranges. This is a solution to the problem of losing elements while iterating over School objects above. In order to preserve the elements of the actual School objects, a special InputRange can be used.
同样,一些 ranges 不拥有任何元素但用于提供访问其他 ranges 的元素。 这是一个解决问题的办法,迭代 School 对象并消除其中的元素。 为了保持 School 对象的实际元素,可以使用一个特殊的 InputRange 。

To see how this is done, let's define a new struct named StudentRange and move all of the range member functions from School to this new struct. Note that School itself is not a range anymore:
要了解如何做到这一点,让我们定义一个名称为 StudentRange 的新的 struct 并且从 School 内移出所有 range 成员函数到新的 struct。需要注意的是 School 本身不是一个 range 了:
struct School
{
    Student[] students;
}

struct StudentRange
{
    Student[] students;

    this(School school)
    {
        this.students = school.students;
    }

    @property bool empty()
    {
        return students.length == 0;
    }

    @property ref Student front()
    {
        return students[0];
    }

    void popFront()
    {
        students = students[1 .. $];
    }
}

The new range starts with a private slice that provides access to the students of School and consumes this private slice in popFront(). As a result, the actual slice in School is preserved:
新 range 开始一个 private slice 切片,提供访问 School 的 students 并且在 popFront() 里消耗这个 private slice 切片,因此,实际切片在 School 中被保留:
   
auto school = School( [ Student("Ebru", 1),
                            Student("Derya", 2) ,
                            Student("Damla", 3) ] );

    print(StudentRange(school));

    // The actual array is now preserved:
    assert(school.students.length == 3);

Note: Since all its work is dispatched to its member slice, StudentRange may not be seen as a good example of a range. In fact, assuming that students is an accessible member of School, the user code could have created a slice of School.students directly and could have used that slice as a range.
注:由于所有的工作被分派到其成员 slice 切片, StudentRange 可能不被视为一个很好的 range 例子。 事实上,假设 students 是 School 的可访问成员,用户代码可以创建一个 School.students 切片,并可直接作为该 range 的切片使用。

Infinite ranges
无限 ranges


Another benefit of ranges without actual elements is the ability to create infinite ranges.
range 没有实际元素的另一个好处能够创建一个 infinite ranges (无限 range 。)

Making an infinite range is as simple as having empty always return false. Since it is constant, empty need not even be a function and is defined as an enum value:
构造一个 infinite range 它总是为空并返回 flase 。由于它是不变的, empty 甚至不需要是一个函数,仅作为 enum 枚举值定义:
   
enum empty = false;                   // ← infinite range
 无限 ranges 

Another option is to use an immutable static member:
另一种选择是使用一个 immutable static 成员(不变的静态成员)
   
static immutable bool empty = false;  // same as above /同上 


As an example of this, let's design a range that represents the Fibonacci series. Despite having only two int members, the following range can be used as the infinite Fibonacci series:
作为一个例子,让我们设计一个 range ,表示斐波纳契数列(或序列)。尽管只有两个 int 成员,下面的 range 可以作为无限 斐波纳契数列(或序列):
struct FibonacciSeries
{
    int first = 0;
    int second = 1;

    enum empty = false;   // ← infinite range 无限 ranges 

    @property int front() const
    {
        return first;
    }

    void popFront()
    {
        int third = first + second;
        first = second;
        second = third;
    }
}


Note: Although it is infinite, because the members are of type int, the elements of this Fibonacci series would be wrong beyond int.max.
注:虽然它是无限的,因为其成员类型是 int,斐波纳契数列的元素将会超过 int.max ,这是个错误。

Since empty is always false for FibonacciSeries objects, the for loop in print() never terminates for them:
由于 FibonacciSeries 对象的 empty 始终是 false , print() 中的循环永远不会终止:
   
print(FibonacciSeries());    // never terminates 永远不会终止


An infinite range is useful when the range need not be consumed completely right away. We will see how to use only some of the elements of a FibonacciSeries below.
当一个 range 不需要马上完全消耗完, infinite range 是非常有用的。我们将看到如何使用只有部分元素的 FibonacciSeries。

Functions that return ranges
函数返回 ranges (返回 ranges 的函数)

Earlier, we have created a StudentRange object by explicitly writing StudentRange(school).
此前,我们通过 StudentRange(school) 已经显示地创建了一个 StudentRange 对象 。

In most cases, a convenience function that returns the object of such a range is used instead. For example, a function with the whole purpose of returning a StudentRange would simplify the code:
在大多数情况下,用一个 range 来代替返回对象是一项很方便的功能。下面的例子,为简化代码,整个函数就返回一个 StudentRange :
StudentRange students_of(ref School school)
{
    return StudentRange(school);
}

// ...

    print(students_of(school));

This is a convenience over having to remember and spell out the names of range types explicitly, which can get quite complicated in practice.
一个可以方便记忆且明确阐明 range 类型的名称,在实践中可以避免很多麻烦。

We can see an example of this with the simple std.range.take function. take() is a function that provides access to a specified number of elements of a range, from the beginning. In reality, this functionality is not achieved by the take() function itself, but by a special range object that it returns. This fact need not be explicit when using take():
我们可以看到 std.range.take 函数的一个简单例子。take() 是一个函数,一开始提供了从 range 访问指定数目的元素,实际上,这个函数没有实现 take() 函数本身,而是由一个特殊的范围返回它。实际上不需要明确的使用 take() :
import std.range;

// ...

    auto school = School( [ Student("Ebru", 1),
                            Student("Derya", 2) ,
                            Student("Damla", 3) ] );

    print(take(students_of(school), 2));

take() returns a temporary range object above, which provides access to the first 2 elements of school. In turn, print() uses that object and produces the following output:
上述take() 返回一个临时 range 对象,它提供了访问 school 的第2个元素。反过来, print() 使用对象,并产生如下输出:

Ebru(1) Derya(2)

The operations above still don't make any changes to school; it still has 3 elements:
上述操作对 school 不会作任何改变,它仍然有3个元素:
   
print(take(students_of(school), 2));
    assert(school.students.length == 3);

The specific types of the range objects that are returned by functions like take() are not important. These types may sometimes be exposed in error messages, or we can print them ourselves with the help of typeof and stringof:
函数返回特定的 range 对象,像 take() 并不重要。这些类型有时可能会显示错误消息,或者我们也可以打印出来, typeof 和 stringof 可以帮助我们:
   
writeln(typeof(take(students_of(school), 2)).stringof);

According to the output, take() returns a template named Take:
据输出,take() 返回一个模板名称:

Take!(StudentRange)

std.range and std.algorithm modules
std.range and std.algorithm 模块

A great benefit of defining our types as ranges is being able to use them not only with our own functions, but with Phobos and other libraries as well.
定义我们的 ranges 类型是大有裨益的  不仅我们自己的函数,而且 Phobos 和其他的库也能够使用他们。

std.range includes a large number of range functions, structs, and classes. std.algorithm includes many algorithms that are commonly found in the standard libraries languages.
std.range 包含了大量函数、structs 和 classes。std.algorithm 包含许多通常在各语言标准库中的算法。

To see an example of how our types can be used with standard modules, let's use School with the std.algorithm.swapFront algorithm. swapFront() swaps the first elements of two InputRange ranges. (More correctly, first elements of two ranges that have swappable elements. Arrays satisfy this condition.)
来看一个例子,标准(库)模块如何使用我们的(range)类型,让 std.algorithm.swapFront 算法使用我们的 School 。swapFront() 交换两个InputRange 的第一个元素。(更准确的说是,两个 range 的第一个元素互换。数组满足这个条件。)
import std.algorithm;

// ...

    auto turkishSchool = School( [ Student("Ebru", 1),
                                   Student("Derya", 2) ,
                                   Student("Damla", 3) ] );

    auto americanSchool = School( [ Student("Mary", 10),
                                    Student("Jane", 20) ] );

    swapFront(students_of(turkishSchool),
              students_of(americanSchool));

    print(students_of(turkishSchool));
    print(students_of(americanSchool));


The first elements of the two schools are swapped:
两个 schools 的第一个元素交换:

Mary(10) Derya(2) Damla(3)
Ebru(1) Jane(20)

As another example, let's now look at the std.algorithm.filter algorithm. filter() returns a special range that filters out elements that do not satisfy a specific condition (a predicate). The operation of filtering out the elements only effects accessing the elements; the original range is preserved.
另外一个例子,让我们现在看看std.algorithm.filter算法,过滤器filter() 过滤掉不符合特定条件的元素后并返回这个特殊的 range (一过滤谓词)。筛选出的元素仅影响被访问的元素,原来的 range 被保留。

Predicates are expressions that must evaluate to true for the elements that are considered to satisfy a condition, and false for the elements that do not. There are a number of ways of specifying the predicate that filter() should use. As we have seen in earlier examples, one way is to use an expression in string form where the element is represented by the letter a:
元素满足一个条件,谓词表达式的结果必须为 true,否则为 false 。使用 filter()有许多方法指定谓词,正如我们在前面的例子中看到,一种方法是使用一个字符串形式的表达式,其中的元素是由字母 a 代表:

和元素虚假的元素没有。有许多的指定谓词多种方式 filter() 应。一个方法是使用一个字符串形式的表达,其中的元素是由英文字母代表一个:
   
filter!"a.number % 2"(students_of(school))

The predicate above selects the elements of the range students_of(school) that has an odd number.
谓词选择上述范围内的元素 students_of(school) ,有一个奇数。

Like take(), filter() too returns a special range object. That range object in turn can be passed to other range functions. For example, it can be passed to print():
像采取() ,过滤()返回一个特殊的范围对象了。这反过来范围对象的范围可以被传递到其他功能。例如,它可以传递给打印() :
   
print(filter!"a.number % 2"(students_of(school)));

That expression can be explained from right to left: start with the range students_of(school), construct a range object that will filter out the elements of that initial range, and pass the new range object to print().
该表达式可以解释从右至左:先从范围students_of(学校),构建一个Range对象,将筛选出该范围内的初始元素,并通过一系列新的对象打印() 。

The output consists of students with odd numbers:
输出包括与奇数的学生:

Ebru(1) Damla(3)

As long as it returns true for the elements that satisfy the condition, the predicate can also be specified as a function:
只要它返回正确的元素,满足条件,谓词也可以指定为一个函数:
import std.array;

// ...

    bool startsWithD(Student student)
    {
        return student.name.front == 'D';
    }

    print(filter!startsWithD(students_of(school)));

The predicate function above returns true for student having names starting with the letter D, and false for the others.
上述谓词函数返回true,如果学生因名以字母D开始,虚假的人。

Note: Using student.name[0] would have meant the first UTF-8 code unit, not the first letter. As I have mentioned above, front uses name as a range and always returns the first Unicode character.
注:使用student.name [0]将意味着第一个UTF - 8代码单元,请注意第一个字母。正如我上面提到的,前面使用的名称为一个范围,并且总是返回第一个Unicode字符。

This time the students whose names start with D are selected and printed:
这一次,学生的姓名与E开头的选择和打印:
Derya(2) Damla(3)

Laziness 惰态

Another benefit of functions' returning range objects is that, those objects can be used lazily. This may be essential for execution speed and memory consumption. Additionally, the fact that infinite ranges can exist at all is possible due to lazy ranges.

Lazy ranges produce their elements one at a time and only when needed. We see an example of this with the FibonacciSeries range: The elements are calculated by popFront() only as they are needed. If FibonacciSeries were an eager range and tried to produce all of the elements up front, it could never end or find room for the elements that it produced.

Another problem of eager ranges is the fact that they would have to spend time and space for elements that may have never going to be needed.

Like most of the algorithms in Phobos, take() and filter() benefit from laziness. For example, we can pass FibonacciSeries to take() and have it generate finite number of elements:

  
 print(take(FibonacciSeries(), 10));


Although FibonacciSeries is infinite, the output contains only the first 10 numbers:

0 1 1 2 3 5 8 13 21 34

ForwardRange

InputRange models a range where elements are taken out of the range as they are iterated over.

Some ranges are capable of saving their states, as well as operating as an InputRange. For example, FibonacciSeries objects can save their states because these objects can freely be copied and the two copies continue their lives independent from each other.

ForwardRange provides the save() member function, which is expected to return a copy of the range. The copy that save() returns must operate independently from the range object that it was copied from: iterating over one copy must not affect the other copy.

Importing std.array automatically makes arrays become ForwardRange ranges.

In order to implement save() for FibonacciSeries, we can simply return a copy of the object:

struct FibonacciSeries
{
// ...

    FibonacciSeries save() const
    {
        return this;
    }
}

The returned copy is a separate range that would continue from the point where it was copied from.

We can demonstrate that the copied object is independent from the actual range with the following program. The algorithm std.range.popFrontN() in the following code removes a specified number of elements from the specified range:

import std.range;

// ...

void report(T)(const dchar[] title, const ref T range)
{
    writefln("%40s: %s", title, take(range, 5));
}

void main()
{
    auto range = FibonacciSeries();
    report("Original range", range);

    popFrontN(range, 2);
    report("After removing two elements", range);

    auto theCopy = range.save();
    report("The copy", theCopy);

    popFrontN(range, 3);
    report("After removing three more elements", range);
    report("The copy", theCopy);
}

The output of the program shows that removing elements from the range does not affect its saved copy:

                          Original range: [0, 1, 1, 2, 3]
             After removing two elements: [1, 2, 3, 5, 8]
                                The copy: [1, 2, 3, 5, 8]
      After removing three more elements: [5, 8, 13, 21, 34]
                                The copy: [1, 2, 3, 5, 8]

Also note that the range is passed directly to writefln in report(). Like our print() function, the output functions of the stdio module can take InputRange objects. I will use stdio's output functions from now on.

An algorithm that works with ForwardRange is std.range.cycle. cycle() iterates over the elements of a range continuously repeatedly from the beginning to the end. In order to be able to start over from the beginning it must be able to save a copy of the initial state of the range. That's why it requires a ForwardRange.

Since FibonacciSeries is now a ForwardRange, we can try cycle() with a FibonacciSeries object; but in order to avoid having cycle() iterate over an infinite range, and as a result never finding the end of it, we must first make a finite range by passing FibonacciSeries through take():

    writeln(take(cycle(take(FibonacciSeries(), 5)), 20));

In order to make the resultant range finite as well, the range that is returned by cycle is also passed through take(). The output consists of the first twenty elements of cycling through the first five elements of FibonacciSeries:

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

We could have defined intermediate variables to make the code more readable. The following is an equivalent of the single line of code above:

    auto series                   = FibonacciSeries();
    auto firstPart                = take(series, 5);
    auto cycledThrough            = cycle(firstPart);
    auto firstPartOfCycledThrough = take(cycledThrough, 20);

    writeln(firstPartOfCycledThrough);

I would like to point out the importance of laziness again: The first four lines above merely construct the range objects that will eventually produce the elements. The numbers that are part of the result are calculated by FibonacciSeries.popFront() as needed.

Note: Although we have started with FibonacciSeries as ForwardRange, we have actually passed the result of take(FibonacciSeries()) to cycle(). take() is adaptive: the range that it returns is a ForwardRange if its parameter is a ForwardRange. We will see how this is accomplished with isForwardRange in the next chapter.
BidirectionalRange

BidirectionalRange provides two member functions over the member functions of ForwardRange. 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 arrays 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(retro([ 1, 2, 3 ]));

The output:

[3, 2, 1]

Let's define a range that behaves similarly to the special range that retro() returns. Although the following range is very limited, it shows how powerful ranges can be:

struct Reversed
{
    int[] range;

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

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

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

    @property 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]

RandomAccessRange

RandomAccessRange represents ranges that allow accessing element with the [] operator. As we have seen in the Operator Overloading chapter, [] operator is defined by the opIndex() member function.

Importing std.array module makes arrays become RandomAccessRange ranges only if possible. For example, since UTF-8 and UTF-16 encodings do not allow accessing Unicode characters by an index, char and wchar arrays cannot be used as RandomAccessRange ranges of Unicode characters. On the other hand, since the codes of the UTF-32 encoding correspond one-to-one to Unicode character codes, dchar arrays can be used as RandomAccessRange ranges of Unicode characters.

It is natural that every type would define the opIndex() member function according to its functionality. However, computer science has an expectation on its algorithmic complexity: random access must be in constant time. Constant time access means that the time spent when accessing an element is independent of the number of elements of the range; no matter how large the range is, element access should not depend on the length of the range.

In order to be considered a RandomAccessRange, one of the following conditions must also be satisfied:

    to be an infinite ForwardRange

or

    to be a BidirectionalRange that also provides the length property

Depending on the condition that is satisfied, the range is either infinite or finite.
Infinite RandomAccessRange

The following are all of the requirements of a RandomAccessRange that is based on an infinite ForwardRange:

    empty, front and popFront() that InputRange requires
    save() that ForwardRange requires
    opIndex() that RandomAccessRange requires
    the value of empty to be known at compile time as false

We were able to define FibonacciSeries as a ForwardRange. However, opIndex() cannot be implemented to operate in constant time for FibonacciSeries, because accessing an element requires accessing all of the previous elements first.

As an example where opIndex() can operate in constant time, let's define an infinite range that consists of the squares of integers. Although this range is infinite, accessing all of its elements can happen in constant time:

class SquaresRange
{
    int first;

    this(int first = 0)
    {
        this.first = first;
    }

    enum empty = false;

    @property int front() const
    {
        return opIndex(0);
    }

    void popFront()
    {
        ++first;
    }

    SquaresRange save() const
    {
        return new SquaresRange(first);
    }

    int opIndex(size_t index) const
    {
         /* This function operates in constant time */
        const integerValue = first + cast(int)index;
        return integerValue * integerValue;
    }
}

Although no space has been allocated for the elements of this range, the elements can be accessed with the [] operator:

    auto squares = new SquaresRange();

    writeln(squares[5]);
    writeln(squares[10]);

The output contains the elements at indexes 5 and 10:

25
100

The element with index 0 should always represent the first element of the range. We can take advantage of popFrontN() when testing whether this really is the case:

    popFrontN(squares, 5);
    writeln(squares[0]);

The first 5 elements of the range are 0, 1, 4, 9 and 16; the squares of 0, 1, 2, 3 and 4. After removing those, the square of the next value becomes the first element of the range:

25

Being a RandomAccessRange (the most functional range), SquaresRange can also be used as other types of ranges. For example as an InputRange:

    bool are_lastTwoDigitsSame(int value)
    {
        /* Must have at least two digits */
        if (value < 10) {
            return false;
        }

        /* Last two digits must be divisible by 11 */
        const lastTwoDigits = value % 100;
        return (lastTwoDigits % 11) == 0;
    }

    writeln(filter!are_lastTwoDigitsSame(take(squares, 50)));

The output consists of elements among the first 50, where last two digits are the same:

[100, 144, 400, 900, 1444, 1600]

Finite RandomAccessRange

。。。。。。。。。。。。
hqs7636 2011-11-19
Finite RandomAccessRange

The following are all of the requirements of a RandomAccessRange that is based on a finite BidirectionalRange:

    empty, front and popFront() that InputRange requires
    save() that ForwardRange requires
    back and popBack() that BidirectionalRange requires
    opIndex() that RandomAccessRange requires
    length, which provides the length of the range

As an example of a finite RandomAccessRange, let's define a range that works similarly to std.range.chain. chain() presents the elements of a number of separate ranges as if they are elements of a single larger range. Although chain() works with any type of element and any type of range, to keep the example short, let's implement a range that works only with int arrays.

Let's name this range as Together and expect the following behavior from it:

    auto range = Together([ 1, 2, 3 ], [ 101, 102, 103]);
    writeln(range[4]);

When constructed with the two separate arrays above, range should present all of those elements as a single range. For example, although neither array has an element at index 4, the element 102 should be the element that corresponds to index 4 of the collective range:

102

As expected, printing the entire range should contain all of the elements:

    writeln(range);

The output:

[1, 2, 3, 101, 102, 103]

Together will operate lazily: the elements will not be copied to a new larger array, they will be accessed from the original arrays.

We can take advantage of variadic functions that was introduced in the Parameter Flexibility chapter when initializing the range with unknown number of original arrays:

struct Together
{
    int[][] arrays;

    this(int[][] arrays ...)
    {
        this.arrays = arrays;

        clearFront();
        clearBack();
    }

// ...
}

The clearFront() and clearBack() calls that the constructor makes are to remove the empty arrays from the beginning and the end of the original arrays. Such empty arrays do not change the behavior of Together and removing them up front will simplify the implementation:

struct Together
{
// ...

    private void clearFront()
    {
        while (!arrays.empty && arrays.front.empty) {
            arrays.popFront();
        }
    }

    private void clearBack()
    {
        while (!arrays.empty && arrays.back.empty) {
            arrays.popBack();
        }
    }
}

We will call those functions later from popFront() and popBack() as well.

Since clearFront() and clearBack() remove all of the empty arrays from the beginning and the end; still having an array would mean that the collective range is not yet empty. In other words, the range should be considered empty only if there is no array left:

struct Together
{
// ...

    @property bool empty()
    {
        return arrays.empty;
    }
}

The first element of the first array is the first element of this Together range:

struct Together
{
// ...

    @property int front()
    {
        return arrays.front.front;
    }
}

Removing the first element of the first array removes the first element of this range as well. Since this operation may leave the first array empty, we must call clearFront() to remove that empty array and the ones that are after that one:

struct Together
{
// ...

    void popFront()
    {
        arrays.front.popFront();
        clearFront();
    }
}

A copy of this range can be constructed by a copy of the original arrays:

struct Together
{
// ...

    Together save()
    {
        return Together(arrays.dup);
    }
}

Please note that .dup copies only the arrays (as slices), not the elements that they contain.

The operations at the end of the range are similar to the ones at the beginning:

struct Together
{
// ...

    @property int back()
    {
        return arrays.back.back;
    }

    void popBack()
    {
        arrays.back.popBack();
        clearBack();
    }
}

The length of the range can be calculated as the sum of the length of the original ranges:

struct Together
{
// ...

    @property size_t length()
    {
        size_t totalLength = 0;

        foreach (array; arrays) {
            totalLength += array.length;
        }

        return totalLength;
    }
}

Alternatively, the length may be calculated with less code by taking advantage of std.algorithm.reduce. reduce() takes an operation as its template parameter and applies that operation to all of the elements of a range:

import std.algorithm;

// ...

    @property size_t length()
    {
        return reduce!"a + b.length"(size_t.init, arrays);
    }

The a in the template parameter represents the current result (the sum in this case) and b represents the current element. The first function parameter is the initial value of the result (size_t.init is 0) and the second function parameter is the range that contains the elements.

Note: Further, instead of calculating the length every time that length is called, it may be measurably faster to maintain a member variable perhaps named length_, which always equals the correct length of the collective range. That member may be calculated once in the constructor and modified accordingly as elements are removed by popFront() and popBack().

One way of returning the element that corresponds to a specific index is to look at every array to determine whether the element would be one the elements of that array:

struct Together
{
// ...

    int opIndex(size_t index)
    {
        /* Save the index for the error message */
        const size_t originalIndex = index;

        foreach (array; arrays) {
            if (array.length > index) {
                return array[index];

            } else {
                index -= array.length;
            }
        }

        throw new Exception(
            format("Invalid index: %s (length: %s)",
                   originalIndex, this.length));
    }
}

Note: This opIndex() does not satisfy the constant time requirement that has been mentioned above. For this implementation to be acceptably fast, the arrays member must not be too long.

This new range is now ready to be used with any number of int arrays. With the help of take() and array(), we can include even the range types that we have defined earlier in this chapter:

    auto range = Together(array(take(FibonacciSeries(), 10)),
                          [ 777, 888 ],
                          array(take(new SquaresRange(), 5)));

    writeln(range.save);

The elements of the three arrays are accessed as if they are elements of a single large array:

[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 777, 888, 0, 1, 4, 9, 16]

We can pass this range to other range algorithms. For example to retro(), which requires a BidirectionalRange:

    writeln(retro(range.save));

[16, 9, 4, 1, 0, 888, 777, 34, 21, 13, 8, 5, 3, 2, 1, 1, 0]

Of course you should use the more functional std.range.chain instead of Together in your programs.
OutputRange

All of the range types that we have seen so far are about element access. OutputRange represents streamed element output, similar to sending characters to stdout.

I have mentioned earlier that OutputRange requires support for the put(range, element) operation. put() is a function defined in the std.range module. It determines the capabilities of the range and the element at compile time and uses the most appropriate method to use to output the elements.

put() considers the following cases in the order that they are listed below, and applies the method for the first matching case. R represents the type of the range; range, a range object; E, the type of the element; and e an element of the range:
Case Considered Method Applied
R has a member function named put
and put can take an E as a parameter

range.put(e);

R has a member function named put
and put can take an E[] as a parameter

range.put([ e ]);

R is an InputRange
and e can be assigned to range.front

range.front = e;
range.popFront();

E is an InputRange
and can be copied to R

for (; !e.empty; e.popFront())
put(range, e.front);

R can take E as a parameter
(e.g. R could be a delegate)

range(e);

R can take E[] as a parameter
(e.g. R could be a delegate)

range([ e ]);

Let's define a range that matches the first case: The range will have a member function named put, which takes a parameter that matches the type of the output range.

This output range will be used to output the elements to multiple files, including stdout. When elements are outputted with put(), they will all be written to all of those files. As an additional functionality, let's add the ability to specify a delimiter to be written after each element.

struct MultiFile
{
    string delimiter;
    File[] files;

    this(string delimiter, string[] fileNames ...)
    {
        this.delimiter = delimiter;

        /* stdout is always included */
        this.files ~= stdout;

        /* A File object for each file name */
        foreach (fileName; fileNames) {
            this.files ~= File(fileName, "w");
        }
    }

    void put(T)(T element)
    {
        foreach (file; files) {
            file.write(element, delimiter);
        }
    }
}

In order to be used as an output range of any type of elements, put() is templatized on the element type.

An algorithm in Phobos that uses OutputRange is std.algorithm.copy. copy() is a very simple algorithm, which copies the elements of an InputRange to an OutputRange.

import std.algorithm;

// ...

    auto output = MultiFile("\n", "output_0", "output_1");
    copy([ 1, 2, 3], output);
    copy([ "red", "blue", "green" ], output);

That code outputs the elements of the input ranges both to stdout and to files named "output_0" and "output_1":

1
2
3
red
blue
green

Using arrays as OutputRange

The std.range module makes arrays OutputRange objects as well. (By contrast, std.array makes them only input ranges.) Unfortunately, using arrays as OutputRange objects has a confusing effect: arrays lose an element for each put() operation on them; and that element is the element that has just been outputted!

The reason for this behavior is a consequence of arrays' not having a put() member function. As a result, the third case of the previous table is matched for arrays and the following method is applied:

    range.front = e;
    range.popFront();

As the code above is executed for each put(), the front element of the array is assigned to the value of the outputted element, to be subsequently removed from the array with popFront():

import std.stdio;
import std.range;

void main()
{
    int[] array = [ 1, 2, 3 ];
    put(array, 100);
    writeln(array);
}

As a result, although the array is used as an OutputRange, it surprisingly loses elements:

[2, 3]

To avoid this, a slice of the array must be used as an OutputRange instead::

import std.stdio;
import std.range;

void main()
{
    int[] array = [ 1, 2, 3 ];
    int[] slice = array;

    put(slice, 100);

    writeln(slice);
    writeln(array);
}

This time the slice is consumed and the array has the expected elements:

[2, 3]
[100, 2, 3]    ← expected result

Another important fact is that the length of the array does not grow when used as an OutputRange. It is the programmer's responsibility to ensure that there is enough room in the array:

    int[] array = [ 1, 2, 3 ];
    int[] slice = array;

    foreach (i; 0 .. 4) {    // ← no room for 4 elements
        put(slice, i * 100);
    }

When the slice becomes completely empty because of the indirect popFront() calls, the program terminates with an exception:

core.exception.AssertError@...: Attempting to fetch the front
of an empty array of int

std.array.appender allows using arrays as an OutputRange where the elements are appended. The put() function of the special range object that appender() returns actually appends the elements to the original array:

import std.array;

// ...

    auto a = appender([ 1, 2, 3 ]);

    foreach (i; 0 .. 4) {
        a.put(i * 100);
    }

In the code above, appender is called with an array and returns a special range object. That range object is in turn used as an OutputRange by calling its put() member function. The resultant elements are accessed by its .data property:

    writeln(a.data);

The output:

[1, 2, 3, 0, 100, 200, 300]

Range templates

Although we have used mostly int ranges in this chapter, ranges and range algorithms are much more useful when defined as templates.

The std.range module includes many range templates. We will see these templates in the next chapter.
Summary

    Ranges is the feature that abstracts data structures from algorithms and allows them to be used seamlessly.
    Ranges are a D concept and are the basis for many features of Phobos.
    Many Phobos algorithms return lazy range objects to accomplish their special tasks.
    When used as InputRange objects, the elements of strings are Unicode characters.
    InputRange requires empty, front and popFront().
    ForwardRange additionally requires save().
    BidirectionalRange additionally requires back and popBack().
    Infinite RandomAccessRange requires opIndex() over ForwardRange.
    Finite RandomAccessRange requires opIndex() and length over BidirectionalRange.
    std.array.appender returns an OutputRange that appends to arrays.
Global site tag (gtag.js) - Google Analytics