Scala Saturday – The Stream.grouped Method

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.