pierrelegall

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 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.

Showing Posts 1 to 10

garrison

garrison

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

sodapopcan

sodapopcan

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

pierrelegall

pierrelegall OP

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:

LostKobrakai

LostKobrakai

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) => [].

derek-zhou

derek-zhou

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))
sodapopcan

sodapopcan

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!

krasenyp

krasenyp

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

lud

lud

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

pierrelegall

pierrelegall OP

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.

jswanner

jswanner

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

Where Next? Top

Trending in Proposals: Ideas Top

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

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews