> One pattern I see repeated constantly in different apps developed by 
myself or others is adding values to a map conditionally or returning the 
map unchanged.

I agree this is a wart common with maps in particular (as the 
out-of-the-box update-often data structure), but the problem is not 
specific to the Map API; rather, conditional expressions in Elixir.

The intentional design decision for *if*-and-friends conditionals to honor 
lexical scoping was not originally part of the language, but added for 
consistency with other branching structures early on. So you in fact used 
to be able to just do  *map = %{}; if conditional, do: map = Map.put(map, 
:foo, :bar)*. Changing this was controversial at the time partially because 
of this knock-on effect of having to always exhaustively handle all 
branches of a conditional if assigning results directly to a variable (or 
otherwise only temporarily branching the control flow of the current scope).

TL;DR you have to do a lot more *foo = if ..., else: foo* to keep 
conditional lexical scoping consistent, and I'm in agreement with José that 
it's that slightly irritating *else: foo* that (if anything) should be 
solved holistically at the core language level, rather than extending 
individual data-structure's APIs.
------------------------------

I don't think we can "solve" *else: foo* without discussing why it's a 
problem. I can think of two rationales, but interested in other opinions:
1. Accidentally omitting it can lead to unintentional nil assignments.
2. It is syntactically noisy for what it accomplishes (from the 
programmer's perspective, literally "nothing"—as in, leaving the assignment 
in question the same).

In my experience, 1. is not a huge issue, but others may have stronger 
opinions. It's 2. that makes it a wart. The problem is that there is not 
much more syntax to strip away from *if*: no else clause means *nil* and 
that cannot reasonably change, and the rest of the macro does not 
understand that there is a "subject" being assigned to for it to choose to 
return unchanged. I would propose either *introducing a new conditional 
assignment macro* (as discussed a little here already), or *consider 
additional syntaxes for conditionals* that makes it a little easier 
visually to ignore the fallback case.

In either case, as José points out, we need to consider 3 components: a 
*subject* to or to not update, a *condition*, and an *action*.
------------------------------
New Macro

I agree with the criticisms of *then_if*. I would rather see something 
explicitly about updating the subject. Say, a hygine-modifying 
*update_when(subject, 
condition, fn/block)* that required a variable reference subject. Ex:

update_when(map, condition, &Map.put(&1, :key, value))

or

update_when(map, condition) do
Map.put(map, :key, value)
end

The pipe-ability of this is limited by design, but this could still work 
with *then*:
changeset
|> do_some_checking()
|> then(fn changeset -> 
update_when(changeset, changeset.valid) do
do_more(changeset)
end
end)
|> do_something_else()

Honestly, not in love with this, but I'm slow to warm to these things. We 
could get cuter with the syntax by overloading guards:

update map when condition do
Map.put(map, :key, value)
end

Reads better, technically parses, but kind of inconsistent with other guard 
constructs conceptually. Also, how would piping work? Is there a way for 
this to make sense in a larger pipeline:

map
|> update when condition do
Map.put(map, :key, value)
end
------------------------------
Changing if

Since *if* cannot be fundamentally aware of a subject, it would have to 
have a place to specify the default fallback, which *else* already does in 
this situation; it's as semantically dense as it can be. To alleviate the 
noise the fallback block introduces, one option would be to have the *if* 
macro accept optional keyword arguments before the block, merging them 
together, allowing hoisting the trivial *else* case inline with the 
condition, independent of the consequent, to create a denser syntax:

map = if condition, else: map do
Map.put(map, :key, value)
end

This also cannot really be piped through without *then*, but otherwise 
reads (slightly) nicer than the base case:

changeset
|> do_some_checking()
|> then(fn changeset -> 
if changset.valid, else: changeset do
do_more(changeset)
end
end)
|> do_something_else()

It's a really small change that I think pretty much fully addresses the 
syntactic noise problem. It does lead to this rather odd formulation I'm 
not sure about:

condition
|> if(else: map) do
Map.put(map, :key, value)
end
------------------------------
Changing case/cond

Of course, we do already have a conditional expression with a semantic 
notion of a subject, *case*. However, there's no specific syntax for 
referencing it, outside clause heads, so the programmer would have to 
provide it again, similar to the fallback *_ -> subject* construct today:

map = case map do
%{} -> Map.put(map, :key, value)
_ -> map
end

I think this is orthogonal to the problem we are trying to solve, but if we 
went the *if(conditional, else: fallback) do* route, we'd need to consider 
if we should extend *case*/*cond* with similar semantics for consistency's 
sake, so:

case map, else: map do
%{key: old_value} -> Map.put(map, :key, old_value + 1)
%{} -> Map.put(map, :key, 0)
end

Of course the problem here is that implies the existence of general *else* 
clauses in those constructs:

case map do
%{key: old_value} -> Map.put(map, :key, old_value + 1)
%{} -> Map.put(map, :key, 0)
else
map
end

We could implement support this and have it compile down to the correct *_ 
-> map* fallback case and warn/error if one was already provided (similarly 
with *true -> map* for *cond*), but generally, not a fan of so many ways to 
do the same thing.
------------------------------
This is less an argument for adding *else* to these constructs, and more an 
argument for calling the keyword argument to *if* *something else* less 
likely to be confused with block semantics. So I'd say that I personally am 
warmest on the *if*  proposal alone, and am open to calling the keyword 
something different and merging it in with the block with the same *else* 
duplication 
warnings/errors we'd need regardless of name, like:

map = if condition, fallback: map do
Map.put(map, :key, value)
end

The *update subject when condition do* syntax sugar reads very nicely, but 
feels like it would lead to confusion down the line.
On Friday, December 6, 2024 at 11:01:28 AM UTC-6 jimf...@gmail.com wrote:

> then_if has no meaning to me and breaks my brain. 
>
> Seems not to flow with other pipeline commands. 
>
> Dr. Jim Freeze, Ph.D.
> ElixirConf®
> ElixirConf.com
> ElixirConf.eu
> (m) 512 949 9683 <(512)%20949-9683>
>
>
> On Fri, Dec 6, 2024 at 10:58 AM José Valim <jose....@gmail.com> wrote:
>
>> Thank you Zach. When I wrote the proposal I felt it was missing something 
>> still and I think you nailed it.
>>
>> Passing two anonymous functions would help with the pipeline but it feels 
>> it would be detrimental to other cases.
>>
>>
>>
>> *José Valimhttps://dashbit.co/ <https://dashbit.co/>*
>>
>>
>> On Fri, Dec 6, 2024 at 17:41 Zach Daniel <zachary....@gmail.com> wrote:
>>
>>> Despite typically being a "put it in the standard library" guy, I don't 
>>> think that `then_if` actually composes as well as it looks like it does on 
>>> the tin due to the fact that `then` is often used in pipelines, where some 
>>> transformation has happened and you want to check a condition *on that 
>>> result*. For example:
>>>
>>> ```elixir
>>> changeset
>>> |> do_some_checking()
>>> |> then_if(<is_valid>, &do_more/1)
>>> ```
>>>
>>> I think that `then` is kind of "already" the composition tool that we 
>>> need for expressive pipes.
>>>
>>> ```elixir
>>> changeset
>>> |> do_some_checking()
>>> |> then(fn changeset -> 
>>>   If changeset.valid do
>>>       do_more(changeset)
>>>   else
>>>      changeset
>>>   end
>>> end)
>>> ```
>>>
>>> I can see an argument that it is very verbose, but its also about as 
>>> flexible as it can get. My suggestion would be to, if added, have `then_if` 
>>> take a function as its first argument.
>>>
>>> ```elixir
>>> changeset
>>> |> do_some_checking()
>>> |> then_if(&(&1.valid?), &do_more/1)
>>> ```
>>>
>>>
>>> On Dec 6, 2024, at 10:30 AM, Ben Wilson <benwil...@gmail.com> wrote:
>>>
>>> Exploring what that looks concretely in this case:
>>>
>>> ```
>>> map
>>> |> other_stuff
>>> |> then_if(opts[:foo], &Map.put(&1, :key, value))
>>> ```
>>>
>>> I like it! Conditional map insert helper functions are definitely 
>>> something we've written over and over again in our code bases and while 
>>> it's easy to do, I think in some cases this is cleaner looking than a 
>>> proliferation of `maybe_put_foo` functions.
>>>
>>> - Ben
>>>
>>> On Friday, December 6, 2024 at 9:59:40 AM UTC-5 José Valim wrote:
>>>
>>>> Hi Juan!
>>>>
>>>> My initial gut feeling is that this approach does not scale. What if 
>>>> you want to delete a key conditionally? Should we have delete_if?
>>>>
>>>> It feels a more general approach would be to introduce `then_if`:
>>>>
>>>> then_if(subject, condition?, function)
>>>>
>>>> Or similar. :)
>>>>
>>>> *José Valimhttps://dashbit.co/ <https://dashbit.co/>*
>>>>
>>>>
>>>> On Fri, Dec 6, 2024 at 3:27 PM Juan Manuel Azambuja <
>>>> ju...@mimiquate.com> wrote:
>>>>
>>>>> Hello,
>>>>>
>>>>> After working with Elixir for some time I have found myself repeating 
>>>>> some patterns when dealing with maps.
>>>>>
>>>>> One pattern I see repeated constantly in different apps developed by 
>>>>> myself or others is adding values to a map conditionally or returning the 
>>>>> map unchanged. This comes in different flavors:
>>>>>
>>>>> [image: Screenshot 2024-12-06 at 11.13.23 AM.png]
>>>>> or
>>>>> [image: Screenshot 2024-12-06 at 11.14.32 AM.png]
>>>>>
>>>>> When this pattern gets used enough in an app, it's normal to see it 
>>>>> abstracted in a MapUtils module that updates the map conditionally if a 
>>>>> condition is met or returns the map unchanged otherwise.
>>>>>
>>>>> My proposal is to include Map.put_if/4 which would abstract the 
>>>>> condition check and return the map unchanged if the condition is not met:
>>>>>
>>>>> [image: Screenshot 2024-12-06 at 11.17.21 AM.png]
>>>>>
>>>>> Enhancing the API by doing this will result in less code and more 
>>>>> readable solutions.
>>>>>
>>>>> Thanks for reading!
>>>>>
>>>>> -- 
>>>>> 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/ed7da716-b9f5-4f64-a77d-d32696326b9en%40googlegroups.com
>>>>>  
>>>>> <https://groups.google.com/d/msgid/elixir-lang-core/ed7da716-b9f5-4f64-a77d-d32696326b9en%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/e9e799a2-ad69-4791-bd9a-22bca327652fn%40googlegroups.com
>>>  
>>> <https://groups.google.com/d/msgid/elixir-lang-core/e9e799a2-ad69-4791-bd9a-22bca327652fn%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/61753088-63E3-4DA0-8CEF-925149D789C6%40gmail.com
>>>  
>>> <https://groups.google.com/d/msgid/elixir-lang-core/61753088-63E3-4DA0-8CEF-925149D789C6%40gmail.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/CAGnRm4JSE7vhfHukf2EZ6bmi4%3DNrfX28q3%2BKpQGZMgFoCM%3D%2BWg%40mail.gmail.com
>>  
>> <https://groups.google.com/d/msgid/elixir-lang-core/CAGnRm4JSE7vhfHukf2EZ6bmi4%3DNrfX28q3%2BKpQGZMgFoCM%3D%2BWg%40mail.gmail.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/a2b730c9-67e8-44d0-be8f-22ba4761fea7n%40googlegroups.com.

Reply via email to