> 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.