Add `List.first!/1` and `List.last!/1`

Problem

Currently, List.first/2 and List.last/2 return a default value (or nil) when the list is empty. However, there are cases where an empty list represents a bug in the program, and silently returning nil can lead to confusing errors later in the code.

Other modules in Elixir provide bang variants for similar situations:

  • Map.fetch!/2 raises when a key is not found
  • List.keyfind!/3 raises when a key is not found
  • Enum.fetch!/2 raises when an index is out of bounds

Proposal

Add List.first!/1 and List.last!/1 functions that raise an ArgumentError when called with an empty list.

List.first!([1, 2, 3])  #=> 1
List.first!([])         #=> ** (ArgumentError) trying to get the first element of an empty list

List.last!([1, 2, 3])   #=> 3
List.last!([])          #=> ** (ArgumentError) trying to get the last element of an empty list

Use case

This is useful when you have a list that should never be empty at a certain point in your code. Using the bang variant makes the intent explicit and provides a clear error message if the assumption is violated, rather than propagating nil through the system.

How to do without it today?

1. Pattern matching (most idiomatic for first element)

[head | _] = list
# raises MatchError if empty

2. hd/1 for first element

hd(list)
# raises ArgumentError: argument error

3. Enum.fetch!/2

Enum.fetch!(list, 0)   # first
Enum.fetch!(list, -1)  # last

4. Case expression

case list do
  [head | _] -> head
  [] -> raise ArgumentError, "empty list"
end

However, none of these are ideal:

  • hd/1 exists for first, but there’s no equivalent for last
  • Enum.fetch!(list, -1) works but is O(n) twice (once to get length, once to traverse) and less readable
  • Pattern matching doesn’t work for last element without reversing first
  • Case expressions are verbose for a simple operation

List.last!/1 fills a real gap. List.first!/1 provides symmetry and a clearer error message than hd/1.

Why ArgumentError?

The implementation raises ArgumentError. Here’s how it compares to similar functions:

Function Error raised Reason
hd([]) ArgumentError Invalid argument (not a nonempty list)
Map.fetch!(%{}, :a) KeyError Key not found in map
Enum.fetch!([], 0) Enum.OutOfBoundsError Index out of bounds
List.keyfind!([], :a, 0) KeyError Key not found
Enum.random([]) Enum.EmptyError Empty enumerable

Arguments for ArgumentError

  1. Consistency with hd/1: The closest existing function is hd/1, which raises ArgumentError for an empty list. List.first!/1 is essentially a more readable hd/1.
  2. Semantic fit: The error is about passing an invalid argument (an empty list where a non-empty one is required). This is the textbook definition of ArgumentError.
  3. Module boundary: List is not Enum — using Enum.EmptyError in List could be seen as a layer violation.

Alternative: Enum.EmptyError

Enum.EmptyError exists for “operation on empty collection” errors and is used by Enum.random/1, Enum.min/1, Enum.max/1, etc. It’s semantically closer to this situation.

Conclusion

Both are defensible. ArgumentError was chosen for consistency with hd/1 and existing List module conventions.

I already have a PR done locally.

3 Likes

Ironically Erlang’s :lists.last() actually raises.

4 Likes

Tangentially, I also find List.wrap(nil) returning [] extremely non-intuitive. I sorta get it but also not. Is there precedent?

1 Like

We can found in the source, on main:

first/2 has been introduced in Elixir v1.12.0, while first/1 has been available since v1.0.0.

last/2 has been introduced in Elixir v1.12.0, while last/1 has been available since v1.0.0.

The behavior was, on v1.0:

@spec first([elem]) :: nil | elem when elem: var
def first(), do: nil
def first([h|_]), do: h
@spec last([elem]) :: nil | elem when elem: var
def last(), do: nil
def last([h]), do: h
def last([_|t]), do: last(t)

Pushing breaking changes is sadly often not reasonable :sweat_smile:

2 Likes

This is quite common around the maybe monad, where no value maps to [] and a value to [value]. In some languages [] and nil would even be considered the same value. Erlang internally represents [] as a datatype called nil. … but more likely in ruby you have Array(nil) => [].

3 Likes

So that I can write

Enum.flat_map(items, fn item -> List.wrap(if important?(item), do: item.price) end)

Instead of:

items
|> Enum.filter(&important?/1)
|> Enum.map(&(&1.price))
3 Likes

I actually was not aware of that Array(nil) in Ruby did that, either because I never came across it but more likely (since I used Array() a ton in Ruby) that I’d explicitly check for nil. But that is certainly good precedent for Elixir. Otherwise ya, that’s all fair. I’ve always thought of nil as living outside of the “monadic” idioms but that makes sense. Thanks for the answer!

Also in Common Lisp, nil and (), the empty list, can be used interchangeably. nil is also a generalised boolean, standing for false.

1 Like

What was the point of first/0 and last/0 ?

The point is List.first/1 and List.last/1 do not return an :error but a nil if the given list if empty, which is error prone.

I haven’t found List.first/0 or List.last/0 in the repo’s history…

1 Like

I believe the mailing list is the correct place for these sorts of suggestions.

1 Like

I really don’t see the reason for a raising version. When the list is empty then there’s no first or last element so nil is exactly what I’d expect. I’d even argue that a non-empty list is a separate data structure because we can’t express the constraint in the type system.

On a not statically typed language, where we can’t distinguish the default nil from a real nil that was in the list, I hate that I have to build my own stuff around it, by matching on the list first, rather than relying on the stdlib.

Code like the following is something I often have in my projects.

def first([]), do: {:error, :empty}
def first(l), do: {:ok, List.first(l)}

I prefer a matchable variant over a raising one though.

1 Like

We have recently discussed it here: Typing lists and tuples in Elixir - The Elixir programming language

The idea is that hd will require a non-empty list to be given as argument (so in statically typed code you won’t get a runtime failure), while a potential List.first! and List.last! will imply a runtime error if an empty list is given (hence the bang). Given we were already planning to have those, please go and submit a PR, although I’d say it is also desirable to speed up Enum.fetch!(list, -1).

I’d say it is redundant to have List.first (or similar) that return a tuple given you can directly match on the list instead:

case List.fetch_first(list) do
  {:ok, first} -> first
  :error -> ...
end

vs

case list do
  [first | _] -> first
  [] -> ...
end

The second case is simpler, faster and clearer (IMO). So the List.first should either return a default (so you can skip the case) OR raise. The tuple case is not valuable.

4 Likes

@pierrelegall @jswanner the post I respond to specifically shows 0 arity functions. Probably a formatting error and not the actual code.

Otherwise on the topic I don’t really see the point of List.first!. Isn’t hd commonly known and used?

That’s great!

Would the type of List.last be (excuse my pseudocode) fn(non_empty_list(element)) → element | fn([]) → nil? And then the compiler would warn you if you forget to handle the empty list case?

The PR has been merged into Elixir main branch.

See: Add `List.first!/1` and `List.last!/1` by pierrelegall · Pull Request #15082 · elixir-lang/elixir · GitHub.

6 Likes

Congrats on your first elixir contribution :tada:

2 Likes