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.

Reply via email to