> We could get cuter with the syntax by overloading guards:

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

It was pointed out to me that this would either have to work only with 
guard-compatible conditions or be wildly inconsistent with the rest of the 
language, so I think this syntax is out
On Friday, December 6, 2024 at 4:27:32 PM UTC-6 Christopher Keele wrote:

> > 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/0d8078fc-64a7-45ab-9195-40416b70c600n%40googlegroups.com.

Reply via email to