Skip to main content

Infinity in the palm of your hand

1 hour 

fs2 is a library for functional streams. It gives us the fs2.Stream datatype and a huge set of operators for working with it.

But what is a functional stream? You might have heard that it’s like a list, but infinite. Instead of describing one, it’s easier to show you:

val kittens = Stream("Mao", "Popcorn")
// kittens: Stream[[x >: Nothing <: Any] => Pure[x], String] = Stream(..)

kittens is a pure fs2 stream, but it’s a pretty boring one. It looks very much like a list.

We need to run the stream to get a meaningful value. Suppose we’re only interested in the last element of kittens. We can run the stream to get that element by calling the .compile.last function.

kittens.compile.last
// res0: Option[String] = Some(value = "Popcorn")

We can transform streams in similar ways to lists. For instance, we can call take and drop on them.

val specialCat = kittens.take(1)
// specialCat: Stream[[x >: Nothing <: Any] => Pure[x], String] = Stream(..)
specialCat.compile.last
// res1: Option[String] = Some(value = "Mao")

"Mao" is indeed a very special cat.

If you’re familiar with the Scala standard library, you’ll know that we could have used List to do everything we’ve seen so far. But we can do many things with an fs2.Stream that we can’t do with a list. For instance, fs2 has a repeat operator:

val manyKittens = kittens.repeat
// manyKittens: Stream[[x >: Nothing <: Any] => Pure[x], String] = Stream(..)

"Popcorn" was the last element of the kittens stream. What is the last element of manyKittens?

manyKittens.compile.last

If you run this in your console, you’ll find that your program will take quite some time. More than enough time to brew a cup of tea, or go for a walk, or have a nap. You could leave it running for a week if you liked, or a year if you cared to because it would never finish. The manyKittens stream is infinite.

You might think that fs2 has a different mode of operation when it comes to infinity: maybe it switches the way it manipulates a finite vs an infinite stream. But we can use exactly the same operators on either sort of stream: take is still available to us, as are all the others.

We can use take to construct an enormous finite stream:

val notSoManyKittens = manyKittens.take(10000000)
// notSoManyKittens: Stream[[x >: Nothing <: Any] => Pure[x], String] = Stream(..)
notSoManyKittens.compile.last
// res2: Option[String] = Some(value = "Popcorn")

Unlike manyKittens, notSoManyKittens does finish after a cup of tea.