Another method Stream
offers is Stream.grouped
, which divides a stream’s elements into groups of a given size.
To take an example, if you have a stream of twelve elements and call Stream.grouped
to turn it into groups of three, you’ll get an iterator over four sequences, each three elements in size:
val xs = (1 to 12).toStream val grouped = xs.grouped(3) // grouped: Iterator[Stream[Int]] = // Iterator( // Stream(1, 2, 3), Stream(4, 5, 6), // Stream(7, 8, 9), Stream(10, 11, 12))
What happens if you use a group size that does not divide evenly into the size of your input stream? No sweat! The last group just contains any remaining elements, however many they may be:
val xs = (1 to 10).toStream val grouped = xs.grouped(3) // grouped: Iterator[Stream[Int]] = // Iterator( // Stream(1, 2, 3), Stream(4, 5, 6), // Stream(7, 8, 9), Stream(10))
Where is this useful? Well, you can take my paging example from my Scala Saturday post on Stream.drop
and make it slightly clearer without the (page - 1) * perPage
arithmetic:
case class Book(title: String, author: String) val books = Stream( Book("Wuthering Heights", "Emily Bronte"), Book("Jane Eyre", "Charlotte Bronte"), Book("Agnes Grey", "Anne Bronte"), Book("The Scarlet Letter", "Nathaniel Hawthorne"), Book("Silas Marner", "George Eliot"), Book("1984", "George Orwell"), Book("Billy Budd", "Herman Melville"), Book("Moby Dick", "Herman Melville"), Book("The Great Gatsby", "F. Scott Fitzgerald"), Book("Tom Sawyer", "Mark Twain") ) val perPage = 3 val page = 3 val records = books.grouped(perPage) .drop(page - 1) .next // records: scala.collection.immutable.Stream[Book] = // Stream(Book(Billy Budd,Herman Melville), // Book(Moby Dick,Herman Melville), // Book(The Great Gatsby,F. Scott Fitzgerald))
This time, instead of having to calculate the number of elements to skip in order to skip n pages, you first use Stream.grouped
to turn the stream into a paged recordset; each “page” is n records long. Then drop page - 1
pages in order to get to the page of records you want. Finally, calling Iterator.next
is necessary because, remember, Stream.grouped
turns a flat stream into a stream of streams.
I will admit that I find it irritating that Stream.grouped
returns something that does not have a head
method. Calling Iterator.next
, while just as easy, is inconsistent with collection semantics. It seems to me that Stream.grouped
ought to return a collection rather than an iterator. Perhaps there was once a reason for returning an iterator instead of a collection, but it would be nice if we could fix that.