One motivating example where lazy sequences enable you to separate your concerns and maximize code reuse — without increasing complexity.
Many people look at the Coroutines TS and wonder: what is all the fuss about?
This article gives you one motivating example where lazy sequences enable you to separate your concerns and maximize code reuse without increasing complexity.
Example: Approximating the Golden Ratio
Every Math student knows how to approximate the golden ratio via the Fibonacci series:
fib(n+1) / fib(n) -> φ = 1.6180…Or using C++:
double golden(double epsilon = 0.00001) {
int a = 0;
int b = 1;
double ratio = 0;
while (1) {
tie(a, b) = tuple{b, a+b};
auto delta = b / (double)a - ratio;
ratio = b / (double)a;
if (abs(delta) < epsilon)
return ratio;
}
}
int main() {
cout << golden() << std::endl;
}This is straightforward, but what if we have additional use-cases for golden?
- Maybe we want to limit the maximum number of iterations
- Or maybe we want to print after every iteration
- Or maybe we want to return a tuple of
{ fib(n+1), fib(n) } - Or maybe there's a use-case we haven't thought of yet
In order to satisfy all of these, we would need to modify the algorithm and provide customization points. We could add configuration options or use generic functions:
template<class TestF, class Map>
auto golden(TestF test, Map f) {
int a = 0;
int b = 1;
double ratio = 1;
while (1) {
tie(a, b) = tuple{b, a+b};
auto delta = b/(double)a - ratio;
ratio = b/(double)a;
if (test(delta, ratio, a, b))
return f(abs(delta), ratio, a, b);
}
}We can see the complexity increased as we generalized it. An alternative would be writing a variant for each specific use-case — readable but difficult to maintain.
How can we achieve reusability and simplicity?
This issue is caused by the fact that the algorithm controls both the iteration and the representation of the sequence. These concerns should be separated.
Inversion of Control via Coroutines TS
The Coroutines TS enables you to define lazy sequences. A lazy sequence is just an ordered set of values where each value is only computed as it is requested.
You might think of a lazy sequence as a container with two properties:
bool hasNext()— is there another value in this sequence?T takeNext()— return the next value in the sequence and advance by 1.
This decouples computation from representation while maintaining full control over the sequence.
The following examples use the Conduit library, which provides various primitives for creating lazy sequences built on the Coroutine TS.
We can define a lazy sequence for Fibonacci:
auto fib() -> seq<int> {
int a = 0;
int b = 1;
while (1) {
co_yield a;
tie(a, b) = tuple{b, a+b};
}
}Since the golden ratios are a transformation of the Fibonacci numbers, we can compute them using a scan:
auto fibRatios = [] {
return fib() >> scan(1, [](auto a, auto b){
return b / (double)a;
});
};Here we use the sequence to print the first 10 Fibonacci-ratios:
int main() {
int i = 0;
for (auto x : fibRatios()) {
cout << x << endl;
if (++i == 10) break;
}
}Now we can control the iteration ourselves — adding a delta check and an iteration limit without touching the algorithm:
int main() {
int n = 100;
auto prevRatio = 0.0;
for (auto ratio : fibRatios()) {
cout << ratio << endl;
auto delta = ratio - prevRatio;
if (n == 0 || delta < 0.0001) break;
prevRatio = ratio;
n--;
}
}The beauty is that we take just one definition of the sequence and use it in multiple places. Unlike the templated solution, the code still maps closely to the mathematical definition.
Conclusion
The Coroutines TS allows us to give control back to the consumer of an algorithm. This frees the implementer from providing customization hooks to the end-user to modify the behaviour of the algorithm.
Related
- Introducing Conduit: Lazy Sequences for C++ — A hands-on tour of the Conduit library with squares, Fibonacci, and compiler optimization examples.
- Conduit — Lazy Sequences for C++ — The Conduit project page with source and installation instructions.
- Either vs Error-Codes — More zero-overhead C++ abstractions: lifting errors into the type-system.
- value_ptr — The Missing C++ Smart-Pointer — Another missing abstraction from the C++ standard library.
Related Articles
Introducing Conduit: Lazy Sequences for C++
Conduit is a header-only library that leverages the Coroutine TS to introduce lazy sequences to C++, enabling concise infinite-sequence programming with zero-cost abstractions.
Either vs Error-Codes: Lifting Errors into the Type-System
Consider using an Either type to handle errors — they lift errors into the type-system and have the same performance characteristics as error-codes.
value_ptr — The Missing C++ Smart-Pointer
Use value_ptr to get value semantics on a heap resource. At the cost of some extra copying, your code will be simpler and easier to reason about.