pierrelegall

pierrelegall

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.

Most Liked

garrison

garrison

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

josevalim

josevalim

Creator of Elixir

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.

Where Next?

Popular in Proposals: Ideas Top

Rubas
Current Challenge When testing LiveViews that use assign_async, we can’t really operate on the async data. render_async(view) waits f...
New
mythicalprogrammer
Hello, since the 1.8rc0 was out, DaisyUI was noted to have the benefit of light and dark mode. From the elixir subreddit, it seem a few ...
New
boriscy
Hello I really love phoenix 1.8 and I think the new magic link generator is great but I find the remember me function unintuitive. I have...
New
lessless
Hi, There are a few writeups describing alternative config arrangements by a topic (or an OTP app): Configuring Phoenix apps: Two sma...
New
shahryarjb
When proposing or suggesting something please consider: As my experience implementing getBoundingClientRect as Phoenix.LiveView.JS funct...
New
superchris
Currently there is no out of the box way to support DOM Custom Events in phoenix. I’ve created a separate library to do this, but I’d lov...
New
c4710n
This is an idea for improving the workflow of iex -S mix phx.server The Problem and The Solution in use When running multiple Phoenix en...
New
dibok
Hi, I’m trying to use phoenix.js in my Qt QML project which has it’s own buildin JavaScript engine. Problem is that (what I googled so f...
New
New
zachdaniel
One thing I find is that I typically “start” with a LiveView even for static pages (in cases where I know I’m likely to add interactivity...
New

Other popular topics Top

sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 52673 488
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement