F# Friday – Array.last and Array.tryLast

Last week, we looked at List.tryHead. 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.tryHead is List.tryLast.

Recall the code example from last week:

type Salesman = { Name : string
                  Sales : decimal }
 
let findTopSalesman salesmen =
    salesmen
    |> List.filter (fun s -> s.Sales >= 10000M)
    |> List.sortBy (fun s -> -s.Sales) // descending
    |> List.tryHead

let sales =
    [
        { Name = "Joe Bob"; Sales = 9500M }
        { Name = "Sally Jane"; Sales = 18500M }
        { Name = "Betty Lou"; Sales = 11800M }
        { Name = "Sammy Joe"; Sales = 6500M }
    ]
 
let top = findTopSalesman sales
// val top : Salesman option = 
//   Some {Name = "Sally Jane";
//         Sales = 18500M;}

The List.sortBy call (highlighted above) sorts the sales records in a descending fashion so that the first record would be 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.tryLast, you can:

type Salesman = { Name : string
                  Sales : decimal }
 
let findTopSalesman salesmen =
    salesmen
    |> List.filter (fun s -> s.Sales >= 10000M)
    |> List.sortBy (fun s -> s.Sales) // ascending
    |> List.tryLast

let sales =
    [
        { Name = "Joe Bob"; Sales = 9500M }
        { Name = "Sally Jane"; Sales = 18500M }
        { Name = "Betty Lou"; Sales = 11800M }
        { Name = "Sammy Joe"; Sales = 6500M }
    ]
 
let top = findTopSalesman sales
// val top : Salesman option = 
//   Some {Name = "Sally Jane";
//         Sales = 18500M;}

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.tryLast. Why all the talk about List.tryLast?

First, one of the things the F# team did in F# 4.0 was to normalize the sequential collections modules. Now just about any function available in one sequential collection module is available in them all. That is, if there’s a List.last, for example, then there’s also a Seq.last and an Array.last.

Second, I want to point out a potential pitfall of using last and tryLast. 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, sequences can be infinite. If you call Seq.last or Seq.tryLast on an infinite sequence, your program will crash:

let init n = n + 1
// This sequence starts at one and just keeps going
let ns = Seq.initInfinite init
let notGood = ns |> Seq.last
// Unhandled Exception: System.InvalidOperationException: 
//   Enumeration based on System.Int32 exceeded 
//   System.Int32.MaxValue.

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

One thought on “F# Friday – Array.last and Array.tryLast”

Leave a Reply

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