Skip to main content
  1. Teaching/
  2. teaching/
  3. teaching/

Project 3: Infinite Streams
#

The skeleton tarball for this project can be found here.

out of 100 points, 110 points hard cap

  1. TOC {:toc}

Hard work pays off later. Laziness pays off now! –Every Haskell tutorial ever

Description
#

In this lab, you will implement one of the most common data structures in functional languages: a lazy infinite list. You will also implement several transformers on this list, allowing you to do things like (potentially) have every prime number stored in a list, or generate a new Hamming number on-demand.

Note that this lab is unlike the others in that a lot of what you need to understand is on this project page and not in the headers. While it’s still important to read the headers, you should at least skim this first few sections of this document first, especially if you are unfamiliar with functional programming languages. 1

Overview of Streams
#

A stream is a special way to represent a sequence of data. It is special in the sense that the only way to access the elements of the sequence is to get an element from the front of the stream: imagine a linked-list where the only valid operation is to read the front of the list, whereupon that element disappears and everything shifts up.

An example of a network stream is a TCP connection: one must grab the packets one at a time, and the packets flow unidirectionally (due to the in-order delivery guarantees the TCP protocol provides). Reading files from the disk can also be thought of as reading from a stream of individual bytes or characters, and that is probably one important reason why the C++ I/O library is called iostream. Strictly speaking, the I/O operations that are available in most programming languages today are more than just streams, but we often think of them as streams (with a few bonus capabilities) because the abstraction is a useful one.

At first glance, streams might seem like a completely kneecapped data structure. What benefit do we get by implementing a sequence with a stream rather than an array or a vector? In a vector, we can access the 9999th element just by saying vector[9999]. In a stream, we have to do stream.take() 9998 times just to see the 9999th element. Even worse, if we don’t record the values of the first 9998 calls to take, the stream won’t hang on to them for us—those elements are gone for good.

It turns out the lack of random-access support is exactly where the power of stream lies2. The main idea is that if one cannot seek into the middle of a stream, there’s no point in actually storing the entire sequence. Only the first element of the sequence, which can be pulled directly by the user, needs to be stored. The rest of the stream can be generated on demand: the second element gets computed only after the first element is pulled out of the stream, and the third one gets computed only after the second one is pulled out, and so on. In other words, instead of computing and storing all elements of a sequence eagerly in advance, what streams allow us to do is to compute and store those elements lazily on-the-fly 3. If used correctly, stream can be a huge savings in both time and memory consumption.

Laziness also makes it possible to represent certain structures that are not representable with eager lists. One such example is the representation of infinite sequences: if you try to store a vector consisting of all natural numbers, your program will (rightfully) crash after attempting to allocate infinite memory. Even if you somehow had infinite memory, trying to store all the natural numbers would cause your program to immediately enter an infinite loop.

On the other hand, modelling infinite sequences with streams is not a problem because generating the elements of the sequences only happens when those elements are accessed. Of course, a stream will not prevent infinite loops from happening if the programmer tries to iterate over all elements in an infinite sequence. But most of the time what the programmer really cares is only a subset of elements in that sequence, and it is often cleaner to get them from a stream.

Consider how you would write a program to calculate the partial sums of the first n fibonacci numbers in C++. Now consider the elegance of the following solution in Haskell:

fibs = 1 : 1 : zipWith (+) fibs (tail fibs) --All fibonacci numbers
sumFirstFibs n = sum $ take n fibs

The first definition says that the list of all fibonacci numbers starts with [1,1] and is then generated by shifting the list and adding it to itself. Don’t worry if you don’t get it, but trust me when I say that it generates an (infinite) stream of fibonacci numbers. The second line simply says that to sum the first n fibonacci numbers, we take the first n fibonacci numbers from the front of the stream and add them all together. Simple, elegant, and quite easy to read once you know what’s going on.

In this assignment, you will implement a library of streams in C++. To make the library more usable, the stream class will be polymorphic: you can have a stream of any type, as long as the types are all the same. To do this, you will need to get comfy with C++ templates.

Directions and Hints
#

Download the skeleton project
#

Download the the assignment file project3.tar.gz to the lab computers (physically or using scp). Unpack the tarball:

> tar -xvf project3.tar.gz

This will create a new directory called project3 in the current directory with the tarball unpacked under it. Take a look at the skeleton code. There are several headers + code files, as well as a Makefile that encodes rules for how to compile the project. You can invoke the makefile by typing make in the project directory (though this will fail at first because the implementations are missing).

Note that this project will be slightly different from the others. Because we are using templated types, implementation will need to be done in header files. The only cpp file will be main.cpp. You will also need to add additional header files to complete this assignment–make sure to #include them in an appropriate location (often Stream.h, but occasionally elsewhere).

Finally, note that template errors are absolutely the worst thing to debug, and that heavily templated code like our Stream class often results in weird, inscrutable compiler errors. Don’t be afraid to post Piazza questions (though I will probably need you to post your code in a private question to help you).

What to write
#

You will need to implement the following functions–some of the functions may need new classes backing the implementation, but the last three should be doable with no additional classes (helper functions are useful though).

  • once()
  • chain()
  • take()
  • filter()
  • map()
  • prime()
  • hamming()
  • pi()

Overview of Code
#

Optional
#

{:.no_toc}

std::optional<T> was added into the C++ standard with C++17. It will form a core part of our Stream API, so let’s take a closer look at it. If you are familiar with other languages, this is sometimes referred to as an Option, a Maybe, or a Nullable.

Variables of type std::optional<T> may or may not hold a value of type T. The default constructor creates an std::optional<T> variable with no value in it. To put a value into an std::optional<T> variable, initialize it or assign it a value of type T. To test whether a variable holds a value or not, convert it into a boolean.

  std::optional<int> opInt;    // Create an optional with no value
  bool hasValue = opInt;    // hasValue is false
  opInt = 3;    // opInt now holds the value of 3
  bool hasValue2 = opInt;    // hasValue2 is true
  std::optional<std::string> opStr = "abc";    // Create an optional with string "abc"
  bool hasValue3 = opStr;    // hasValue3 is true

If a std::optional<T> variable holds a value, you can read that value using the dereferencing operator *. Be careful! You do not want to dereference any std::optional<T> variable that holds no value, as it behaves like null-pointer dereference and will result in undefined behavior.

  std::optional<int> opInt = 42;
  if (opInt) {
      int theInt = *opInt;
      // do something with theInt
  } else {
      // do NOT dereference opInt!
  }

In general, the std::optional<T> class is very useful for representing the result of an operation that may fail. For example, it can be the return type of a function that does some table lookup: if the element being looked up is not found in the table, an empty std::optional variable gets returned; otherwise, an std::optional variables that holds the lookup result gets returned. In our Stream, the next() function will return an empty std::optional variable if the stream is empty, and an std::optional variable that holds the next element if it is not empty.

Streams
#

{:.no_toc}

The Stream class is implemented as a handle class, i.e. it is a wrapper around a pointer to the real implementation of streams. Because manual memory management would be awful in these scenarios, we choose to use an std::shared_ptr instead of a raw pointer. This means that, in pretty much all cases, we do not need to worry about memory management: the streams will free themselves as appropriate.4

To represent a stream of any type, the Stream class is templated over an value type. That value type is the type of elements that can be pulled out from the stream. Users of the Stream class must specify what type of values they want the stream to hold. For example, Stream<int> represents a stream of integers, and Stream<std::string> represents a stream of strings.

The interface of our Stream class is very simple: it exposes only one member function, next(). This function models the “pulling” from a stream. It has two possible outcomes: either the stream is empty, in which case nothing gets pulled out, or the stream is not empty, in which case an item is pulled out and the stream updates itself. To represent both outcomes with one return value, we use std::optional<T>.

An empty stream
#

{:.no_toc}

The simplest stream one can have is a stream that has nothing in it: calling next() on the stream always produce an empty optional. Implementing such a stream is extremely easy and has already been done for you. Take a look at EmptyStreamImpl.h. The empty() function, which is defined in Stream.h, is a helper function that creates an empty stream directly:

  using namespace stream;
  auto s0 = empty<int>();  // Creates an empty stream of integer
  for (int i = 0; i < 100; ++i){
      assert(!s0.next());  // Invoking next() always yields empty value
  }

A stream of one value
#

{:.no_toc}

A slightly more sophisticated stream we can write is a singleton stream. The first time one calls next() on it, it yields a certain value, and all subsequent next() calls yield empty value. To create such a stream, use the helper function once():

  using namespace stream;
  // Creates a singleton stream that contains 42 only
  // Note that we do not need to specify the type of the stream,
  // as it can be deduced from the value we passed to the once() function
  auto s0 = once(42);  
  auto opInt = s0.next();  
  // The first element pulled from s0 holds 42
  assert(opInt && *opInt == 42);
  // Subsequent next() invocation always yield empty value
  for (int i = 0; i < 100; ++i){
      assert(!s0.next());
  }

The body of the once() function is left unfinished. Filling it in is part of the assignment.

Stream concatenation
#

{:.no_toc}

Now that we have two ways to create streams, one common task we can do with those created streams is to chain them together. The chain() function takes two streams s0, s1 that shares the same value type, and return a new stream. Pulling from this returned stream will first yield elements from s_0, and after s_0 is exhausted it starts to pull elements from s_1. For example,

  // Concatenate two singleton streams
  // Produce a stream with two elements
  auto s0 = chain(once(3), once(4));  
  auto first = s0.next();
  assert(first && *first == 3);  // Note: must check variable for nothing-ness!
  auto second = s0.next();
  assert(second && *second == 4);
  
  // No further values in the stream
  assert(!s0.next());

And of course, with once() and chain() we are able to build streams with arbitrary (but finite) length:

  // Produce a stream with three elements
  auto s0 = chain(once(1.1), chain(once(1.2), once(1.3)));
  
  // Produce a stream with size 100
  auto s1 = once(0);
  for (int i = 1; i < 99; ++i)
      s1 = chain(s1, once(i));

The body of the chain() function is left unfinished. Filling it in is part of the assignment.

An infinite stream (first attempt)
#

{:.no_toc}

Up until now, everything we’ve done with streams can also be achieved with arrays or vectors. What makes streams exceptional is the ability to build sequences of potentially infinite size.

The most elegant way of constructing an infinite stream makes heavy use of recursion. Here is an example:

  Stream<int> repeat() {
      return chain(once(1), repeat());
  }

My claim is that invoking the repeat() function should yield a stream from which one can pull out integer 1 indefinitely. Why?

The answer to the “why” is in the linked footnote. Before following it, think about it yourself for 60 seconds and see if you can come up with an answer.5

Unfortunately, at this point, we must invoke the famous computer scientist Donald Knuth, who once said: “Beware of bugs in the above code; I have only proved it correct, not tried it.” In fact, the analysis in the footnote is provably correct and mathematically sound, but if we try to compile and run the code, we get a segfault.

Delayed function call
#

{:.no_toc}

Why did our implementation of repeat() fail? Unfortunately, the problem lies not in our code, but in the C++ language. C++ does what’s called eager evaluation (sometimes called applicative-order). What this means is that a function’s arguments are evaluated before the function.

Let’s see what that means for our function:

  Stream<int> repeat() {
      return chain(once(1), repeat());
  }

When we call repeat(), we need to evaluate chain. To do that, we first need to evaluate the arguments to chain, which in this case, are once(1) and repeat(). Evaluating once(1) is pretty simple. To evaluate repeat(), we need to evaluate chain. To do that, we first need to evaluate the arguments to chain, which in this case, are once(1) and repeat(). Evaluating once(1) is pretty simple. To evaluate repeat(), we need to evaluate chain…..

…well hopefully you can see where this is going.

What we want is some way of telling C++ to delay the (inner) evaluation of repeat until it’s actually needed. For that, we’ve written a helper function called delay(). You pass it a function (usually a lambda, but callable objects) are also allowed) that, when evaluated, generates a new stream. Note that the delay() function itself makes no attempt to track the parameters of this input function–you need to do this yourself.

Due to its intricacy of implementation, the delay() function is given to you as part of the skeleton.

An infinite stream (second attempt)
#

{:.no_toc}

Armed with delay(), we are ready to give the correct implementation of the repeat() function:

  Stream<int> repeat() {
      return chain(once(1), delay(repeat));
  }

Since the repeat() function takes no argument, we could just pass its function pointer to delay(). For functions with non-empty parameter list, we need to use callables.

For example:

  Stream<int> counter(int a) {
      return chain(once(a), 
          delay([a] () { return counter(a + 1); } )
      );
  }

This creates a function that captures a at the time the function is created, then evaluates the function body at a later time. Note that your lambdas must always have no arguments, but they can capture whatever they need to.

This unit of deferred evaluation is referred to in many languages as a thunk. If you ask for help on this project, you’ll probably hear me talk about thunks a fair bit–it’s my preferred terminology. Just remember that in our case, a thunk is an unevaluated stream that is being protected by a delay().

Infinite Counters with delay()
#

{:.no_toc}

The counter() function takes an integer a as a parameter and returns a stream that contains an increasing sequence of integers, starting from a. Note that the lambda we passed to the delay() function needs to capture variable a by value. Another more interesting example is a function fib() that returns a stream of all fibonacci numbers:

  Stream<int> fibgen(int a, int b) {
      return chain(once(a), 
          delay([a, b] () { return fibgen(b, a + b); } )
      );
  }
  Stream<int> fib() { return fibgen(0, 1); }

Implement the counter() function in your code.

More helper functions
#

{:.no_toc}

The four functions we mentioned so far, namely empty(), once(), chain() and delay(), are all the basic building blocks we need to construct all kinds of sophisticated streams. However, to make our streams easier to work with, we want more helper functions:

take
#

{:.no_toc}

The take() function takes a stream s and an unsigned number n as parameters, and produce another stream of size n that contains only the first n elements of s.

  auto s0 = counter(1);  // s0 = { 1, 2, 3, ... }
  auto s1 = take(s0, 2);  // s1 = { 1, 2 }

filter
#

{:.no_toc}

The filter() function takes a stream s of type T and a callable f as parameters. The callable f should take a parameter of type T and return bool. The result of filter() is another stream that only contains elements in s on which f returns true.

  auto s0 = counter(1);  // s0 = { 1, 2, 3, ... }
  auto s1 = filter(s0, 
      // filter out all odd numbers
      [] (int num) { return num % 2 == 0; }
  );  // s1 = { 2, 4, 6, ... }

map
#

{:.no_toc}

The map() function takes a stream s of type S and a callable f as parameters. The callable f should take a parameter of type S and return a value of type T.

The result of map() is another stream of type T which contains the elements that are the results of calling f on the corresponding elements from stream s.

Conceptually, if stream s consists of {s0, s1, s2, ...}, then the output of map(s, f) should be {f(s0), f(s1), f(s2), ...}.

  auto s0 = counter(1);  // s0 = { 1, 2, 3, ... }
  auto s1 = map(s0, 
      // increase each element by 3
      [] (int num) { return num + 3; }
  );  // s1 = { 4, 5, 6, ... }

IMPORTANT: The return type of map does not have to match the input type, e.g. I can take in a Stream<string> and return a Stream<std::optional<int>>. Make sure you write a test for this, as the compiler cannot detect if your code is broken for S != T without a test case (see the Testing section for more details).

A stream of primes
#

Now that we have our basic stream library ready, it’s time to build some interesting streams!

Your next job in this assignment is to write a function prime() that returns a stream consisting of all prime numbers, in ascending order.

Of course, the easy way to do it is to take the stream counter(2) and filter out all numbers that are not prime:

  bool isPrime(int n) { ... }
  Stream<int> prime() {
      return filter(counter(2), isPrime);
  }

But constructing a prime stream this way is very inefficient: the prime test has to be carried out form every number, and for each number the isPrime() function has to conduct a division test on all potential divisors. A more efficient approach is to build this stream using Sieve of Eratosthenes.

We start from the stream counter(2). The first element in that stream is the first prime. To get the rest of the primes, filtering out the multiples of 2 from the rest of the stream. This leaves us with a stream beginning with 3, which is the next prime. To get the next prime, simply filter out the multiples of 3 from the rest of the stream. This leaves us with a stream beginning with 5, which is the next prime, and so on.

Your prime() function should look like this:

  Stream<size_t> sieve(Stream<size_t> s) { ... }
  Stream<size_t> prime() {
      return sieve(counter(2));
  }

The sieve() function should be a recursive function that performs the sieving procedure described above. It takes a stream s and use filter() to return another stream that contains no multiple of its first element.

To test whether your sieve() function is correct or not, use the following codes to print out the first 20 elements of the stream:

  auto p = take(prime(), 20);
  while (auto elem = p.next())
      std::cout << *elem << '\n';

NB: Attempting to use the Haskell-based definition of a fibonacci sequence presented earlier will not work in this project, because we have not implemented the zipWith function.

A stream of Hamming numbers
#

Your next job is to write a function hamming() that returns a stream of all hamming numbers, in ascending order.

Hamming numbers are numbers whose only prime divisors are 2, 3 and 5. Again, there is a rather inefficient way of computing the Hamming number stream:

  bool isHamming(int n) { ... }
  Stream<int> hamming() {
      return filter(counter(2), isHamming);
  }

But we could do better than this: notice that the Hamming number stream S has the following properties:

  • S begins with 1

  • Multiplying each element of S by 2, and the resulting elements are also in S

  • Multiplying each element of S by 3, and the resulting elements are also in S

  • Multiplying each element of S by 5, and the resulting elements are also in S

  • These are all elements in S

Suppose we have a helper function mergeUnique() that takes two increasing streams and combines the two into one increasing stream, eliminating repetitions. Then the hamming stream S can be implemented by concatenate the singleton stream { 1 } with the mergeUnique() of S multiplied by 2, S multiplied by 3, and S multiplied by 5. Multiplications of S can be easily implemented through the map() helper function.

mergeUnique() will be very similar to the standard merge function that is used in mergesort.

A stream of pi
#

Our final task is to write a stream of double that converge to $$\pi$$. The value of pi can be approximated with the following series:

$$ \pi = 4\times\sum_{i=1}^{\infty}\frac{(-1)^{i-1}}{2i - 1} = 4\times \left(1 - \frac{1}{3} + \frac{1}{5} - \frac{1}{7} + \frac{1}{9} - \cdots\right) $$

Write a function pi() that returns a stream, whose i-th element is the result of the above series cut from the i-th term. In other words, the first element in that stream is going to be $$4.0 = (4\times 1)$$, the second being $$ 2.66667 = (4\times(1-\frac{1}{3}))$$, the third one being $$ 3.46667 (4\times(1-\frac{1}{3} + \frac{1}{5}))$$, and so on.

To aid your programming, you probably want to write a helper function partialSum() that takes a stream {a0, a1, a2, ... } and return another stream {a0, a0+a1, a0+a1+a2, ... }.

Testing
#

Testing with header-based programs (like our Stream) is a very different beast from testing typical C++ code. In particular, because code is generated on-demand, the compiler sometimes cannot even typecheck simple header code. Consider the following code:

template <typename U, typename V, typename W>
W add(U u, V v){ return u + v; }

int main(void){
  add<int,int,int>(2,3);
}

Does this code work? Yes, it compiles and runs just fine. However, hopefully you can see that this will not work for all template types:

template <typename U, typename V, typename W>
W add(U u, V v){ return u + v; }

int main(void){
  add<int, std::string, void*>(3,"steve");
}

This little bundle of insanity will fail to compile, throwing up almost 100 lines of error output.

Note that the only change between these two snippets is how we used the template, not how the template was defined. This leads to a rather disturbing conclusion about template code: the compiler cannot help us typecheck our template code.

This means that when you do testing, you will need to be extra vigilant to suss out hidden type bugs. A common mistake is to implement map() for functions that do not change the type (e.g. f(int x){return x+1;}), and then forget to test it for functions that do change the type. When I try to compile against functions that do change the type (e.g f(int x){return x.to_string(); })), the program fails to compile, because the new types don’t work.

Another common error is to implement the stream using the same types for the stream type and an argument, e.g.

template <typename U>
some_stream_function(stream<U> s, U num_elements)

If you only test with U = int, this works just fine! But if we set U = string, suddenly num_elements has the type string, which is unlikely to end well.

I recommend testing all your generic stream functions (map,take, etc.) with at least three different types in the stream: int, std::string, and void*. This will likely catch most type incompatibilities. Make sure you write your own additional test cases–a failure to compile is still zero points, even if the failure to compile is because I called your function with types you didn’t think of.6

Finally, note that this is the first time we are dealing with explicit rvalues and lvalues in this class. You must make sure your code works appropriately with both types of values, where it makes sense.

For example, if the following code works:

  auto stream = map(counter(), [](int x){return x+1;});

then the following code should also work:

  auto addOne = [](int x){return x + 1;};
  auto stream = map(counter, addOne);

Having one or the other fail is definitely a bug, and should be addressed.

Submission and Grading
#

Make sure to read the general project grading rules.

You will submit a single tarball called project3.tar.gz which contains a folder inside called project3. I must be able to do the following in an automated manner:

  tar -xf project3.tar.gz
  cd project3
  make clean
  make
  ./stream

There are a few additional things to check for this project:

  • Make sure the only headers you include in main.cpp are Stream.h, <cassert>, and <iostream>. I will be testing your files with my own main functions, and they will only include these three files.

  • Make sure that you haven’t changed any existing function signatures. This is especially important for this project, as a changed function signature will lead to a failure to compile, which is a zero.

  • Do not compile header files.

If you have any questions about the assignment, please ask on Piazza.

Stretch Objectives
#

If you finish the project early and have time to burn, you might consider trying one of these stretch goals. They will give you small amounts of bonus points, capped at 15 points.

Fair warning: these stretch goals are not easy, and are more designed so that those of you who want to learn a little more about the weird fringes of C++ can cut your teeth. You will get many more points from implementing the main project well and testing it thoroughly than you will get from these bonuses.

If you attempt these, you must make sure your code still compiles on the lab machines and does not interfere with the correctness of the results. Make sure you document your changes in the README!

Stretch A: Folding (3pts)
#

As useful as map and filter are, they are not considered to be the foundations of working with lists in many functional languages. That honor goes to a family of functions known as the folds.

Implement the left fold (foldl) and right fold (foldr) in this code, then use them to re-implement map and filter. Hint: only one of these functions will work correctly on infinite streams. Choose carefully.

Stretch B: Pythagorean Triples (3pts)
#

In April of 2019, ranges were introduced to the C++ standard. Unfortunately, in the blog post announcing the feature, the author chose to demonstrate how you could use ranges to generate Pythagorean triples.

It turns out that ranges had some detractors, and they seized on the fact that you could write pythagorean triples without using ranges to conclude that the feature was unnecessary and represented bloat. For about two weeks, meaningful discussion about C++ and its ecosystem ground to a halt under the crushing weight of a bajillion blog posts and pointless nitpicking debates over the CoRrEcT™ way to write pythagorean triples code.

While we can no longer contribute to this debate (mostly because people realized it was dumb and decided to move on to better things), we can still show them up by writing not just a pythagorean triple generator, but an infinite pythagorean triple generator.

Write a stream that returns a list of all pythagorean triples. The stream should be ordered in the following sense: if a1 + b1 + c1 < a2 + b2 + c2, the the first triple should appear before the second triple in the stream.

You can represent a triple either as a custom class, or as an std::tuple.

Stretch C: Experimental Compiler Shenanigans (4pts)
#

One thing you might have noticed while implementing this project is that template errors, are, scientifically speaking, butts.

Part of the problem is due to a C++ rule called SFINAE, which, very roughly speaking, says that a template usage is not invalid unless all possible overloads are incorrect. This forces the compiler to try all the possible overloads of a function, and to print them out to aid in debugging.

C++ 20 was very recently standardized, and one of the features that was included in the standard is the concept. This allows us to place constraints on what types are allowed in a function–for example, we can say that only types that support the + operation are allowed in a sum function, or that only types that support being printed with cout are allowed in a custom output stream.

To do this stretch, you will need a compiler with experimental support for C++20. The latest version of g++ has this, available on the CS machines as g++-9. Newer versions of clang will also have this. Sometimes, C++20 support is under the name c++2a ,the name of the new standard before it was finalized.

Write a three stream functions that assume the ability to do certain things–for example, a partialSum() stream function (you may already have this from writing pi()), which assumes the ability to add elements, or a duplicate() stream which duplicates each element.7

You should also modify your implementations of map and filter to restrict the types that can be passed for F: right now, attempting to pass e.g. F = int will fail spectacularly, with messages going all over the place.

Record the difference between error messages when using/not using concepts, and demonstrate that your functions work correctly for that concept.



  1. Examples of such languages include Haskell, OCaml, Scala, or the Lisps ↩︎

  2. While this may be surprising at first, restricting the power of constructs is a time-honored technique for making more powerful abstractions. ↩︎

  3. This is why people with functional language background sometimes refer to streams as lazy lists↩︎

  4. Note that using shared_ptr means that we cannot have self-loops in our streams. To allow self-referencing, we would need to use a tracing garbage collector. If only we had one lying around somewhere… ↩︎

  5. Here is the reason: repeat() is constructed by concatenating a singleton stream { 1 } with another stream, which means that if you call next() on it for the first time, the singleton stream gets pulled and you will get 1 as a result. The second time next() is invoked, the singleton stream becomes exhausted and we start to pull out the first element from the second part of the concatenation. However, the second part is a repeat() function itself, whose first element is another 1. So we will get a 1 for the second time. Similarly, we will get another 1 the third time next() is invoked, and another 1 for the fourth time, and so on. ↩︎

  6. Fortunately, I’ll be compiling test cases separately this time, so you’ll only get a zero for that particular test case. Still, a zero on a test is something you should try to avoid. ↩︎

  7. Some elements cannot be safely duplicated: for example, if you are using a mutex to control exclusive access to a device, cloning the lock results in two locks, which allows two writers to access the device at once…not good! In C++, non-copyable elements can be implemented by deleting the copy constructor. ↩︎