Scala Saturday – Array.last and Array.lastOption

Last week, we looked at List.headOption. If you don’t need the first item in a list, but rather the last item, the counterpart of List.head is List.last. Likewise, the counterpart of List.headOption is List.lastOption.

Recall the code example from last week:

case class Salesman(name: String, sales: BigDecimal)

def findTopSalesman (salesmen : List[Salesman]) = {
  salesmen.filter { _.sales >= 10000 }
          .sortBy { -_.sales } // descending
          .headOption
}

val sales = List(
  Salesman("Joe Bob", 9500),
  Salesman("Sally Jane", 18500),
  Salesman("Betty Lou", 11800),
  Salesman("Sammy Joe", 6500)
)

val top = findTopSalesman(sales)
// top: Option[Salesman] = 
//   Some(Salesman(Sally Jane,18500))

The List.sortBy call (highlighted above) sorts the sales records in a descending fashion so that the first record is the top salesman. What if it makes more sense to you to sort the records in an ascending fashion and take the last record? With List.lastOption, you can:

case class Salesman(name: String, sales: BigDecimal)

def findTopSalesman (salesmen : List[Salesman]) = {
  salesmen.filter { _.sales >= 10000 }
          .sortBy { _.sales } // ascending
          .lastOption
}

val sales = List(
  Salesman("Joe Bob", 9500),
  Salesman("Sally Jane", 18500),
  Salesman("Betty Lou", 11800),
  Salesman("Sammy Joe", 6500)
)

val top = findTopSalesman(sales)
// top: Option[Salesman] = 
//   Some(Salesman(Sally Jane,18500))

This is a trivial example: I mean, you can sort the records however you wish; they’re your records! But what if you receive the recordset from elsewhere—an API that is outside your control, for instance—and it is already sorted in an ascending fashion. It is probably better to accept the recordset as is and just take the last item rather than to sort it again. Which brings me to another couple of other points …

This post claims to be about Array.last and Array.lastOption. Why all the talk about List.lastOption?

First, as you have probably noticed already, just about any method available on one sequential collection is available on them all. That is, if there’s a List.last, for example, then there’s also a Seq.last, an Array.last, and a Stream.last.

Second, I want to point out a potential pitfall of using last and lastOption. Both the size and type of the collection can affect the performance of your program or even crash it.

Arrays give you O(1) access to their elements. (In case you’re not familiar with it, that’s called “Big O notation.” It’s a way of expressing how long an algorithm takes to execute.) That is, arrays give you nearly instant access to any element—first, last, somewhere in the middle—doesn’t matter.

Lists and sequences, on the other hand, give you O(n) access to their elements. That is, the more items in the list/sequence, the longer it takes to get to the one you want because the machine always has to start at the first element and iterate through every single one until it gets to the one you want. No big deal if there are only 100 elements, but if there are 10,000,000 elements, fetching the last element will take a while.

Furthermore, streams can be infinite. If you call Stream.last or Stream.lastOption on an infinite sequence, your program will crash:

// This sequence starts at one and just keeps going
val ns = Stream.from(1)
val notGood = ns.last
// java.lang.OutOfMemoryError: GC overhead limit exceeded

You don’t have to eschew last and lastOption. Just take into account what kind of collection you’re calling them on. Array.last and Array.lastOption are perfectly safe. (Well, do remember that Array.last throws an exception if the array is empty, but with regard to performance, it’s fine.) But before you call last or lastOption on a list or a stream, make sure you know how big it is, or you could, as they say, shoot yourself in the foot.

Leave a Reply

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