[Proposal] Add List.prepend and List.append to easily pipe this operations

176 views
Skip to first unread message

Almir Neto

unread,
May 16, 2025, 6:24:25 PMMay 16
to elixir-lang-core
Today, if you want to pipe an append or a prepend, you must use then/2 to achieve this. This can be useful if the first elements of a list must be inserted in order through a pipe. Let me show an example:

dto.ledger.accounts
|> Enum.reject(fn %{account_number: acc_number} ->
acc_number in [debit_acc_number, credit_acc_number]
end)
|> then(fn accounts -> [get_ordered_debit_accounts() | accounts])
|> then(fn accounts -> [get_ordered_credit_accounts() | accounts] end)
|> then(fn accounts -> accounts ++ retrieve_unordered_accounts() end)
|> then(&Map.replace(dto.ledger, :accounts, &1))

Obviously, that one is too simple and can be done in one line without losing too much readability, like this:

dto.ledger.accounts
|> Enum.reject(fn %{account_number: acc_number} ->
acc_number in [debit_acc_number, credit_acc_number]
end)
|> then(fn accounts -> [get_ordered_debit_accounts(), get_ordered_credit_accounts() | accounts])
|> Kernel.++(retrieve_unordered_accounts())
|> then(&Map.replace(dto.ledger, :accounts, &1))

But it could be cleaner if it could just do this:

dto.ledger.accounts
|> Enum.reject(fn %{account_number: acc_number} ->
acc_number in [debit_acc_number, credit_acc_number]
end)
|> List.prepend(get_ordered_debit_accounts())
|> List.prepend(get_ordered_credit_accounts())
|> List.append(retrieve_unordered_accounts())
|> then(&Map.replace(dto.ledger, :accounts, &1))

So I think that the implementation of this can just perform a "remap" of the ++ operator and the [e | list] expression.

def append(list, element) when is_list(list), do: list ++ [element]
def prepend(list, element) when is_list(list), do: [element | list]


This feature is more of a syntactic sugar than something innovative; it would be a way to keep the code more vertical and easier to read for some.

I will be glad to send a PR.


Jean Klingler

unread,
May 24, 2025, 3:21:22 AMMay 24
to elixir-l...@googlegroups.com
My concern is that adding List.append/2 would send the wrong signal, since it has the wrong performance characteristics.
It is a pattern that people coming from an imperative need to unlearn so we shouldn't make it more convenient.
We also just deprecated Tuple.append/2.

While List.prepend/2 doesn't suffer this issue, I'm not sure the pipe-ability alone is enough to justify it, esp. since there is a first-class syntax for prepending: [h | t].
It feels to me that then/2 gives us the ability to use it with the pipe if we want to, with the flexibility of choosing what the first argument is:

... |> then(&[elem | &1])

... |> then(&[&1 | list])


--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/elixir-lang-core/ed3ccb00-5069-4b66-9d6f-132eadc7ea90n%40googlegroups.com.

Dallin Osmun

unread,
May 27, 2025, 6:16:30 PMMay 27
to elixir-lang-core
This was a hurdle for me when I first started coding in Elixir. Whenever I wanted to update a data structure, I'd look through the standard library for the appropriate function.

Instead of using `list1 ++ list2` I looked for `List.concat/2`. When that didn't exist I'd look for and find `Enum.concat/2`. Similarly, it rarely occurred to me to use `[elem | list]`. I instead looked for `List.prepend/2`, then `Enum.prepend/2`. When neither of those existed, I had to resort to elixirforum/slack/stackoverflow/etc to lead me to the `[elem | list]` syntax.

All that to say, I'm torn on this proposal. On the one hand it could help those new to elixir get un-stuck faster. On the other hand, those same coders wouldn't be pushed to learn the `[a | b]` syntax.

Christopher Keele

unread,
May 30, 2025, 5:34:12 PMMay 30
to elixir-lang-core
Some thoughts in no particular order:

- Generally, the Elixir stdlb does not support multiple ways of doing things, especially with operators (see: no Integer.add/2, etc). I generally like this.
- The other container that uses a < ... | ... > update syntax (maps) does have a dedicated function for this (Map.update). I also like this because piping Map operations is so common.
- After a decade of Elixir I still reach for List.concat and List.prepend every few months. I'm intimate with the operators and not an overzealous piper, but when piping I still expect it to be there. If I wrote Elixir exclusively then I would probably fully adjust.

> So I think that the implementation of this can just perform a "remap" of the ++ operator and the [e | list] expression.

I agree, in fact I would implement this as a compile-time inline. (I believe just invoking :erlang is insufficient, IIRC there is something going on in the .erl compiler as well to enable this performance characteristic. (Found it—here.))

> Similarly, it rarely occurred to me to use `[elem | list]`. I instead looked for `List.prepend/2`, then `Enum.prepend/2`. When neither of those existed, I had to resort to elixirforum/slack/stackoverflow/etc to lead me to the `[elem | list]` syntax.

One could argue this is working as intended. Preferentially, though, you would have first discovered the correct operators by consulting the List moduledocs when you could not find the expected function.

However, we could implement the expected functions here to 1) support the pipe use-case and 2) have an indexable and discoverable point in the documentation to direct folk to the operators, at least for concat and prepend. This is my preference. Implementing an append gives us another discoverable place to advise against it, so there's an argument there too I guess.

I also wonder if it would be possible to somehow attach metadata to the list operators so that they show up when searching for the expected function equivalent. The current situation is pretty poor for these operators.

Today, typeahead for "prepend" shows nothing related. A full search for "prepend" eventually shows something almost relevant in 11th place (the ++ docs telling you when to prefer [ | ]). The List moduledoc instructions for [ | ] show up in 15th place. There is no entry for the actual infix list concat operator in Kernel or SpecialForms as it is context-dependent. Map and tuple literals have entries in SpecialForms (%{} and {} respectively), but there is no similar entry for list literals ([]).

Similarly, there are no related results for ++ when searching "concat". "concatenation" brings up the relevant operator in 10th place. Neither turn up anything related in the typeahead.

Overall, I'm pro List.prepend/2 and List.concat/2. Documentation search aside, I think the friction of discovering the operators could be reduced by mentioning them in dedicated function docs, and alongside the pipe-usecase and parallels to the Map APIs, there is a sufficiently compelling case to be made to abandon the one-way-to-do-it principle here.

Conversely, reaching for List.append/2 should produce some form of friction and lead the programmer through a learning experience. Whether or not the "resort to elixirforum/slack/stackoverflow/etc" experience is a productive sort of friction, I don't know.

I think we should investigate improving the hexdocs situation for these operators regardless.

Finally, if these are implemented as inlined-at-compile-time to their operator forms, it occurs to me we could have the formatter auto-correct non-pipe usage to their operator forms. I'm a little wary of that, but could be convinced otherwise.

José Valim

unread,
May 30, 2025, 5:47:40 PMMay 30
to elixir-l...@googlegroups.com
We do have Enum.concat/2 and typically we don't repeat functions in the Enum module within the List module.

My biggest concern is that I don't think piping to prepend leads to easier to read code here, because you have to reverse the order in your head (the order you read the lines is the opposite of the order it will appear in the list)

Given this code:


    dto.ledger.accounts
    |> Enum.reject(fn %{account_number: acc_number} ->
      acc_number in [debit_acc_number, credit_acc_number]
    end)
    |> then(fn accounts -> [get_ordered_debit_accounts() | accounts])
    |> then(fn accounts -> [get_ordered_credit_accounts() | accounts] end)
    |> then(fn accounts -> accounts ++ retrieve_unordered_accounts() end)
    |> then(&Map.replace(dto.ledger, :accounts, &1))

I would rather write (keeping roughly the same structure to make it easier to compare):
   
    accounts = 
      Enum.reject(dto.ledger.accounts, fn %{account_number: acc_number} ->

        acc_number in [debit_acc_number, credit_acc_number]
      end)

    accounts = [get_ordered_credit_accounts(), get_ordered_debit_accounts() | accounts]
    Map.replace(dto.ledger, :accounts, accounts ++ retrieve_unordered_accounts())

Especially if I am calling functions with *ordered* in the name.   




Christopher Keele

unread,
May 30, 2025, 6:35:02 PMMay 30
to elixir-lang-core
> We do have Enum.concat/2 and typically we don't repeat functions in the Enum module within the List module.

I would argue that it wouldn't be a repetition as there would be a material difference between them: List.concat would obey ++ semantics, meaning 1) it would only work with actual lists as a first argument rather than any Enumberable and runtime error otherwise, but hopefully be caught by typing systems; and 2) allow construction of improper lists via non-list non-Enumberables in the second argument. Sometimes that's desired behaviour but it does feel footgunny now that I say it, though. Certainly something that would need to be outlined in the function docs if implemented, it does give me some pause.

> My biggest concern is that I don't think piping to prepend leads to easier to read code here, because you have to reverse the order in your head (the order you read the lines is the opposite of the order it will appear in the list).

I agree the provided example can be confusing way to model many solutions, and may be an indicator of a need for a simpler refactor. But I think your point is orthogonal to the List.prepend discussion—the same problem exists when chaining [ | ] through intermediate variables. It's a (valid) argument against prepending at all, not against this API. For example, an experienced Elixirist will know they'll need to pipe the result of a recursive defp-function-implemented reduce into a :lists.reverse at the end, regardless of the API they used to prepend along the way—and sometimes piping into a prepend would save noisy intermediate variables along the way. I do not see the availability of List.prepend greatly influencing developers to build backwards lists more often when inappropriate, but we may disagree there.

FWIW my usecase is almost never chaining multiple prepends—usually I reach for it as a finisher when map/reducing a series of transforms on a list of dynamic values, and adding a special case/hardcoded value at the end; same with the proposed List.concat. The ergonomics of the pipe operator truly shine in these map/reduces, so it can be painful to have to create an intermediate variable just to use the literal operators to conclude building the desired list. As an example, mapping over a database table to create options for a select, and wanting to prepend the null option that is not modeled in the source data set.

Billy Lanchantin

unread,
Jun 2, 2025, 4:17:37 PMJun 2
to elixir-l...@googlegroups.com
I'm for adding `List` functions that make piping easier and help with discoverability. Though I agree they won't often be the best choice for the reasons discussed.

I will note that I think there are actually 4 operations that need to be considered if the goal is pipe-ergonomics (names TBD):

defmodule List do
# For a new single element
def prepend(list, x), do: [x | list]
def append(list, x), do: list ++ [x]
# For a new list
def prepend_list(list, new_list), do: new_list ++ list
def append_list(list, new_list), do: list ++ new_list
end

I think a clear distinction between functions that take an element and functions that take a list is crucial. In this thread I saw a few instances where `x ++ list` was needed but `[x | list]` was used instead, which I think highlights that the mistake is easy to make.

As for needing both `prepend_list` and `append_list`, making `++` pipe-able is only convenient if both argument orders are available. Otherwise, you have to drop back into intermediate variables or `then` to get a `prepend_list`.

I'll also note that I'm at like a 6/10 on this feature: I'm on the pro-side of the fence but not by a lot.

benjamin...@gmail.com

unread,
Jun 4, 2025, 4:53:31 PMJun 4
to elixir-lang-core
You can pipe into append and prepend today with https://www.erlang.org/doc/apps/stdlib/lists.html#append/2 and https://www.erlang.org/doc/apps/stdlib/lists.html#flatten/2

This feels like more of a documentation issue than something missing in the stdlib.

Billy Lanchantin

unread,
Jun 4, 2025, 5:15:47 PMJun 4
to elixir-l...@googlegroups.com
benjamin...@gmail.com good point about `:lists.append/2` being available. But I don't see how you can prepend with `:lists.flatten/2` in a way that's pipe-able. Can you show an example?


benjamin...@gmail.com

unread,
Jun 5, 2025, 3:26:16 AMJun 5
to elixir-lang-core
[el|list] is pipeable as well, and if you are working with Erlang strings/binaries you can use https://www.erlang.org/doc/apps/stdlib/string.html#pad/4

I think it’s important not to stare us self blind on a specific syntax. All the functions there are currently available helps highlighting that the lists we have in the BEAM are double linked list. 

When I first read this thread all I could think about was that it could create more confusion for new developers who might believe that our lists are arrays. 

Although you can easily write your own macro or functions to deal with this, I don’t see the value in adding it to stdlib at this point. However, I do see value in expanding the documentation with examples on what to do in situations like these as they are excellent cases.

benjamin...@gmail.com

unread,
Jun 5, 2025, 3:35:36 AMJun 5
to elixir-lang-core
And we can’t forget about https://hexdocs.pm/elixir/List.html#insert_at/3
Reply all
Reply to author
Forward
0 new messages