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. On Fri, May 30, 2025 at 6:35 PM Christopher Keele <christheke...@gmail.com> wrote: > > 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. > > > On Friday, May 30, 2025 at 4:47:40 PM UTC-5 José Valim wrote: > >> 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. >> >> >> *José Valimhttps://dashbit.co/ <https://dashbit.co/>* >> >> >> On Fri, May 30, 2025 at 11:34 PM Christopher Keele <christ...@gmail.com> >> wrote: >> >>> 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 >>> <https://hexdocs.pm/elixir/Kernel.html#module-inlining>. (I believe just >>> invoking :erlang >>> <https://github.com/elixir-lang/elixir/blob/7b20c281d521aa7aa2ad2baa1e9ae6c579d79d0c/lib/elixir/lib/kernel.ex#L1590> >>> is insufficient, IIRC there is something going on in the .erl compiler as >>> well to enable this performance characteristic. (Found it—here >>> <https://github.com/elixir-lang/elixir/blob/41d1a721ad52e9fbc5f1770b74c3c1c31bccd2d0/lib/elixir/src/elixir_rewrite.erl#L89> >>> .)) >>> >>> > 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. >>> On Tuesday, May 27, 2025 at 5:16:30 PM UTC-5 Dallin Osmun wrote: >>> >>>> 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. >>>> >>>> On Saturday, May 24, 2025 at 1:21:22 AM UTC-6 sabi...@gmail.com wrote: >>>> >>>>> 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 >>>>> <https://hexdocs.pm/elixir/changelog.html#4-hard-deprecations> >>>>> 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]) >>>>> >>>>> >>>>> Le sam. 17 mai 2025 à 07:24, Almir Neto <almir.a...@gmail.com> a >>>>> écrit : >>>>> >>>>>> 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.* >>>>>> >>>>>> >>>>>> -- >>>>>> 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 >>>>>> <https://groups.google.com/d/msgid/elixir-lang-core/ed3ccb00-5069-4b66-9d6f-132eadc7ea90n%40googlegroups.com?utm_medium=email&utm_source=footer> >>>>>> . >>>>>> >>>>> -- >>> 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/37c4788d-662d-4de4-98e1-06913db75665n%40googlegroups.com >>> <https://groups.google.com/d/msgid/elixir-lang-core/37c4788d-662d-4de4-98e1-06913db75665n%40googlegroups.com?utm_medium=email&utm_source=footer> >>> . >>> >> -- > 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-core+unsubscr...@googlegroups.com. > To view this discussion visit > https://groups.google.com/d/msgid/elixir-lang-core/ef1c181b-513f-40ac-9f17-24cdfbb453b5n%40googlegroups.com > <https://groups.google.com/d/msgid/elixir-lang-core/ef1c181b-513f-40ac-9f17-24cdfbb453b5n%40googlegroups.com?utm_medium=email&utm_source=footer> > . > -- 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-core+unsubscr...@googlegroups.com. To view this discussion visit https://groups.google.com/d/msgid/elixir-lang-core/CABvJisMUxDw%2Byr67aR7Nm16%3DXeo3-g2MfiB7aTR1j7ry5w_4ug%40mail.gmail.com.