Creating a higher-order range

Analogous to a higher-order function that is a function that returns a function, a higher-order range is a range that returns another range. This technique allows the lazy evaluation of results and maximum propagation of range features (for example, random access or bidirectionality) for best efficiency and type safety as you compose algorithms. Phobos' std.algorithm module has several higher-order ranges. Here, we'll create a simplified version of std.algorithm.map to explore the implementation and usage of this flexible concept.

How to do it…

Let's create a higher-order range by executing the following steps:

  1. Make a struct template that stores another struct.
  2. Do your transformation in relevant places.
  3. Forward all possible methods of the input range via static if.
  4. Create a helper method to create it conveniently.
  5. Use a static assert to ensure it properly fulfils the interfaces.
  6. Try using the interfaces!

The code is as follows:

import std.range;
import std.traits;
struct OurMap(alias transformation, T)
    if(isInputRange!T)
{
    this(T range) {
        this.range = range;
    }

    T range;

    // forward infiniteness
    static if(isInfinite!T)
        enum bool empty = false;
    else
        @property bool empty() { return range.empty; }

    // forward the basic functions
    void popFront() { range.popFront(); }
    @property auto front() { return transformation(range.front); }

    // forward advanced functions, if available…
    static if(isForwardRange!T)
        @property auto save() {
            return OurMap!(transformation, T)(range.save);
        }

    static if(isBidirectionalRange!T) {
        @property auto back() {
            return transformation(range.back);
        }
        void popBack() {
            range.popBack();
        }
    }

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

    static if(hasLength!T)
        @property auto length() { return range.length; }
}

// check the basics with InputRange from std.range
static assert(isInputRange!(OurMap!((a) => a, InputRange!int)));
// check the forwarding with an array - a fully functional range
static assert(isRandomAccessRange!(OurMap!((a) => a, int[])));

// the helper function
auto map(alias transformation, T)(T t) {
    return OurMap!(transformation, T)(t);
}

void main() {
    import std.stdio;
    // try using it!
    foreach(item; map!((a) => a*2)([1,2,3]))
        writeln(item);
}

This will print the following output:

2 4 6

How it works…

Here, we created a struct template. It works just like a function template. We write a struct, add a list of compile-time parameters, we add a constraint (if desired), and then use the compile-time parameters at any point inside the definition.

One of our compile-time parameters, transformation, is listed as an alias parameter. The alias parameters in D take some other compile-time entity as argument. It might be a variable, a function, a template, or any other D symbol. Inside the template, when the alias parameter is used, it refers directly to the passed-in entity. Unlike a value parameter, which receives a copy of or reference to a variable's contents, an alias parameter refers to the variable itself.

Note

If you used reflection capabilities on an alias parameter, it would tell you about the original item. We use it here to provide maximum speed access to the function passed by the user. It isn't a function pointer, which would need to be dereferenced at run time; it is the function and is called directly by the generated code.

A higher-order range, in the same way as a range-consuming function, takes a range as input to its constructor and is constrained by the most general type you can handle. A higher-order range also is a range, so it has the same methods and properties as any other range.

The OurMap struct is a higher-order range that takes a transformation function and an input range and applies the transformation function to all the data within, upon request (lazily). Using static if, it presents the same interface as the inner range, ensuring that no capabilities are lost. The helper functions used in the static if, hasLength, isInfinite, and other functions are found in the std.range module. If we did a transformation that could no longer present part of an interface efficiently (filter, for example) we would leave the slower parts out because efficiency guarantees are an important part of the conceptual range interface. With filter, random access would be impossible at full speed, since it wouldn't know how many elements were filtered without looking at all of them. So, even if the input range offered random access, we couldn't present it in our range. OurMap, however, has all functions that could be forwarded without a loss of efficiency.

After the struct definition, we used two static asserts to test. When given an input range, we need to ensure our range is also an input range. As we can forward random access efficiently, we also ensured that our range correctly implemented a random access range when given an array (arrays provide random access).

Finally, the helper function is important here to make our range usable without hassle and thus has the presentable name of map. As functions allow implicit type deduction, the helper function saves the user the lengthy, repetitive boilerplate of listing the type of each range they pass to the function when calling the constructor. With the helper function, they can simply pass it a transformation and some data and it just works.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.42.134