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

markevans
Hi! I feel like Phoenix is slightly missing a trick when it comes to front-end Javascript libraries like React, Svelte, etc. I feel tha...
New
woylie
We are seeing a lot of warning logs like this: navigate event to "https://someurl" failed because you are redirecting across live_sessio...
New
marcandre
I notice that most events have bindings (e.g. phx-keyup) but not the input event. The input event is the preferred way to interact with ...
New
bartblast
This could resolve to {[a: 1, b: 2]}. Was it ever considered to allow such syntax? Notice this: {:abc, a: 1, b: 2} and this: my_fun(:abc,...
New
pierrelegall
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...
New
bamorim
Story behind Recently, I gave a talk on a meetup about improving performance of Phoenix applications and the example app was a LiveView ...
New
sevensidedmarble
Hello all, Apologies if this has been proposed before I guess, but I have a very simple one: With the increasing importance of LV, I th...
New
zachdaniel
Something we do in various Ash packages is provide routes that the user forwards to in their own application. This is because we are deri...
New
MatinDevs
Currently, there are ongoing discussions about enhancing Phoenix LiveView, particularly focusing on improving performance and user experi...
New
rmoorman
Current situation Currently, the structure of the HTML returned by phoenix is determined by the layouts (components/layouts/[root,app].ht...
New

Other popular topics Top

malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 39467 209
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" => #BSON.ObjectId<58eb1a7a9ad169198c3dXXXX>, "email" => ...
New
vegabook
I’m brand new to Phoenix and I have stripped one of the demo applications to the bone. I just want to get an svg up on the screen. Here i...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement