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. On Monday, June 2, 2025 at 10:17:37 PM UTC+2 william.l...@cargosense.com wrote: > 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 <christ...@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-co...@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/db899a13-2a01-4d0a-bd06-881541d51d4bn%40googlegroups.com.