I’ve been busy since I last wrote about ranges. I have a lot of news to share, but in this post, I’m going to narrowly focus on a recent development that has me very excited. It’s a new feature that I’m calling range comprehensions, and they promise to greatly simplify the business of creating custom ranges.

List Comprehensions

If you’re familiar with list comprehensions from Haskell or Python, your ears might have pricked up when I said “range comprehensions.” List comprehensions give you a pithy way to generate new lists from existing ones, either by transforming thenm, filtering them, or combining them, or whatever. Here, for instance, is a Haskell program for generating the first 10 Pythagorean triples:

main = print (take 10 triples) triples = [(x, y, z) | z <- [1..] , x <- [1..z] , y <- [x..z] , x^2 + y^2 == z^2]

The way to read the triples line is this: generate a list of tuples (x, y, z) where z goes from 1 to infinity, x goes from 1 to z (inclusive), and y goes from x to z , but only yield those triples for which x^2 + y^2 == z^2 is true. The code then generates every combination of x , y , and z in the specified ranges in some order and filters it, yielding a list of the Pythagorean triples. Beautiful. Of particular interest is the fact that, since Haskell is lazy, there is no problem with a comprehension that generates an infinite list.

Back-Story

Back in October, I published a blog post about API design and std::getline in which I showed how a range-based interface is better than the existing one. My friend Bartosz Milewski commented that ranges are difficult to work with and challenged me to show the range-based equivalent of the above pithy Haskell program. I admit that at the time, I had no answer for Bartosz.

Recently, Bartosz published a blog post about just this problem. In his post, Bartosz describes some pretty simple results from category theory (if any category theory can be described as “simple”), and applies it to the problem of generating the Pythagorean triples lazily in C++. It’s a great post, and you should read it. Here, finally, was my answer. Although Bartosz’s code was terribly inefficient, somewhat difficult to reason about, and not formulated in terms of STL-ish concepts, I knew the direction I had to take.

Introducing Range Comprehensions

Without further ado, here is my solution to the Pythagorean triples problem:

using namespace ranges; // Lazy ranges for generating integer sequences auto const intsFrom = view::iota; auto const ints = [=](int i, int j) { return view::take(intsFrom(i), j-i+1); }; // Define an infinite range of all the Pythagorean // triples: auto triples = view::for_each(intsFrom(1), [](int z) { return view::for_each(ints(1, z), [=](int x) { return view::for_each(ints(x, z), [=](int y) { return yield_if(x*x + y*y == z*z, std::make_tuple(x, y, z)); }); }); }); // Display the first 10 triples for(auto triple : triples | view::take(10)) { std::cout << '(' << std::get<0>(triple) << ',' << std::get<1>(triple) << ',' << std::get<2>(triple) << ')' << '

'; }

Lines 4 and 5 define intsFrom and ints , which are lazy ranges for generating sequences of integers. Things don’t get interesting until line 12 with the definition of triples . That’s the range comprehension. It uses view::for_each and yield_if to define a lazy range of all the Pythagorean triples.

view::for_each

What is view::for_each ? Like std::for_each , it takes a range and function that operates on each element in that range. But view::for_each is lazy. It returns another range. The function that you pass to view::for_each must also return a range. Confused yet?

So many ranges, but what’s going on? Conceptually, it’s not that hard. Let’s say that you call view::for_each with the range {1,2,3} and a function f(x) that returns the range {x,x*x} . Then the resulting range will consist of the elements: {1,1,2,4,3,9} . See the pattern? The ranges returned by f all got flattened. Really, range flattening is all that’s going on.

Now look again at line 12 above:

auto triples = view::for_each(intsFrom(1), [](int z) { return view::for_each(ints(1, z), [=](int x) { return view::for_each(ints(x, z), [=](int y) { return yield_if(x*x + y*y == z*z, std::make_tuple(x, y, z)); }); }); });

For every integer z in the range 1 to infinity, we call view::for_each (which, recall, returns a flattened range). The inner view::for_each operates on all the integers x from 1 to z and invokes a lambda that captures z by value. That function returns the result of a third invocation of view::for_each . That innermost lambda, which finally has x , y , z , makes a call to a mysterious-looking function that is provocatively named yield_if . What’s that?

yield_if

The semantics of yield_if is to “inject” the tuple (x,y,z) into the resulting sequence, but only if is a Pythagorean triple. Sounds tricky, but it’s really very simple. Recall that the function passed to view::for_each must return a range. Therefore, yield_if must return a range. If the condition x*x + y*y == z*z is false, it returns an empty range. If it’s true, it returns a range with one element: (x,y,z) . Like I said, simple. There’s also a function called yield that unconditionally returns a single-element range.

Now that you know how it works, you can forget it. You can just use view::for_each and yield_if as if you were writing a stateful function that suspends itself when you call yield or yield_if , kind of like a coroutine. After all, I picked the name “yield” to evoke the yield keyword from C#. That keyword gives the function it appears in precisely those coroutine-ish semantics. What’s more, C# functions that have yield statements automatically implement C#’s IEnumerable interface. IEnumerable fills the same niche as the Iterable concept I’ve described in previous posts. That is, you can loop over the elements.

For instance, in C# you can do this (taken from Wikipedia):

// Method that takes an iterable input (possibly an // array) and returns all even numbers. public static IEnumerable<int> GetEven(IEnumerable<int> numbers) { foreach(int i in numbers) { if((i % 2) == 0) { yield return i; } } }

With range comprehensions, equivalent code looks like this:

auto GetEvens = view::for_each(numbers, [](int i) { return yield_if((i % 2) == 0, i); });

That’s darn near the same thing, and we don’t need any special keyword or compiler magic.

Performance

Ranges that return ranges that return ranges, oy vey. How horribly does it perform at runtime? As it turns out, not horribly at all, but a lot depends on how good your optimizer is.

I wrote a simple benchmark program that iterates over the first 3000 triples and does some trivial computation with them. I do this in two ways. One is with the range comprehension above, and the other is with this triply-nested for loop:

for(int z = 1;; ++z) { for(int x = 1; x <= z; ++x) { for(int y = x; y <= z; ++y) { if(x*x + y*y == z*z) { result += (x + y + z); ++found; if(found == 3000) goto done; } } } } done:

You would expect this solution to fly and the range-based one to crawl. But here are the numbers using a hot-off-the-presses gcc-4.9 with -O3 :

Raw loop: 2.2s Range comprehension: 2.3s

That’s it?! Yes, all that extra work being done by the range comprehension is totally transparent to the optimizer, which generates near-optimal code. Kind of amazing, isn’t it?

If, however, your compiler of choice is clang, I have some bad news for you. The range comprehension is (wait for it) 15 times slower. Dear god, that’s awful. I guess that goes to show that despite the astounding awesome-ness of clang in most respects, its optimizer still has some ways to go.

Summary

Haskell and Python have list comprehensions. C# has LINQ and yield . And now C++ has range comprehensions. It’s now pretty trivial to generate non-trivial sequences on the fly, lazily and efficiently, in a way that plays well with all the STL algorithms. Like I said, I’m pretty excited.

Acknowledgements

My deep thanks to Bartosz Milewski for getting me 90% of the way there. I couldn’t have done this without his insights, and the insights of all the functional programmers and category theoreticians that came before. Math FTW!