What’s going on here?

Well… let’s look at the docs, shall we? According to them, lists:seq/2,3 …

returns a sequence of integers that starts with From and contains the successive results of adding Incr to the previous element, until To is reached or passed (in the latter case, To is not an element of the sequence). Incr defaults to 1 .

Alright, so… translating that into our examples: lists:seq(From, 3). returns a sequence of integers that starts with From and contains the successive results of adding 1 to the previous element, until 3 is reached or passed.

That’s fine with our first 3 examples, but what about when From is greater than 3 . Well, the doc continues and tells us what the failure conditions are for lists:seq/2,3 …

Failures: · If To < From — Incr and Incr > 0 . · If To > From — Incr and Incr < 0 . · If Incr =:= 0 and From =/= To .

I love that last scenario, which basically tells us that we can evaluate stuff like lists:seq(1, 1, 0). to get [1] . Awesome!

Anyway, in our case, since To =:= 3 and Incr =:= 1 , what we care about is that the function will fail if 3 < From — 1 , which is exactly what happens for all values of From such that From > 4 .

…but Why?

That’s OK, properly documented as I said and everything. But why? It seems a bit contradictory to say that the function will return a sequence of integers that starts with From and then let the function return an empty list when To == From — 1 . Why not just fail when To < From and Incr > 0 (and the analogous case for Incr < 0 ?

On the other hand, if lists:seq/3 is going to return an empty list, why not simply return empty lists always instead of having failure scenarios, i.e. why not just return an empty list when (To < From — Incr andalso Incr > 0) orelse (To > From — Incr andalso Incr < 0) ?

To figure this out, let’s go… to the code!

As you can see, validations for failure conditions are clearly stated and very specifically set outside of the recursive function that performs the list generation (i.e. seq_loop ):

seq(First, Last)

when is_integer(First), is_integer(Last), First-1 =< Last ->

seq_loop(Last-First+1, Last, []).

…and…

seq(First, Last, Inc)

when is_integer(First), is_integer(Last), is_integer(Inc) ->

if

Inc > 0, First - Inc =< Last;

Inc < 0, First - Inc >= Last ->

N = (Last - First + Inc) div Inc,

seq_loop(N, Inc*(N-1)+First, Inc, []);

Inc =:= 0, First =:= Last ->

seq_loop(1, First, Inc, [])

end.

The reason for those validations lies in the way seq_loop functions work. They’re clearly not implemented in a naïve way, i.e.…

seq_loop(Item, Last, Acc) when Item > Last -> reverse(Acc);

seq_loop(Item, Last, Acc) -> seq_loop(Item+1, Last, [Item|Acc])

They’re implemented in a more optimized fashion. Instead of counting up from First to Last, they count down from the last element on the list to 0. That way seq_loop can count 4-by-4, then 2-by-2 and more importantly, avoid a call to reverse/1 in the end.

But that recursion style changes the base cases for the recursion. While in the naïve approach above, the base case is when Item > Last , the actual implementation has 2 base cases: N == 1 and N == 0 . So, in the naïve method, if you start with a From that is already larger than Last , the function will just return an empty list. With the actual seq_loop functions, if we remove the initial validation in seq , and we provide a From that would generate a negative list length (e.g. Last-From+1 < 0 ), seq_loop will fail with a function_clause error since it has no clause to handle negative values for N .

What the authors of seq did was just raise that same error before calling seq_loop . By doing that, they’re hiding from you (the user) the implementation details of the functions. The errors are raised from seq/2,3 , not from seq_loop/3,4 . I think that’s a good practice: validating user input at the API level lets you implement internal functions assuming that their input is already valid.